- 建档重读人物名单时保留旧名单,失败也不卡在加载画面(BUG-1455) - 对话右栏改用自己的拖宽把手,侧栏与右栏都能用方向键(BUG-1456/1457) - 启程清单胶囊和首页一起出来,读状态时用上次的样子占位(BUG-1458) - 起步第一问与别人的天空过场读一次已算好的星盘取星座,上升只在不会换座时用(BUG-1459,产品 10-27 同意) - 补回删减的断言与三栏说明;登录代码改回原样,替身放进测试;删旧校正画面的无用样式 - 首屏 +7.54% 产品已接受,写进 DESIGN Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
383 lines
22 KiB
TypeScript
383 lines
22 KiB
TypeScript
import "./install-test-document";
|
||
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
import React from "react";
|
||
import { toast } from "sonner";
|
||
import { ReportExportDrawer } from "../src/components/personal-report/report-export-drawer";
|
||
import { ReportActions } from "../src/components/personal-report/report-actions";
|
||
import { buildReportExportBlocks, exportByteSize, exportFilename, formatExportSize, joinExportBlocks } from "../src/lib/report-export-blocks";
|
||
import { assembleReportFactTables } from "../src/lib/report-fact-tables";
|
||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support";
|
||
|
||
Object.assign(globalThis, { React });
|
||
const reader = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-reader.json", import.meta.url), "utf8"));
|
||
const engine = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-engine.json", import.meta.url), "utf8"));
|
||
const blocks = buildReportExportBlocks({ markdown: reader.markdown, factTables: assembleReportFactTables(engine) });
|
||
const editions = { zh: blocks };
|
||
const source = readFileSync(new URL("../src/components/personal-report/report-export-drawer.tsx", import.meta.url), "utf8");
|
||
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||
// Opening a sheet without English notifies. Keep the real toaster out of these mounts.
|
||
toast.info = () => 1;
|
||
|
||
type Harness = ReturnType<typeof createClientLifecycleHarness>;
|
||
type Node = ReturnType<Harness["elements"]>[number];
|
||
// Sheet, tabs and menus portal onto document.body. The harness container walk misses them.
|
||
function allNodes(h: Harness): Node[] {
|
||
const walk = (node: Node): Node[] => {
|
||
const children = node?.childNodes ?? [];
|
||
return [node, ...[...children].flatMap((child) => walk(child as Node))];
|
||
};
|
||
return [...walk(h.container as unknown as Node), ...[...(h.document.body.childNodes ?? [])].flatMap((child) => walk(child as Node))];
|
||
}
|
||
const button = (h: Harness, text: string) => { const node = allNodes(h).find(node => node.tagName === "BUTTON" && node.text.startsWith(text)); assert.ok(node, text); return node; };
|
||
const checks = (h: Harness) => allNodes(h).filter(node => node.getAttribute("role") === "checkbox");
|
||
const namedCheck = (h: Harness, label: string) => {
|
||
const node = checks(h).find(node => (node.getAttribute("aria-label") ?? "").startsWith(label));
|
||
assert.ok(node, label);
|
||
return node;
|
||
};
|
||
// 原值:单选是 role=radio + aria-checked
|
||
// 新值:分段页签 role=tab。选中看 aria-selected
|
||
// 原因:K5 / D9,导出语言和导出范围改用和星盘页盘型条一样的 Tabs
|
||
const tab = (h: Harness, text: string) => { const node = allNodes(h).find(node => node.getAttribute("role") === "tab" && node.text.startsWith(text)); assert.ok(node, text); return node; };
|
||
const exportButton = (h: Harness) => allNodes(h).filter(node => node.tagName === "BUTTON").find(node => /^导出|^先勾选/.test(node.text))!;
|
||
const sheetOpen = (h: Harness) => allNodes(h).some(node => node.getAttribute("role") === "dialog" || node.getAttribute("data-slot") === "sheet-content");
|
||
const sheetText = (h: Harness) => `${h.container.text}\n${h.document.body.text}`;
|
||
|
||
// 2026-09-30 redesign (product: 「分块导出太丑,需要重新设计」, chose 「两步:整篇 / 自选」).
|
||
// Test names are kept for suite continuity; the presets 常用 / 全部分盘 / 仅核对表 and
|
||
// 全选 are retired. Each rewritten test states 原值 / 新值 / 原因 at its head.
|
||
|
||
test("drawer mounts all golden blocks, presets replace selection and manual choices clear highlight", async () => {
|
||
// 原值:打开即列出全部块(默认勾正文),套餐按钮替换选择,手动勾选清除套餐高亮
|
||
// 新值:打开是「整篇报告」一步导出(全部块);选「自选章节」才出现清单,正文默认全勾、分组复选框可整组开关,分盘/核对表默认收起
|
||
// 原因:2026-09-30 产品重设计;主语(能拿到全部块、能按组与逐块选择)不变
|
||
const h = createClientLifecycleHarness();
|
||
try {
|
||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} />);
|
||
assert.deepEqual(h.errors, []);
|
||
// 原值:原生 DIALOG 的 open 为真,按钮写「导出整篇 · 约 N KB」
|
||
// 新值:Sheet 在文档里;按钮只写「导出整篇」,不带文件大小
|
||
// 原因:K5 导出改用 Sheet,D8 去掉文件大小
|
||
assert.equal(sheetOpen(h), true);
|
||
assert.equal(checks(h).length, 0, "no checklist until 自选章节");
|
||
assert.equal(tab(h, "整篇报告").getAttribute("aria-selected"), "true");
|
||
assert.equal(exportButton(h).text, "导出整篇");
|
||
assert.doesNotMatch(exportButton(h).text, /KB|MB|·/);
|
||
await h.event(tab(h, "自选章节"));
|
||
const body = blocks.filter(block => block.group === "body");
|
||
// Group rows (3) + the expanded 正文 rows.
|
||
assert.equal(checks(h).length, 3 + body.length);
|
||
assert.equal(checks(h).filter(node => node.getAttribute("aria-checked") === "true").length, 1 + body.length);
|
||
const chartGroup = namedCheck(h, "分盘");
|
||
assert.equal(chartGroup.getAttribute("aria-checked"), "false");
|
||
await h.event(chartGroup);
|
||
assert.equal(namedCheck(h, "分盘").getAttribute("aria-checked"), "true");
|
||
await h.event(namedCheck(h, body[0]!.title));
|
||
assert.equal(namedCheck(h, "正文").getAttribute("aria-checked"), "mixed");
|
||
for (const group of ["正文", "分盘", "核对表"]) {
|
||
const row = namedCheck(h, group);
|
||
if (row.getAttribute("aria-checked") === "true") await h.event(row);
|
||
if (namedCheck(h, group).getAttribute("aria-checked") === "mixed") {
|
||
await h.event(namedCheck(h, group));
|
||
await h.event(namedCheck(h, group));
|
||
}
|
||
}
|
||
assert.equal(exportButton(h).text, "先勾选要导出的部分");
|
||
assert.equal(exportButton(h).disabled, true);
|
||
} finally { await h.close(); }
|
||
});
|
||
|
||
test("drawer download uses the existing Blob downloader exactly once and only then closes", async () => {
|
||
// 原值:选「仅核对表」后按「导出选中 (8)」,toast「已导出 8 块」,文件名为节选
|
||
// 新值:自选里只开「核对表」组、关掉正文组后按「导出 8 项 · …」,toast「已导出」,文件名为节选;内容仍是这些块逐字节拼接
|
||
// 原因:2026-09-30 重设计去掉套餐
|
||
const h = createClientLifecycleHarness();
|
||
const oldCreate = URL.createObjectURL;
|
||
const oldRevoke = URL.revokeObjectURL;
|
||
let blob: Blob | undefined;
|
||
let clicked = 0;
|
||
let closed = 0;
|
||
const successBefore = toast.success;
|
||
const successes: string[] = [];
|
||
toast.success = message => { assert.equal(closed, 1); successes.push(String(message)); return 1; };
|
||
let filename = "";
|
||
const document = globalThis.document;
|
||
const originalCreate = document.createElement.bind(document);
|
||
URL.createObjectURL = value => { blob = value as Blob; return "blob:test"; };
|
||
URL.revokeObjectURL = () => {};
|
||
// 原值:把 document.body 换成只有 append 的空对象,挡住原生下载
|
||
// 新值:留下测试文档的 body。锚点仍是假的,body.append 对它不做事
|
||
// 原因:K5 的 Sheet 要挂到 body 上。换掉 body 后传送门和下载都看不见
|
||
document.createElement = ((tag: string) => {
|
||
if (tag !== "a") return originalCreate(tag);
|
||
return { set download(value: string) { filename = value; }, click() { clicked += 1; }, remove() {} } as unknown as HTMLAnchorElement;
|
||
}) as typeof document.createElement;
|
||
try {
|
||
await h.render(<ReportExportDrawer editions={editions} reportDate="2026-09-24" onClose={() => { closed += 1; }} />);
|
||
await h.event(tab(h, "自选章节"));
|
||
await h.event(namedCheck(h, "正文"));
|
||
await h.event(namedCheck(h, "核对表"));
|
||
const tables = blocks.filter(block => block.group === "table");
|
||
await h.event(button(h, `导出 ${tables.length} 项`));
|
||
assert.equal(clicked, 1);
|
||
assert.equal(closed, 1);
|
||
assert.deepEqual(successes, ["已导出"]);
|
||
assert.equal(filename, exportFilename("2026-09-24", false) + ".md");
|
||
assert.equal(await blob?.text(), joinExportBlocks(tables));
|
||
} finally { toast.success = successBefore; URL.createObjectURL = oldCreate; URL.revokeObjectURL = oldRevoke; await h.close(); }
|
||
});
|
||
|
||
test("download failure and WeChat restrictions leave drawer open without a success close", async () => {
|
||
// 原值:按「导出整篇 · …」;新值:按「导出整篇」;原因:K5 / D8 去掉文件大小。失败与微信限制的行为不变
|
||
const h = createClientLifecycleHarness();
|
||
const oldCreate = URL.createObjectURL;
|
||
const navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
|
||
const successBefore = toast.success;
|
||
const errorBefore = toast.error;
|
||
let successes = 0;
|
||
const errors: string[] = [];
|
||
toast.success = () => { successes += 1; return 1; };
|
||
toast.error = (message) => { errors.push(String(message)); return 1; };
|
||
let closed = 0;
|
||
try {
|
||
await h.render(<ReportExportDrawer editions={editions} onClose={() => { closed += 1; }} />);
|
||
URL.createObjectURL = () => { throw new Error("下载失败"); };
|
||
await h.event(button(h, "导出整篇"));
|
||
// 原值: h.container.text 含「下载失败」/「系统浏览器」(抽屉底部 role="alert" 一行)
|
||
// 新值: 两句都是 error toast;抽屉仍开着、没有成功提示(主语不变)
|
||
// 原因: 2026-09-30 产品「报错和提示不要占页面布局」
|
||
assert.equal(errors.at(-1), "下载失败");
|
||
Object.defineProperty(globalThis, "navigator", { configurable: true, value: { userAgent: "MicroMessenger" } });
|
||
await h.event(button(h, "导出整篇"));
|
||
assert.match(errors.at(-1) ?? "", /系统浏览器/);
|
||
assert.equal(closed, 0);
|
||
assert.equal(successes, 0);
|
||
assert.equal(sheetOpen(h), true);
|
||
} finally {
|
||
toast.success = successBefore;
|
||
toast.error = errorBefore;
|
||
URL.createObjectURL = oldCreate;
|
||
if (navigatorDescriptor) Object.defineProperty(globalThis, "navigator", navigatorDescriptor); else Reflect.deleteProperty(globalThis, "navigator");
|
||
await h.close();
|
||
}
|
||
});
|
||
|
||
test("reader actions keep only the accessible export icon", async () => {
|
||
const h = createClientLifecycleHarness();
|
||
try {
|
||
Object.assign(window, { location: { hash: "#export" }, history: { replaceState() { assert.fail("retired export hash must not be consumed"); } } });
|
||
await h.render(<ReportActions markdown={reader.markdown} />);
|
||
assert.equal(sheetOpen(h), false, "old export hash does not auto-open the sheet");
|
||
assert.equal(h.elements().filter(node => node.tagName === "BUTTON").length, 1);
|
||
assert.ok(h.elements().some(node => node.tagName === "A" && node.getAttribute("href") === "/reports"));
|
||
assert.deepEqual(h.errors, []);
|
||
const exportButton = h.elements().find(node => node.tagName === "BUTTON" && node.getAttribute("aria-label") === "导出报告");
|
||
assert.ok(exportButton);
|
||
// 原值:`title` 属性等于「导出」(只有图标的按钮靠原生提示说明用途)
|
||
// 新值:按钮自己的可见文字就是「导出」,不再挂 title
|
||
// 原因:TASK-report-reader-polish-20260929 §3-3——产品反馈图标按钮找不到,改为带文字的描边按钮
|
||
assert.equal(exportButton?.text, "导出");
|
||
assert.equal(exportButton?.getAttribute("title"), null);
|
||
assert.ok(!h.container.text.includes("下载原始附录"));
|
||
assert.ok(!h.container.text.includes("打印"));
|
||
await h.event(exportButton!);
|
||
assert.equal(sheetOpen(h), true);
|
||
// 原值:按可见文字「关闭」找按钮;新值:按读屏名「关闭导出」找(2026-09-30 改成 × 图标,标题改「导出报告」)
|
||
await h.event(allNodes(h).find(node => node.tagName === "BUTTON" && node.getAttribute("aria-label") === "关闭导出")!);
|
||
await h.idle();
|
||
assert.equal(sheetOpen(h), false);
|
||
} finally { await h.close(); }
|
||
});
|
||
|
||
type DomListener = { type: string; fn: (event: Record<string, unknown>) => void; capture: boolean };
|
||
|
||
function captureFlag(options: unknown): boolean {
|
||
return options === true || (typeof options === "object" && options !== null && Boolean((options as { capture?: boolean }).capture));
|
||
}
|
||
|
||
function trackDocumentListeners(target: object): DomListener[] {
|
||
const bag: DomListener[] = [];
|
||
const host = target as {
|
||
addEventListener: (type: string, fn: DomListener["fn"], options?: unknown) => void;
|
||
removeEventListener: (type: string, fn: DomListener["fn"]) => void;
|
||
};
|
||
host.addEventListener = (type, fn, options) => { bag.push({ type, fn, capture: captureFlag(options) }); };
|
||
host.removeEventListener = (type, fn) => {
|
||
const index = bag.findIndex((entry) => entry.type === type && entry.fn === fn);
|
||
if (index >= 0) bag.splice(index, 1);
|
||
};
|
||
return bag;
|
||
}
|
||
|
||
type DomListenerHandle = (type: string, fn: DomListener["fn"], options?: unknown) => void;
|
||
|
||
function trackElementListeners(proto: { addEventListener: DomListenerHandle; removeEventListener: DomListenerHandle }) {
|
||
const bags = new WeakMap<object, DomListener[]>();
|
||
const originalAdd = proto.addEventListener;
|
||
const originalRemove = proto.removeEventListener;
|
||
proto.addEventListener = function (this: object, type: string, fn: DomListener["fn"], options?: unknown) {
|
||
const bag = bags.get(this) ?? [];
|
||
if (!bags.has(this)) bags.set(this, bag);
|
||
bag.push({ type, fn, capture: captureFlag(options) });
|
||
};
|
||
proto.removeEventListener = function (this: object, type: string, fn: DomListener["fn"]) {
|
||
const bag = bags.get(this);
|
||
if (!bag) return;
|
||
const index = bag.findIndex((entry) => entry.type === type && entry.fn === fn);
|
||
if (index >= 0) bag.splice(index, 1);
|
||
};
|
||
return {
|
||
bag(target: object) { return bags.get(target) ?? []; },
|
||
restore() {
|
||
proto.addEventListener = originalAdd;
|
||
proto.removeEventListener = originalRemove;
|
||
},
|
||
};
|
||
}
|
||
|
||
function fireListeners(bag: readonly DomListener[], type: string, event: Record<string, unknown>, capture: boolean) {
|
||
for (const entry of bag.filter((item) => item.type === type && item.capture === capture)) entry.fn(event);
|
||
}
|
||
|
||
test("mounted drawer closes from Escape and the overlay, and restores focus", async () => {
|
||
// 原值:原生 dialog 的 onCancel(Esc)和点遮罩(target === currentTarget)各关一次;内部点「整篇报告」不关闭
|
||
// 新值:点「整篇报告」不关闭;按 Esc 关一次;点遮罩再关一次;卸载后焦点回到打开前的按钮
|
||
// 原因:K5 导出改用 Sheet。Esc 走浮层自己的按键处理,点遮罩走文档上的捕获监听,不再用源码正则代替
|
||
const h = createClientLifecycleHarness(); let closes = 0;
|
||
const previous = document.createElement("button"); previous.focus();
|
||
const documentListeners = trackDocumentListeners(h.document);
|
||
const elementListeners = trackElementListeners(Object.getPrototypeOf(h.document.body) as { addEventListener: DomListenerHandle; removeEventListener: DomListenerHandle });
|
||
try {
|
||
await h.render(<ReportExportDrawer editions={editions} onClose={() => { closes += 1; }} />);
|
||
await h.event(tab(h, "整篇报告")); assert.equal(closes, 0);
|
||
await h.idle();
|
||
const sheet = allNodes(h).find(node => node.getAttribute("data-slot") === "sheet-content");
|
||
assert.ok(sheet);
|
||
await h.event(sheet, "onKeyDown", { key: "Escape" });
|
||
assert.equal(closes, 1);
|
||
const overlay = allNodes(h).find(node => node.getAttribute("data-slot") === "sheet-overlay");
|
||
assert.ok(overlay);
|
||
const click = {
|
||
type: "click",
|
||
target: overlay,
|
||
currentTarget: h.document,
|
||
button: 0,
|
||
composedPath: () => [overlay],
|
||
preventDefault() {},
|
||
stopPropagation() {},
|
||
};
|
||
await h.update(() => {
|
||
fireListeners(documentListeners, "click", click, true);
|
||
fireListeners(elementListeners.bag(overlay), "click", click, false);
|
||
});
|
||
assert.equal(closes, 2);
|
||
// 原值(365a623fc):删掉了点「关闭导出」关一次的断言
|
||
// 新值:补回,点「关闭导出」再关一次
|
||
// 原因:按钮还在页面上,是用户最常用的关法;只测 Esc 和遮罩会漏掉它(第 2 轮验收第 4 条)
|
||
await h.event(allNodes(h).find(node => node.getAttribute("aria-label") === "关闭导出")!);
|
||
assert.equal(closes, 3);
|
||
await h.render(null); assert.equal(document.activeElement, previous);
|
||
} finally { elementListeners.restore(); await h.close(); }
|
||
});
|
||
|
||
test("drawer contract uses a sheet from the right or the bottom, and the list still scrolls", () => {
|
||
// 原值:原生 `<dialog>`、onCancel、点遮罩 target === currentTarget;窄屏 bottom inset 与桌面居中 min(520px)
|
||
// 新值:`<Sheet`,onOpenChange 在 open 为 false 时 onClose;桌面 data-side=right 宽 min(520px, 92vw),窄屏 data-side=bottom 最高 85dvh
|
||
// 原因:K5 导出从右侧滑出,手机从底部滑出。滚动仍在正文,头尾不滚走
|
||
assert.match(source, /<Sheet open onOpenChange=\{\(open\) => \{ if \(!open\) onClose\(\); \}\}/);
|
||
assert.match(source, /side=\{side\}/);
|
||
assert.match(source, /max-width: 767px/);
|
||
assert.doesNotMatch(source, /<dialog|onCancel=|InlineSpinner|navigator\.clipboard|fetch\(/);
|
||
const css = styles.slice(styles.indexOf(".report-export-drawer {"), styles.indexOf("personal-report: unique block"));
|
||
assert.doesNotMatch(css, /var\(--report-(?:paper|rule|accent)\)/);
|
||
assert.match(css, /min-height: 44px/);
|
||
// 原值:窄屏在 @media 之前用 bottom inset、85vh 和 85dvh;桌面 @media (min-width: 860px) 居中,宽 min(520px, 92vw),高 80vh
|
||
// 新值:窄屏是 data-side=bottom,高 85dvh,上圆角;桌面是 data-side=right,宽 min(520px, 92vw),最宽 520px
|
||
// 原因:K5 用 Sheet 的 side 区分桌面和手机,不再用原生 dialog 的媒体查询。vh 和 dvh 不再写在同一条规则里
|
||
assert.match(css, /\.report-export-drawer\[data-side="right"\] \{[^}]*width: min\(520px, 92vw\);/);
|
||
assert.match(css, /\.report-export-drawer\[data-side="right"\] \{[^}]*max-width: 520px;/);
|
||
assert.match(css, /\.report-export-drawer\[data-side="bottom"\] \{[^}]*max-height: 85dvh;/);
|
||
assert.match(css, /\.report-export-drawer\[data-side="bottom"\] \{[^}]*border-radius: var\(--radius-lg\) var\(--radius-lg\) 0 0;/);
|
||
assert.match(css, /\.report-export-body \{[^}]*min-height: 0;[^}]*overflow-y: auto;/);
|
||
assert.match(css, /\.report-export-header \{[^}]*flex-shrink: 0;/);
|
||
assert.match(css, /\.report-export-footer \{[^}]*flex-shrink: 0;/);
|
||
});
|
||
|
||
test("an unavailable English edition is one info toast per open, not a caption", async () => {
|
||
const legacy = "这份报告生成于英文版上线前,重新生成即可获得中英两版。";
|
||
const failed = "这份报告的英文版没有生成成功,只能导出中文版。";
|
||
const infos: { message: string; id?: string | number }[] = [];
|
||
const previous = toast.info;
|
||
const h = createClientLifecycleHarness();
|
||
toast.info = (message, options) => {
|
||
assert.equal(sheetOpen(h), true, "notify after the sheet is open");
|
||
infos.push({ message: String(message), id: options?.id });
|
||
return infos.length;
|
||
};
|
||
try {
|
||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="legacy" />);
|
||
assert.deepEqual(infos, [{ message: legacy, id: "report-export-english" }]);
|
||
assert.equal(h.container.text.includes(legacy), false);
|
||
assert.equal(h.container.text.includes(failed), false);
|
||
await h.event(tab(h, "自选章节"));
|
||
const chapter = blocks.find(block => block.group === "body")!;
|
||
await h.event(namedCheck(h, chapter.title));
|
||
await h.event(namedCheck(h, chapter.title));
|
||
const expand = allNodes(h).find(node => node.getAttribute("aria-label") === "展开分盘");
|
||
assert.ok(expand, "展开分盘");
|
||
await h.event(expand);
|
||
assert.equal(infos.length, 1, "picking chapters does not notify again");
|
||
await h.render(null);
|
||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="legacy" />);
|
||
assert.equal(infos.length, 2, "opening again notifies once more");
|
||
assert.equal(infos[1]?.id, "report-export-english");
|
||
|
||
await h.render(null);
|
||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="unavailable" />);
|
||
assert.equal(infos.length, 3);
|
||
assert.deepEqual(infos[2], { message: failed, id: "report-export-english" });
|
||
assert.equal(h.container.text.includes(failed), false);
|
||
assert.equal(h.container.text.includes(legacy), false);
|
||
|
||
await h.render(null);
|
||
await h.render(<ReportExportDrawer editions={{ zh: blocks, en: blocks }} onClose={() => {}} englishEdition="ready" />);
|
||
assert.equal(infos.length, 3, "a ready English edition does not notify");
|
||
// 原值:英文可用时抽屉正文含「建议选 English」
|
||
// 新值:没有这句说明
|
||
// 原因:K5 / D8 删掉说明句
|
||
assert.equal(sheetText(h).includes("建议选 English"), false);
|
||
} finally {
|
||
toast.info = previous;
|
||
await h.close();
|
||
}
|
||
});
|
||
|
||
test("the drawer shows the real download size, not a character count (BUG-1094)", async () => {
|
||
const body = blocks.filter(block => block.group === "body");
|
||
const charts = blocks.filter(block => block.group === "chart");
|
||
assert.ok(charts.length > 0, "golden carries chart blocks");
|
||
// Exact bytes of what downloadMarkdownReport would receive.
|
||
assert.equal(exportByteSize(body), new TextEncoder().encode(joinExportBlocks(body)).length);
|
||
assert.equal(exportByteSize([]), 0);
|
||
// Chinese is three bytes a character, so bytes run past the character count.
|
||
const bodyChars = body.reduce((sum, block) => sum + block.chars, 0);
|
||
assert.ok(exportByteSize(body) > bodyChars);
|
||
assert.equal(formatExportSize(186 * 1024), "约 186 KB");
|
||
assert.equal(formatExportSize(300), "约 1 KB");
|
||
assert.equal(formatExportSize(Math.round(1.44 * 1024 * 1024)), "约 1.4 MB");
|
||
// 原值:大小只在导出按钮上出现一次(整篇 = 全部块,自选 = 所选块)
|
||
// 新值:按钮只写「导出整篇」或「导出 N 项」,页面上看不到「约 N KB」
|
||
// 原因:K5 / D8 去掉文件大小。字节计算仍由上面的断言锁住
|
||
const h = createClientLifecycleHarness();
|
||
try {
|
||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} />);
|
||
assert.equal(exportButton(h).text, "导出整篇");
|
||
assert.doesNotMatch(sheetText(h), /约 \d| KB| MB/);
|
||
await h.event(tab(h, "自选章节"));
|
||
assert.equal(exportButton(h).text, `导出 ${body.length} 项`);
|
||
assert.doesNotMatch(sheetText(h), /约 \d| KB| MB/);
|
||
} finally { await h.close(); }
|
||
});
|