Files
Jyotisha/frontend/tests/personal-report-view.test.ts
T
jesse-uxandGrok (agent loop) 1f9947a0d7 删除三个无调用方接口和约 3000 行前端死代码
关掉未登录就会触发约 20 秒计算的入口,并记下 BUG-1342。普通导出的逐字节锁保留。自测 tsc 0 错,lint 0 error / 123 warning,改到的前端测试 fail=0,两份 Python 测试通过,test:db 失败 13 条与 Windows 已知清单一致。

Co-Authored-By: Grok (agent loop)
2026-10-14 08:39:32 +08:00

246 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { classifyReportEnvelope } from "../src/components/personal-report/personal-report-page.tsx";
import { cssDeclarations } from "./css-contract-test-support.ts";
import { safeParseReportDocument } from "../src/lib/personal-report-contract.ts";
import type { LegacyReportDocumentV1, ReportDocumentV2 } from "../src/lib/personal-report-contract.ts";
const fixturePath = new URL("../../tests/fixtures/personal_report_document.v1.json", import.meta.url);
const v2FixturePath = new URL("../../tests/fixtures/personal_report_document.v2.json", import.meta.url);
const canonicalFixture = JSON.parse(readFileSync(fixturePath, "utf8")) as LegacyReportDocumentV1;
const canonicalV2Fixture = JSON.parse(readFileSync(v2FixturePath, "utf8")) as ReportDocumentV2;
// 原值: 用 PersonalReportDocumentView 渲染旧阅读页的用例留在本文件
// 新值: 那些用例随旧阅读页删除;解析、路由信封、阅读外壳、目录和打印留在下面
// 原因: 阅读页从 09 月起走 Markdown 视图,旧视图没有页面引用
test("stored v1 and current v2 fixtures pass the canonical contract parse", () => {
const storedV1 = safeParseReportDocument(canonicalFixture);
assert.equal(storedV1.ok, true);
if (storedV1.ok) {
assert.equal(storedV1.document.schemaVersion, "report_document.v1");
assert.equal(storedV1.document.reportId, canonicalFixture.reportId);
}
const currentV2 = safeParseReportDocument(canonicalV2Fixture);
assert.equal(currentV2.ok, true);
if (currentV2.ok) {
assert.equal(currentV2.document.schemaVersion, "report_document.v2");
assert.equal(currentV2.document.reportId, canonicalV2Fixture.reportId);
}
});
test("GET envelope classification: 401/404/ready/generating/failed/invalid (real API shape)", () => {
assert.deepEqual(classifyReportEnvelope(401, {}), { phase: "unauthorized" });
assert.deepEqual(classifyReportEnvelope(404, {}), { phase: "not-found" });
assert.deepEqual(classifyReportEnvelope(403, {}), { phase: "invalid", message: "无权访问该报告。" });
const view = (status: string, failureCode: string | null = null) => ({
id: "11111111-1111-1111-1111-111111111111",
requestId: "22222222-2222-2222-2222-222222222222",
reportType: "personal_full",
presentationMode: "default",
status,
failureCode,
createdAt: "2026-08-06T00:00:00Z",
completedAt: null,
});
const ready = classifyReportEnvelope(200, {
report: view("ready"),
longformMarkdown: "# 个人长报告\n\n### 解读摘要\n正文",
});
assert.equal(ready.phase, "markdown-ready");
assert.ok(ready.phase === "markdown-ready" && ready.markdown.includes("解读摘要"));
const readyV2 = classifyReportEnvelope(200, {
report: view("ready"),
longformMarkdown: "## 成品阅读导航\n导航",
reportDocument: canonicalV2Fixture,
});
assert.equal(readyV2.phase, "markdown-ready");
assert.deepEqual(classifyReportEnvelope(200, { report: view("generating") }), { phase: "generating" });
assert.deepEqual(classifyReportEnvelope(200, { report: view("failed", "model_unavailable") }), {
phase: "failed",
failureCode: "model_unavailable",
});
assert.deepEqual(
classifyReportEnvelope(200, {
report: { ...view("failed", "report_schema_invalid"), failureSummary: "4 个主题中 4 个写作失败:输出被截断" },
}),
{
phase: "failed",
failureCode: "report_schema_invalid",
failureSummary: "4 个主题中 4 个写作失败:输出被截断",
},
);
assert.deepEqual(
classifyReportEnvelope(200, {
report: {
...view("failed", "calculation_unavailable"),
failureSummary: "报告正文未能保存,请稍后重试",
appendixLastErrorCode: "appendix_persist_failed",
},
}),
{
phase: "failed",
failureCode: "appendix_persist_failed",
failureSummary: "报告正文未能保存,请稍后重试",
appendixLastErrorCode: "appendix_persist_failed",
},
);
assert.deepEqual(classifyReportEnvelope(200, { report: view("failed") }), { phase: "failed", failureCode: null });
// Server-side error envelopes carry a stable code at the top level.
assert.deepEqual(classifyReportEnvelope(503, { error: "x", code: "report_persistence_unavailable" }), {
phase: "failed",
failureCode: "report_persistence_unavailable",
});
assert.deepEqual(classifyReportEnvelope(500, {}), { phase: "failed", failureCode: null });
const missingDocument = classifyReportEnvelope(200, { report: view("ready") });
assert.equal(missingDocument.phase, "legacy-unavailable");
const badDocument = classifyReportEnvelope(200, {
report: view("ready"),
reportDocument: { schemaVersion: "wrong" },
});
assert.equal(badDocument.phase, "legacy-unavailable");
const unknownStatus = classifyReportEnvelope(200, { report: view("mystery") });
assert.equal(unknownStatus.phase, "invalid");
assert.equal(classifyReportEnvelope(200, null).phase, "invalid");
assert.equal(classifyReportEnvelope(200, { error: "oops" }).phase, "invalid");
});
test("report center and detail surfaces prefer a readable failure summary over the bare schema code", () => {
const center = readFileSync(
new URL("../src/components/personal-report/personal-report-center.tsx", import.meta.url),
"utf8",
);
const page = readFileSync(
new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url),
"utf8",
);
assert.match(center, /report\.failureSummary \?\? report\.failureCode/);
assert.match(page, /state\.failureSummary \?\? "生成过程中出现问题/);
assert.match(page, /错误码:\{state\.failureCode\}/);
});
test("canonical contract rejects malformed documents", () => {
assert.equal(safeParseReportDocument({}).ok, false);
const noCharts = { ...structuredClone(canonicalFixture) };
delete (noCharts as Partial<LegacyReportDocumentV1>).charts;
assert.equal(safeParseReportDocument(noCharts).ok, false);
const badVersion = { ...structuredClone(canonicalFixture), schemaVersion: "report_document.v2" };
assert.equal(safeParseReportDocument(badVersion).ok, false);
const badMode = { ...structuredClone(canonicalFixture), presentationMode: "ultra" };
assert.equal(safeParseReportDocument(badMode).ok, false);
const badReportId = { ...structuredClone(canonicalFixture), reportId: "not-a-uuid" };
assert.equal(safeParseReportDocument(badReportId).ok, false);
const badRefs = structuredClone(canonicalFixture) as LegacyReportDocumentV1;
badRefs.thematicNarrative[0].evidenceRefs = ["ev-does-not-exist"];
assert.equal(safeParseReportDocument(badRefs).ok, false, "dangling evidenceRefs must fail guards");
});
test("ready reports expose only the back link and accessible export icon", () => {
const actionsSource = readFileSync(
new URL("../src/components/personal-report/report-actions.tsx", import.meta.url),
"utf8",
);
assert.match(actionsSource, /href="\/reports"/);
assert.match(actionsSource, /返回报告中心/);
assert.match(actionsSource, /ReportExportDrawer/);
// 原值:`assert.match(actionsSource, /size="icon"/)` 与 `/title="导出"/`
// 新值:不再是图标按钮(doesNotMatch size="icon"),可见文字「导出」,描边样式
// 原因:TASK-report-reader-polish-20260929 §3-3——图标按钮太不显眼,产品要求做明显
assert.doesNotMatch(actionsSource, /size="icon"/);
assert.match(actionsSource, /variant="outline"/);
assert.match(actionsSource, /<Download aria-hidden="true" \/>导出/);
assert.doesNotMatch(actionsSource, /PERSONAL_REPORT_EXPORT_LABEL|#export|reportId|reportTitle/);
assert.match(actionsSource, /aria-label="导出报告"/);
assert.doesNotMatch(actionsSource, /RawAppendixDownload|printPersonalReport|PERSONAL_REPORT_PRINT_LABEL|Printer|useSyncExternalStore|detectPrintRestriction/);
});
test("the reader renders inside the app shell, in every phase", () => {
// /reports/[reportId] was the last standalone full-screen route: opening a
// report dropped the nav out of the app. Every phase carries the shell now,
// so the way back is the rail rather than one in-page link.
const pageSource = readFileSync(
new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url),
"utf8",
);
// 原值:`SecondaryShell` 的 import 与 `<SecondaryShell title="个人报告">` 的出现次数
// 新值:`SecondaryHeader` 的 import 与 `<SecondaryHeader title="个人报告" />` 的出现次数
// 原因:TASK-sidebar-unify D2 把 provider + 侧栏 + inset 上移到
// `app/(app)/layout.tsx`,`SecondaryShell` 拆剩 46px 顶栏并改名
// `SecondaryHeader`。断言主语(每个阶段都在 app 外壳里、都恰好带一次外壳、
// 没有一个阶段返回裸 `<main>`)一字未改。
assert.match(pageSource, /import \{ SecondaryHeader \} from "@\/components\/secondary-header";/);
// No phase may return a bare <main>: that would nest inside the shell's own.
assert.doesNotMatch(pageSource, /<main className="personal-report-(?:state|reader)"/);
const shells = pageSource.match(/<SecondaryHeader title="个人报告" \/>/g) ?? [];
const phases = pageSource.match(/className="personal-report-(?:state|reader)"/g) ?? [];
assert.equal(shells.length, phases.length, "every rendered phase is wrapped exactly once");
assert.ok(phases.length >= 8, `all report phases render, got ${phases.length}`);
});
test("printing a report inside the shell drops the chrome and its height lock", () => {
const pageSource = readFileSync(
new URL("../src/components/personal-report/personal-report-page.tsx", import.meta.url),
"utf8",
);
const globals = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
// The @page rule is untouched.
assert.match(pageSource, /<style media="print">\{"@page \{ size: A4; margin: 13mm 12mm 14mm; \}"\}<\/style>/);
// The shell is hidden AND unlocked. Hiding alone is not enough: .chat-app and
// .chat-panel are `height: 100%; overflow: hidden`, which would clip a
// nine-section report to one page.
assert.match(pageSource, /<style media="print">\{REPORT_SHELL_PRINT_CSS\}<\/style>/);
const start = pageSource.indexOf("const REPORT_SHELL_PRINT_CSS");
assert.ok(start >= 0, "the shell print stylesheet must be declared");
const end = pageSource.indexOf("/** Wall-clock budget", start);
assert.ok(end > start, "the declaration must end before the next export");
const shellPrintCss = pageSource.slice(start, end);
assert.match(shellPrintCss, /\.chat-app, \.chat-panel/);
assert.match(shellPrintCss, /overflow: visible !important/);
assert.match(shellPrintCss, /\[data-slot='sidebar'\]/);
assert.match(shellPrintCss, /\.chat-header/);
assert.match(shellPrintCss, /display: none !important/);
// The TOC is screen furniture and stays off paper.
const printBlock = globals.slice(globals.indexOf("personal-report: unique block"));
assert.match(printBlock, /@media print/);
assert.match(printBlock, /\.personal-report-toc \{ display: none !important; \}/);
});
test("the report TOC is a persistent right-hand rail that leaves the paper palette alone", () => {
const globals = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const viewSource = readFileSync(
new URL("../src/components/personal-report/personal-report-markdown-view.tsx", import.meta.url),
"utf8",
);
// Built from the outline's own heading ids — no second slugger, and nothing
// that would need report-chart-grid-rehype to change (BUG-616/617).
// 原值:`/<ReportToc headings=\{outline\.headings\} \/>/`
// 新值:同一 headings 之外允许并只允许多一个 `language` 属性
// 原因:TASK-report-english-edition-20260929 E6——目录标签随报告语言切换;目录仍只从同一份 outline 的 heading id 构建
assert.match(viewSource, /<ReportToc headings=\{outline\.headings\} language=\{language\} \/>/);
assert.match(viewSource, /aria-current=\{item\.id === activeId \? "location" : undefined\}/);
assert.match(viewSource, /rehypePlugins=\{\[reportChartGrid\]\}/, "BUG-616/617 grid stays wired");
const toc = cssDeclarations(".personal-report-toc", globals);
assert.match(toc, /grid-column: 2/, "the rail sits right of the paper");
assert.match(toc, /position: sticky/);
const layout = cssDeclarations(".personal-report-md-layout", globals);
assert.match(layout, /grid-template-columns: minmax\(0, 1fr\) minmax\(11rem, 15rem\)/);
// The rail is app chrome; the three paper tokens are not on it (D11/D3).
const current = cssDeclarations(".personal-report-toc a.is-current", globals);
assert.match(current, /border-left-color: var\(--color-action\)/);
assert.doesNotMatch(toc + current, /--report-(?:paper|rule|accent)/);
assert.match(globals, /--report-accent: #85432f;/);
});