167 lines
12 KiB
TypeScript
167 lines
12 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { answerUiDemoBlock } from "./support/answer-ui-demo";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { captureReadingGeometry } from "./support/reading-geometry";
|
|
|
|
for (const width of [1600, 375, 320]) {
|
|
test(`formal rich reports preserve drafts, map and versioned references at ${width}px`, async ({ page }, testInfo) => {
|
|
await page.setViewportSize({ width, height: 960 });
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
await mockAgentApi(page);
|
|
// Load Vite's React preamble without mounting a second application. Its
|
|
// global resize listeners would otherwise survive page.setContent().
|
|
await page.route("**/src/main.tsx", (route) => route.fulfill({ contentType: "application/javascript", body: "" }));
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.unroute("**/src/main.tsx");
|
|
await page.setContent('<!doctype html><html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body><main id="reader"></main></body></html>');
|
|
await page.evaluate(async ({ block, width }) => {
|
|
const entry = await (await fetch("/src/main.tsx")).text();
|
|
const reactUrl = entry.match(/from "([^"\n]*\/react\.js[^"\n]*)"/)?.[1];
|
|
const clientUrl = entry.match(/from "([^"\n]*\/react-dom_client\.js[^"\n]*)"/)?.[1];
|
|
if (!reactUrl || !clientUrl) throw new Error("Vite React paths unavailable");
|
|
const reactModule = await import(reactUrl);
|
|
const React = reactModule.default ?? reactModule;
|
|
const clientModule = await import(clientUrl);
|
|
const { createRoot } = clientModule.default ?? clientModule;
|
|
const { InvestigationTaskReport } = await import("/src/features/workbench/components/investigation-task-report.tsx");
|
|
const { ReadingWorkspaceProvider } = await import("/src/shared/ui/reading-workspace.tsx");
|
|
const { useReadingWorkspace } = await import("/src/shared/ui/reading-workspace-context.ts");
|
|
const { WorkbenchMainFrame } = await import("/src/features/workbench/workspace/workbench-main-frame.tsx");
|
|
const now = "2026-10-20T08:00:00Z";
|
|
const report = { revision: 3, execution_generation: 2, published_at: now, report: {
|
|
title: "供水压力演练核验", current_conclusion: "当前演练数据提示压力变化,仍待现场核验。",
|
|
conclusion_boundary: "仅覆盖已加载的演练记录。", stage: "final", outcome: "concluded",
|
|
data_gaps: [], recommendations: ["核对现场位置及对应时段。"], pending_items: [], evidence_refs: ["DEMO-001"],
|
|
ui_blocks: [
|
|
{ block, evidence_refs: ["DEMO-001"] },
|
|
{ block: { version: "answer-ui@future", block_id: "future", fallback_text: "扩展分析摘要:等待下一次现场核验。", spec: {} }, evidence_refs: ["DEMO-001"] },
|
|
],
|
|
} };
|
|
const historical = { ...report, revision: 2, report: { ...report.report, current_conclusion: "历史结论:初次演练核验。" } };
|
|
const task = { task_id: "task-ui-demo", title: "供水压力演练核验", summary: "当前报告已发布",
|
|
execution_generation: 2, lifecycle_status: "completed", status: "completed", updated_at: now,
|
|
latest_report: report, latest_progress: null, latest_plan: null, actions: [],
|
|
evidence: [{ evidence_id: "DEMO-001", kind: "synthetic", summary: "保存的演练监测记录,等待现场核验。",
|
|
source_label: "演练监测记录", observed_at: now, recorded_at: now }],
|
|
};
|
|
const noAction = () => undefined;
|
|
function Harness() {
|
|
const reading = useReadingWorkspace();
|
|
return React.createElement("div", { style: { "--workbench-agent-current-width": "400px" } },
|
|
React.createElement("div", { style: { position: "absolute", inset: 0, left: width >= 1024 ? 400 : 0 } },
|
|
React.createElement(WorkbenchMainFrame, { mapContent: React.createElement("div", { "data-map-instance": true }, "演练地图工作区"),
|
|
timelineContent: null, timelineMode: "compact", timelineBottom: 0, activeArtifact: null, pendingArtifact: null,
|
|
surfaceMode: "map_only", mapSplitAvailable: width >= 1360, readingSplitAvailable: width >= 1360,
|
|
onOpenPendingArtifact: noAction, onDestroyPendingArtifact: noAction, onSetArtifactView: noAction,
|
|
onCollapseArtifact: noAction, onDestroyArtifact: noAction })),
|
|
React.createElement("aside", { "aria-label": "报告列表", style: {
|
|
position: "absolute", inset: "0 auto 0 0", width: width >= 1024 ? 400 : "100%", overflow: "auto", padding: 16,
|
|
display: width < 1024 && reading.active ? "none" : "block",
|
|
} }, React.createElement(InvestigationTaskReport, { task,
|
|
onContinue: (discussion) => { Object.assign(window, { quotedReport: discussion }); },
|
|
renderAction: noAction, renderEvidence: () => React.createElement("p", null, "原始演练记录"),
|
|
onLoadReports: async () => ({ items: [report, historical], next_offset: null }),
|
|
})),
|
|
);
|
|
}
|
|
createRoot(document.getElementById("reader")).render(React.createElement(ReadingWorkspaceProvider, null, React.createElement(Harness)));
|
|
}, { block: answerUiDemoBlock, width });
|
|
await page.getByLabel("现场位置(必填)").fill("展开前的现场资料");
|
|
await expect(page.getByRole("navigation", { name: "报告章节" })).toBeHidden();
|
|
const map = page.locator("[data-map-instance]");
|
|
await map.evaluate((element) => Object.assign(window, { originalMap: element }));
|
|
await page.getByRole("button", { name: "展开查看", exact: true }).click();
|
|
const workspace = page.getByRole("region", { name: "展开报告工作区", exact: true });
|
|
await expect(workspace).toContainText("当前演练数据提示压力变化,仍待现场核验。");
|
|
await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("展开前的现场资料");
|
|
await expect(page.getByLabel("现场位置(必填)")).toHaveCount(1);
|
|
const chapters = workspace.getByRole("navigation", { name: "报告章节" });
|
|
await expect(chapters).toBeVisible();
|
|
await expect(chapters.getByRole("button", { name: "待办", exact: true })).toHaveCount(0);
|
|
await chapters.getByRole("button", { name: "依据", exact: true }).focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(workspace.getByRole("region", { name: "主要依据", exact: true })).toBeFocused();
|
|
await testInfo.attach("chapter-jump-geometry", {
|
|
body: JSON.stringify(await workspace.evaluate((root) => {
|
|
const scroll = root.querySelector("[data-reading-scroll]")!;
|
|
return {
|
|
scrollTop: scroll.scrollTop,
|
|
maxScroll: scroll.scrollHeight - scroll.clientHeight,
|
|
sections: [...root.querySelectorAll(".investigation-report > section")].map((section) => ({
|
|
label: section.getAttribute("aria-label"),
|
|
top: Math.round(section.getBoundingClientRect().top - scroll.getBoundingClientRect().top),
|
|
})),
|
|
};
|
|
}), null, 2), contentType: "application/json",
|
|
});
|
|
await expect(chapters.getByRole("button", { name: "依据", exact: true })).toHaveAttribute("aria-current", "location");
|
|
const navigationTop = (await chapters.boundingBox())!.y;
|
|
const scrollTop = (await workspace.locator("[data-reading-scroll]").boundingBox())!.y;
|
|
expect(Math.abs(navigationTop - scrollTop)).toBeLessThan(2);
|
|
await chapters.getByRole("button", { name: "分析", exact: true }).click();
|
|
await expect(workspace.getByRole("region", { name: "交互分析", exact: true })).toBeFocused();
|
|
await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("展开前的现场资料");
|
|
await page.evaluate(() => document.fonts.ready);
|
|
await page.screenshot({ path: `test-results/formal-report-analysis-${width}.png` });
|
|
await expect(workspace).toContainText("扩展分析摘要:等待下一次现场核验。");
|
|
await workspace.getByRole("button", { name: /查看演练证据/ }).click();
|
|
await expect(workspace).toContainText("保存的演练监测记录,等待现场核验。");
|
|
if (width >= 1360) {
|
|
await workspace.getByRole("button", { name: "地图对照" }).click();
|
|
await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_split");
|
|
await expect(map).toBeVisible();
|
|
const separator = page.getByRole("separator", { name: "调整地图与报告宽度" });
|
|
await expect(separator).toHaveAttribute("data-slot", "resizable-handle");
|
|
const mapPanel = page.locator("#workspace-map");
|
|
const initialWidth = (await mapPanel.boundingBox())!.width;
|
|
const handle = (await separator.boundingBox())!;
|
|
await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2);
|
|
await expect(separator).toHaveAttribute("data-separator", "hover");
|
|
await page.mouse.down();
|
|
await expect(separator).toHaveAttribute("data-separator", "active");
|
|
await page.mouse.move(handle.x + 100, handle.y + handle.height / 2);
|
|
await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeGreaterThan(initialWidth + 90);
|
|
await page.mouse.up();
|
|
await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeGreaterThan(initialWidth + 90);
|
|
await separator.focus();
|
|
await page.keyboard.press("ArrowLeft");
|
|
await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeLessThan(initialWidth + 90);
|
|
const resizedWidth = (await mapPanel.boundingBox())!.width;
|
|
await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("展开前的现场资料");
|
|
await page.screenshot({ path: "test-results/resizable-report-map.png" });
|
|
await workspace.getByRole("button", { name: "专注阅读" }).click();
|
|
await expect(separator).toBeHidden();
|
|
await workspace.getByRole("button", { name: "地图对照" }).click();
|
|
await expect.poll(async () => (await mapPanel.boundingBox())!.width).toBeCloseTo(resizedWidth, 0);
|
|
await workspace.getByRole("button", { name: "专注阅读" }).click();
|
|
} else {
|
|
await workspace.getByRole("button", { name: "查看地图" }).click();
|
|
await expect(map).toBeVisible();
|
|
await page.getByRole("button", { name: "返回报告", exact: true }).click();
|
|
}
|
|
expect(await map.evaluate((element) => element === (window as unknown as { originalMap: Element }).originalMap)).toBe(true);
|
|
await workspace.locator("[data-reading-scroll]").evaluate((element) => { element.scrollTop = 0; });
|
|
await expect(chapters.getByRole("button", { name: "结论", exact: true })).toHaveAttribute("aria-current", "location");
|
|
await page.evaluate(() => document.fonts.ready);
|
|
const geometry = await captureReadingGeometry(workspace, testInfo);
|
|
expect(geometry.metricsHeight).toBeLessThan(145);
|
|
expect(geometry.horizontalOverflow).toBe(false);
|
|
await page.screenshot({ path: `test-results/formal-report-workspace-${width}.png` });
|
|
await workspace.getByRole("button", { name: "展开报告版本" }).click();
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByRole("dialog", { name: "报告版本" })).toHaveCount(0);
|
|
await expect(workspace).toBeVisible();
|
|
await workspace.getByRole("button", { name: "展开报告版本" }).click();
|
|
await page.getByRole("dialog", { name: "报告版本" }).getByRole("button", { name: /报告第 2 版/ }).click();
|
|
await expect(workspace).toContainText("历史结论:初次演练核验。");
|
|
await workspace.getByRole("tab", { name: "数据", exact: true }).click();
|
|
await workspace.getByRole("checkbox", { name: "选择第 2 行" }).check();
|
|
await workspace.locator("[data-interactive-table]").getByRole("button", { name: "针对这部分提问" }).click();
|
|
const quote = await page.evaluate(() => (window as unknown as { quotedReport: { context: { report_revision: number }; reference: { title: string; text: string } } }).quotedReport);
|
|
expect(quote.context.report_revision).toBe(2);
|
|
expect(quote.reference.title).toContain("报告第 2 版");
|
|
expect(quote.reference.text).toContain("0.28");
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
|
});
|
|
}
|