Files
next-tjwater-frontend/tests/browser/agent-report-workspace.e2e.ts
T
jiang 2d8506fea0
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s
feat: add project-aware agent workspace
2026-10-22 18:52:18 +08:00

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);
});
}