feat: add project-aware agent workspace
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user