Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
130 lines
8.9 KiB
TypeScript
130 lines
8.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
|
|
test("调查图加载与尺寸变化保持阅读位置,刷新后仍可缩放和查看节点", async ({ page }, testInfo) => {
|
|
await page.route("http://127.0.0.1:8787/api/v1/**", (route) => route.fulfill({ json: { items: [] } }));
|
|
await mockAgentApi(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.setContent('<!doctype html><html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body><main id="reader" style="max-width:640px;margin:auto;padding:16px;height:100vh;overflow:auto"></main></body></html>');
|
|
await page.evaluate(async () => {
|
|
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 module paths unavailable");
|
|
const reactModule = await import(reactUrl);
|
|
const React = reactModule.default ?? reactModule;
|
|
const clientModule = await import(clientUrl);
|
|
const { createRoot } = clientModule.default ?? clientModule;
|
|
const { InvestigationDiagramView } = await import("/src/features/workbench/components/investigation-diagram-view.tsx");
|
|
const { investigationDiagramPlugins } = await import("/src/features/workbench/utils/investigation-diagram-style.ts");
|
|
const renderer = investigationDiagramPlugins.mermaid.getMermaid.bind(investigationDiagramPlugins.mermaid);
|
|
let release: (() => void) | undefined;
|
|
let renderPending = false;
|
|
investigationDiagramPlugins.mermaid.getMermaid = (config: unknown) => {
|
|
const instance = renderer(config);
|
|
return { ...instance, async render(id: string, source: string) {
|
|
renderPending = true;
|
|
await new Promise<void>((resolve) => { release = resolve; });
|
|
renderPending = false;
|
|
return instance.render(id, source);
|
|
} };
|
|
};
|
|
const root = createRoot(document.getElementById("reader"));
|
|
const documentHeights: number[] = [];
|
|
new MutationObserver(() => {
|
|
documentHeights.push(document.documentElement.scrollHeight);
|
|
}).observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
let source = "flowchart TD\nA[读取工单] --> B[核对监测]\nB --> C[现场采样]\nC --> D[关联证据]\nD --> E[发布报告]";
|
|
const render = () => root.render(React.createElement(React.Fragment, null,
|
|
React.createElement(InvestigationDiagramView, {
|
|
diagram: { title: "调查刷新检查", mermaid: source,
|
|
node_refs: [{ node_id: "B", label: "核对监测", evidence_refs: ["e1"], action_refs: [] }] },
|
|
renderDetails: () => React.createElement("p", null, "监测记录的引用依据")
|
|
}),
|
|
React.createElement("section", { id: "reading-anchor", style: { height: 1200 } }, "当前正在阅读的证据")
|
|
));
|
|
Object.assign(window, { investigationFixture: {
|
|
release: () => release?.(), pending: () => renderPending,
|
|
update: (next: string) => { source = next; render(); }
|
|
}, documentHeights });
|
|
render();
|
|
});
|
|
const fixture = (action: "release" | "pending") => page.evaluate((key) => {
|
|
const api = (window as unknown as { investigationFixture: { release: () => void; pending: () => boolean } }).investigationFixture;
|
|
return api[key]();
|
|
}, action);
|
|
const diagram = page.getByRole("region", { name: "调查脉络", exact: true });
|
|
const viewport = diagram.locator(".investigation-diagram");
|
|
await expect.poll(() => fixture("pending")).toBe(true);
|
|
const loadingHeight = (await viewport.boundingBox())!.height;
|
|
await fixture("release");
|
|
await expect(diagram.locator("svg .node")).toHaveCount(5);
|
|
const renderHeights = await page.evaluate(() => (window as unknown as { documentHeights: number[] }).documentHeights);
|
|
expect(Math.max(...renderHeights) - Math.min(...renderHeights)).toBeLessThanOrEqual(1);
|
|
const loadedHeight = (await viewport.boundingBox())!.height;
|
|
testInfo.annotations.push({ type: "geometry", description: `loading=${loadingHeight}, loaded=${loadedHeight}` });
|
|
expect(loadedHeight).toBeGreaterThan(loadingHeight);
|
|
// Read below the graph: native scroll anchoring must preserve that content,
|
|
// while the graph is free to take its real (different) height.
|
|
await page.locator("#reading-anchor").evaluate((element) => element.scrollIntoView({ block: "start" }));
|
|
const anchorTop = await page.locator("#reading-anchor").evaluate((element) => element.getBoundingClientRect().top);
|
|
await page.evaluate(() => (window as unknown as { investigationFixture: { update: (source: string) => void } }).investigationFixture
|
|
.update("flowchart LR\nA[读取工单] --> B[核对监测]"));
|
|
await expect.poll(() => fixture("pending")).toBe(true);
|
|
await expect(diagram.locator("svg .node")).toHaveCount(5);
|
|
await fixture("release");
|
|
await expect(diagram.locator("svg .node")).toHaveCount(2);
|
|
expect((await viewport.boundingBox())!.height).toBeLessThan(loadedHeight - 100);
|
|
expect(Math.abs(await page.locator("#reading-anchor").evaluate((element) => element.getBoundingClientRect().top) - anchorTop)).toBeLessThanOrEqual(1);
|
|
await page.locator("#reader").evaluate((element) => { element.scrollTop = 0; });
|
|
await diagram.locator(".node").filter({ hasText: "核对监测" }).click();
|
|
await expect(diagram.getByLabel("节点详情 核对监测")).toContainText("监测记录的引用依据");
|
|
await diagram.getByRole("button", { name: "Zoom in", exact: true }).click();
|
|
await expect(diagram.locator('[role="application"]')).toHaveCSS("transform", "matrix(1.1, 0, 0, 1.1, 0, 0)");
|
|
await diagram.getByRole("button", { name: "Reset zoom and pan", exact: true }).click();
|
|
const checkCollapse = async (keyboard: boolean) => {
|
|
const before = await page.locator("#reading-anchor").evaluate((element) => element.getBoundingClientRect().top);
|
|
await diagram.getByRole("button", { name: "放大查看", exact: true }).click();
|
|
const detail = page.getByRole("dialog", { name: "调查刷新检查", exact: true });
|
|
await expect(detail.locator("svg .node")).toHaveCount(2);
|
|
await page.evaluate(() => {
|
|
const reader = document.getElementById("reader")!;
|
|
const anchor = document.getElementById("reading-anchor")!;
|
|
const frames: { top: number; scroll: number; height: number }[] = [];
|
|
Object.assign(window, { collapseFrames: frames, collapseDone: false });
|
|
const start = performance.now();
|
|
const sample = () => {
|
|
frames.push({ top: anchor.getBoundingClientRect().top, scroll: reader.scrollTop, height: reader.scrollHeight });
|
|
if (performance.now() - start < 800) requestAnimationFrame(sample);
|
|
else Object.assign(window, { collapseDone: true });
|
|
};
|
|
sample();
|
|
});
|
|
if (keyboard) await page.keyboard.press("Escape");
|
|
else await detail.getByRole("button", { name: "关闭详情", exact: true }).click();
|
|
await expect(detail).toHaveCount(0);
|
|
await page.waitForFunction(() => (window as unknown as { collapseDone: boolean }).collapseDone);
|
|
const frames = await page.evaluate(() => (window as unknown as { collapseFrames: { top: number; scroll: number; height: number }[] }).collapseFrames);
|
|
const range = (key: "top" | "scroll" | "height") => Math.max(...frames.map((frame) => frame[key])) - Math.min(...frames.map((frame) => frame[key]));
|
|
testInfo.annotations.push({ type: "collapse-geometry", description: `keyboard=${keyboard}, anchorShift=${range("top")}, scrollShift=${range("scroll")}, heightShift=${range("height")}` });
|
|
expect(Math.abs(frames[0].top - before)).toBeLessThanOrEqual(1);
|
|
expect(range("top")).toBeLessThanOrEqual(1);
|
|
expect(range("scroll")).toBeLessThanOrEqual(1);
|
|
expect(range("height")).toBeLessThanOrEqual(1);
|
|
expect(await diagram.locator(":scope > .flow-root").evaluate((element) => (element as HTMLElement).style.height)).toBe("");
|
|
};
|
|
await checkCollapse(false);
|
|
const allRenderHeights = await page.evaluate(() => (window as unknown as { documentHeights: number[] }).documentHeights);
|
|
expect(Math.max(...allRenderHeights) - Math.min(...allRenderHeights)).toBeLessThanOrEqual(1);
|
|
testInfo.annotations.push({ type: "page-height", description: `min=${Math.min(...allRenderHeights)}, max=${Math.max(...allRenderHeights)}` });
|
|
await diagram.screenshot({ path: testInfo.outputPath("diagram-stable-desktop.png") });
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
await expect(diagram.locator("svg .node")).toHaveCount(2);
|
|
await expect(diagram.getByRole("button", { name: "放大查看", exact: true })).toBeVisible();
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
|
|
await diagram.screenshot({ path: testInfo.outputPath("diagram-stable-mobile.png") });
|
|
await checkCollapse(true);
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
await expect(diagram.locator('[role="img"] > svg')).toHaveCSS("animation-name", "none");
|
|
});
|