Files
next-tjwater-frontend/tests/browser/investigation-chart-windows.e2e.ts
T
jiang 0ae0a41e61
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m8s
feat(charts): manage investigation charts in a scrolling workspace
2026-11-10 17:56:23 +08:00

416 lines
27 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 { expect, test, type Page } from "@playwright/test";
import { investigationFixture, mockInvestigationPresentation, selectInvestigation } from "./support/mock-investigation-presentation";
const chart = (index: number) => ({
action: "open_chart", chart_id: "pressure", evidence_refs: ["synthetic:pressure"],
chart: { title: `合成压力分析 ${index}`, chart_type: "line", x_data: ["10:00", "10:15"],
series: [{ name: "压力(m)", data: [30, 30 - index] }] }
});
const managedPages = new WeakSet<Page>();
async function openChild(page: Page, title: string, live = true) {
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
if (live && !managedPages.has(page)) {
await expect(manager).toBeVisible();
managedPages.add(page);
}
if (await manager.isVisible()) await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
await page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 ${title}`, exact: true }).click();
}
test.setTimeout(60_000);
// Keep unrelated avatar animation out of chart geometry and drag measurements.
test.use({ reducedMotion: "reduce" });
test("losing window focus cancels a drag and allows a fresh gesture", async ({ page }) => {
const task = investigationFixture(1, "图窗失焦恢复核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await mock.emit(task, chart(1));
await openChild(page, "合成压力分析 1");
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
const before = (await popup.boundingBox())!;
await page.mouse.move(before.x + 180, before.y + 24);
await page.mouse.down();
await page.mouse.move(before.x + 100, before.y + 64, { steps: 4 });
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-80);
await page.evaluate(() => window.dispatchEvent(new Event("blur")));
expect(await popup.boundingBox()).toEqual(before);
expect(await popup.evaluate((node) => (node as HTMLElement).style.willChange)).toBe("");
await page.mouse.up();
await page.mouse.move(before.x + 180, before.y + 24);
await page.mouse.down();
await page.mouse.move(before.x + 140, before.y + 44, { steps: 4 });
await page.mouse.up();
expect(Math.round((await popup.boundingBox())!.x - before.x)).toBe(-40);
expect(Math.round((await popup.boundingBox())!.y - before.y)).toBe(20);
});
test("cancelled drags release capture and preserve restore, resizing and keyboard controls", async ({ page }) => {
const task = investigationFixture(1, "图窗拖动取消核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await mock.emit(task, chart(1));
await openChild(page, "合成压力分析 1");
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
await expect(popup).toBeVisible();
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
const heading = popup.locator(".floating-window-heading");
await heading.click({ trial: true, position: { x: 200, y: 24 } });
const original = (await popup.boundingBox())!;
await heading.evaluate((node) => node.addEventListener("pointerdown", (event) => {
(node as HTMLElement).dataset.testPointerId = String((event as PointerEvent).pointerId);
}, { once: true }));
await page.mouse.move(original.x + 200, original.y + 24);
await page.mouse.down();
await page.mouse.move(original.x + 100, original.y + 64, { steps: 4 });
await expect.poll(async () => Math.round((await popup.boundingBox())!.x)).toBe(original.x - 100);
await heading.evaluate((node) => node.dispatchEvent(new PointerEvent("pointercancel", {
pointerId: Number((node as HTMLElement).dataset.testPointerId)
})));
await page.mouse.up();
expect(await popup.boundingBox()).toEqual(original);
expect(await popup.evaluate((node) => (node as HTMLElement).style.willChange)).toBe("");
await popup.getByRole("button", { name: "最大化窗口", exact: true }).click();
expect((await popup.boundingBox())!.x).toBe(8);
expect((await popup.boundingBox())!.y).toBe(64);
await popup.getByRole("button", { name: "还原窗口", exact: true }).click();
expect(await popup.boundingBox()).toEqual(original);
await heading.focus();
await page.keyboard.press("ArrowRight");
expect((await popup.boundingBox())!.x).toBe(original.x + 16);
await page.mouse.move(original.x + 16 + original.width - 3, original.y + original.height - 3);
await page.mouse.down();
await page.mouse.move(original.x + 16 + original.width + 37, original.y + original.height + 27, { steps: 4 });
await page.mouse.up();
expect((await popup.boundingBox())!.width).toBe(original.width + 40);
expect((await popup.boundingBox())!.height).toBe(original.height + 30);
await heading.focus();
await page.keyboard.press("Escape");
await expect(popup).toHaveCount(0);
expect(mock.mutations).toEqual([]);
});
test("profiles a single drag with eight comparison chart windows", async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1600, height: 1000 });
const task = investigationFixture(1, "多图拖动性能核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
const slots = Array.from({ length: 13 }, (_, index) => `${String(5 + Math.floor(index / 4)).padStart(2, "0")}:${String(index % 4 * 15).padStart(2, "0")}`);
for (let index = 1; index <= 8; index++) {
await mock.emit(task, { action: "open_chart", chart_id: `comparison-${index}`, evidence_refs: ["synthetic:pressure"], chart: {
title: `合成七日同期曲线 ${index}`, chart_type: "time_comparison", x_data: slots,
series: [{ name: "当前", type: "line", data: slots.map((_, point) => 30 - index + point / 10),
sample_times: slots.map((time) => `2026-07-16T${time}:00+08:00`) },
...Array.from({ length: 7 }, (_, day) => ({ name: `前${day + 1}天`, type: "scatter",
data: slots.map((_, point) => point < 10 ? null : 30 + day / 10),
sample_times: slots.map((time, point) => point < 10 ? null : `2026-07-${String(15 - day).padStart(2, "0")}T${time}:00+08:00`) }))]
} });
await expect(page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 合成七日同期曲线 ${index}`, exact: true })).toBeVisible();
await openChild(page, `合成七日同期曲线 ${index}`);
await expect(page.getByRole("dialog", { name: `合成七日同期曲线 ${index}`, exact: true })).toBeVisible();
}
await expect(page.locator("[data-floating-window]")).toHaveCount(8);
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
await page.evaluate(() => document.fonts.ready);
const popup = page.getByRole("dialog", { name: "合成七日同期曲线 8", exact: true });
const otherWindows = page.locator('[data-floating-window]').filter({ hasNot: page.getByRole("heading", { name: "合成七日同期曲线 8", exact: true }) });
const otherPositions = await otherWindows.evaluateAll((nodes) => nodes.map((node) => {
const { x, y } = node.getBoundingClientRect();
return { x, y };
}));
const heading = (await popup.locator(".floating-window-heading").boundingBox())!;
const before = (await popup.boundingBox())!;
const cdp = await page.context().newCDPSession(page);
await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 });
await cdp.send("Performance.enable");
const first = await cdp.send("Performance.getMetrics");
const events: Array<{ name: string; dur?: number }> = [];
cdp.on("Tracing.dataCollected", ({ value }) => events.push(...value));
await cdp.send("Tracing.start", { categories: "devtools.timeline", transferMode: "ReportEvents" });
await page.evaluate(() => {
Object.assign(window, { dragSvgMutations: 0, dragTransformWrites: 0 });
const observer = new MutationObserver((records) => {
(window as unknown as { dragSvgMutations: number }).dragSvgMutations += records.length;
});
document.querySelectorAll("[data-floating-window] svg").forEach((node) => observer.observe(node, { subtree: true, attributes: true, childList: true }));
const windowObserver = new MutationObserver((records) => {
for (const record of records) {
const previous = document.createElement("div");
previous.setAttribute("style", record.oldValue ?? "");
if (previous.style.transform !== (record.target as HTMLElement).style.transform)
(window as unknown as { dragTransformWrites: number }).dragTransformWrites++;
}
});
document.querySelectorAll("[data-floating-window]").forEach((node) => windowObserver.observe(node, { attributes: true, attributeFilter: ["style"], attributeOldValue: true }));
});
const x = heading.x + heading.width - 110;
const y = heading.y + heading.height / 2;
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.move(x - 100, y - 60, { steps: 20 });
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-100);
await page.mouse.move(x - 200, y - 120, { steps: 20 });
await page.mouse.up();
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-200);
await expect.poll(async () => Math.round((await popup.boundingBox())!.y - before.y)).toBe(-120);
const last = await cdp.send("Performance.getMetrics");
const finished = new Promise<void>((resolve) => cdp.once("Tracing.tracingComplete", () => resolve()));
await cdp.send("Tracing.end");
await finished;
const metric = (name: string) => (last.metrics.find((item) => item.name === name)?.value ?? 0) - (first.metrics.find((item) => item.name === name)?.value ?? 0);
const summary = { metrics: Object.fromEntries(["ScriptDuration", "LayoutDuration", "RecalcStyleDuration", "TaskDuration", "LayoutCount", "RecalcStyleCount"].map((name) => [name, metric(name)])),
trace: Object.fromEntries(["Paint", "Layout", "UpdateLayoutTree", "FunctionCall"].map((name) => [name, { count: events.filter((event) => event.name === name).length,
ms: events.filter((event) => event.name === name).reduce((total, event) => total + (event.dur ?? 0), 0) / 1000 }])),
svgMutations: await page.evaluate(() => (window as unknown as { dragSvgMutations: number }).dragSvgMutations),
transformWrites: await page.evaluate(() => (window as unknown as { dragTransformWrites: number }).dragTransformWrites) };
await testInfo.attach("drag-profile", { body: JSON.stringify(summary, null, 2), contentType: "application/json" });
console.info("Window drag metrics", JSON.stringify(summary));
expect(summary.transformWrites).toBeLessThanOrEqual(2);
expect(summary.svgMutations).toBe(0);
expect(await otherWindows.evaluateAll((nodes) => nodes.map((node) => {
const { x, y } = node.getBoundingClientRect();
return { x, y };
}))).toEqual(otherPositions);
await testInfo.attach("multiple-charts-after-drag", { body: await page.screenshot({ path: testInfo.outputPath("multiple-charts-after-drag.png") }), contentType: "image/png" });
});
for (const height of [900, 720]) {
test(`multiple chart headings stay accessible and drag on first press at ${height}px height`, async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1440, height });
const task = investigationFixture(1, "多图窗口拖动核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
for (let index = 1; index <= 4; index++) {
await mock.emit(task, { ...chart(index), chart_id: `pressure-${index}` });
await expect(page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 合成压力分析 ${index}`, exact: true })).toBeVisible();
await openChild(page, `合成压力分析 ${index}`);
await expect(page.getByRole("dialog", { name: `合成压力分析 ${index}`, exact: true })).toBeVisible();
}
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
if (height === 900) await page.setViewportSize({ width: 1440, height: 720 });
for (let index = 1; index <= 4; index++) {
const popup = page.getByRole("dialog", { name: `合成压力分析 ${index}`, exact: true });
const heading = popup.locator(".floating-window-heading");
const box = (await heading.boundingBox())!;
expect((await popup.boundingBox())!.y).toBe(64 + (index - 1) * (box.height + 8));
await expect.poll(async () => {
const current = (await heading.boundingBox())!;
return page.evaluate(({ x, y }) => document.elementFromPoint(x, y)?.closest("[data-floating-window]")?.getAttribute("aria-labelledby"),
{ x: current.x + current.width / 2, y: current.y + current.height / 2 });
}).toBe(await popup.getAttribute("aria-labelledby"));
}
await testInfo.attach(`multiple-charts-before-drag-${height}`, {
body: await page.screenshot({ path: testInfo.outputPath(`multiple-charts-before-drag-${height}.png`) }), contentType: "image/png"
});
for (const index of [1, 4, 2]) {
const popup = page.getByRole("dialog", { name: `合成压力分析 ${index}`, exact: true });
const before = (await popup.boundingBox())!;
const heading = (await popup.locator(".floating-window-heading").boundingBox())!;
const start = { x: heading.x + heading.width - 110, y: heading.y + heading.height / 2 };
await page.mouse.move(start.x, start.y);
await page.mouse.down();
await page.mouse.move(start.x - 120, start.y - 32, { steps: 12 });
await page.mouse.up();
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-120);
await expect.poll(async () => Math.round((await popup.boundingBox())!.y - before.y)).toBe(Math.max(64, before.y - 32) - before.y);
expect((await popup.boundingBox())!.width).toBe(before.width);
expect((await popup.boundingBox())!.height).toBe(before.height);
}
await testInfo.attach(`multiple-charts-after-drag-${height}`, {
body: await page.screenshot({ path: testInfo.outputPath(`multiple-charts-after-drag-${height}.png`) }), contentType: "image/png"
});
expect(mock.mutations).toEqual([]);
});
}
test("live analysis updates one manager and respects dismissal", async ({ page }, testInfo) => {
const task = investigationFixture(1, "图表统一展示核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
const panel = page.getByLabel("调查实时展示", { exact: true });
const collapse = panel.getByRole("button", { name: "收起展示", exact: true });
await collapse.focus();
await mock.emit(task, chart(1));
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
await expect(manager).toBeVisible();
await expect(collapse).toBeFocused();
await expect(page.locator("[data-floating-window]")).toHaveCount(1);
await manager.locator(".floating-window-heading").focus();
await page.keyboard.press("ArrowRight");
const moved = await manager.boundingBox();
await mock.emit(task, chart(2));
await expect(manager.getByRole("img", { name: "合成压力分析 2", exact: true })).toBeVisible();
expect(await manager.boundingBox()).toEqual(moved);
await page.keyboard.press("Escape");
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await mock.emit(task, chart(3));
await expect(panel.getByRole("img", { name: "合成压力分析 3", exact: true })).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(manager.getByRole("img", { name: "合成压力分析 3", exact: true })).toBeVisible();
await testInfo.attach("live-chart-manager", { body: await manager.screenshot(), contentType: "image/png" });
expect(mock.mutations).toEqual([]);
});
test("playback restores charts without opening windows while live results remain separate", async ({ page }, testInfo) => {
const task = investigationFixture(1, "图表回放核查");
const commands = [chart(1), { action: "zoom_to_map", center: [121.48, 31.1], zoom: 13 }, chart(2), { action: "close_chart", chart_id: "pressure" }];
const saved = commands.map((command, index) => ({ task_id: task.task_id, event_seq: index + 1,
run_id: task.active_run!.run_id, execution_generation: 1, occurred_at: task.created_at, command }));
const mock = await mockInvestigationPresentation(page, [task], saved);
await selectInvestigation(page, task.title);
const panel = page.getByLabel("调查实时展示", { exact: true });
await expect(panel.getByRole("button", { name: "播放历史", exact: true })).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await panel.getByRole("button", { name: "播放历史", exact: true }).click();
await panel.getByRole("button", { name: "暂停历史播放", exact: true }).press("Enter");
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await openChild(page, "合成压力分析 1", false);
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
await expect(popup).toBeVisible();
await mock.emit(task, chart(3));
await expect(popup).toBeVisible();
await expect(page.getByRole("dialog", { name: "合成压力分析 3", exact: true })).toHaveCount(0);
await popup.getByRole("button", { name: "关闭窗口", exact: true }).click();
const progress = panel.getByRole("slider", { name: "历史播放进度" });
await progress.focus();
await page.keyboard.press("ArrowRight");
await expect(panel.getByRole("combobox", { name: "切换展示动作" })).toContainText("2.");
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await page.keyboard.press("ArrowRight");
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await expect(panel.getByRole("img", { name: "合成压力分析 2", exact: true })).toBeVisible();
await expect(progress).toBeFocused();
await testInfo.attach("playback-chart-window", { body: await page.screenshot({ path: testInfo.outputPath("playback-chart-window.png") }), contentType: "image/png" });
await page.keyboard.press("ArrowRight");
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await panel.getByRole("button", { name: "回到当前", exact: true }).click();
await expect(panel.getByRole("img", { name: "合成压力分析 3", exact: true })).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
expect(mock.mutations).toEqual([]);
});
test("structured analysis charts retain manual enlargement without opening during history", async ({ page }, testInfo) => {
const task = investigationFixture(1, "结构化分析展示核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
const tools = page.getByTestId("investigation-dev-tools");
await tools.getByLabel("调查展示 JSONL", { exact: true }).fill(JSON.stringify({ record_type: "ui_block", block: {
version: "answer-ui@1", block_id: "chart-analysis", phase: "final", fallback_text: "合成压力数据", spec: {
root: "analysis", elements: { analysis: { type: "Chart", children: [],
props: { ...chart(1).chart, source: "仅供浏览器测试的合成数据" } } }
}
} }));
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
const preview = page.getByLabel("本地预览展示", { exact: true });
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await preview.getByRole("button", { name: "放大查看 合成压力分析 1", exact: true }).click();
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
await expect(popup).toBeVisible();
await expect(popup.getByRole("img", { name: "合成压力分析 1", exact: true })).toBeVisible();
for (const width of [375, 320]) {
await page.setViewportSize({ width, height: 720 });
const box = (await popup.boundingBox())!;
expect(box.x).toBeGreaterThanOrEqual(0);
expect(box.x + box.width).toBeLessThanOrEqual(width);
expect(await popup.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
await testInfo.attach(`structured-chart-${width}`, { body: await page.screenshot({ path: testInfo.outputPath(`structured-chart-${width}.png`) }), contentType: "image/png" });
}
await popup.getByRole("button", { name: "关闭窗口", exact: true }).click();
await page.setViewportSize({ width: 1440, height: 900 });
await preview.getByRole("button", { name: "播放历史", exact: true }).click();
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
expect(mock.mutations).toEqual([]);
});
test("live charts fit desktop and narrow windows without outer scrollbars", async ({ page }, testInfo) => {
const task = investigationFixture(1, "实时图表紧凑布局核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await mock.emit(task, chart(1));
await openChild(page, "合成压力分析 1");
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
const content = popup.locator("[data-floating-window-content]");
await expect(popup.getByRole("img", { name: "合成压力分析 1", exact: true })).toBeVisible();
for (const [width, height] of [[1440, 900], [1280, 720], [800, 600], [375, 540], [320, 540]]) {
await page.setViewportSize({ width, height });
await expect.poll(() => content.evaluate((node) => ({
x: node.scrollWidth - node.clientWidth,
y: node.scrollHeight - node.clientHeight
}))).toEqual({ x: 0, y: 0 });
const plot = popup.getByRole("img", { name: "合成压力分析 1", exact: true });
expect((await plot.boundingBox())!.height).toBeGreaterThanOrEqual(120);
await testInfo.attach(`live-chart-fit-${width}`, {
body: await popup.screenshot({ path: testInfo.outputPath(`live-chart-fit-${width}.png`) }), contentType: "image/png"
});
}
await page.setViewportSize({ width: 1280, height: 720 });
await popup.getByRole("button", { name: "筛选数据范围", exact: true }).click();
await expect(popup.getByRole("combobox", { name: "区间起点" })).toBeVisible();
await expect.poll(() => content.evaluate((node) => node.scrollHeight - node.clientHeight)).toBe(0);
await popup.getByRole("tab", { name: "数据", exact: true }).click();
await expect(popup.getByRole("table")).toBeVisible();
expect(mock.mutations).toEqual([]);
});
test("SCADA chart axes and legends fit every nested container", async ({ page }, testInfo) => {
const task = investigationFixture(1, "SCADA 压力布局核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await mock.emit(task, { action: "open_chart", chart_id: "scada-pressure", evidence_refs: ["synthetic:pressure"], chart: {
title: "SCADA 回放压力:异常前后(2026-07-16,UTC+8)", chart_type: "line",
x_axis_name: "时间(UTC+8)", y_axis_name: "压力(m)", x_data: ["07:30", "07:45", "08:00"],
series: [{ name: "测点 123748(实测回放)", data: [26.2, 25.7, 23.5] },
{ name: "测点 159673(实测回放)", data: [24.9, 24.87, 21.6] }]
} });
await openChild(page, "SCADA 回放压力:异常前后(2026-07-16)");
const popup = page.getByRole("dialog", { name: "SCADA 回放压力:异常前后(2026-07-16)", exact: true });
await expect(popup).toBeVisible();
await expect.poll(() => popup.evaluate((node) => Array.from(node.querySelectorAll<HTMLElement>("*"))
.filter((element) => {
const style = getComputedStyle(element);
return (style.overflowX === "auto" || style.overflowX === "scroll" || style.overflowY === "auto" || style.overflowY === "scroll") &&
(element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight);
}).map((element) => ({ class: element.className, x: element.scrollWidth - element.clientWidth, y: element.scrollHeight - element.clientHeight }))
)).toEqual([]);
const plot = popup.getByRole("group", { name: /按回车查看数据点/ });
await expect.poll(() => plot.evaluate((element) => ({
x: element.scrollWidth - element.clientWidth, y: element.scrollHeight - element.clientHeight
}))).toEqual({ x: 0, y: 0 });
await testInfo.attach("scada-chart-layout", {
body: await popup.screenshot({ path: testInfo.outputPath("scada-chart-layout.png") }), contentType: "image/png"
});
});
test("SCADA pressure and flow keep full comparison data without timezone wording", async ({ page }, testInfo) => {
const task = investigationFixture(1, "SCADA 三小时对照核查");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
const labels = Array.from({ length: 13 }, (_, i) => `${String(4 + Math.floor((45 + i * 15) / 60)).padStart(2, "0")}:${String((45 + i * 15) % 60).padStart(2, "0")}`);
for (const [metric, device, unit] of [["压力", "123748", "m"], ["流量", "487434", "m³/h"]]) {
await mock.emit(task, { action: "open_chart", chart_id: `scada-${device}`, evidence_refs: ["synthetic:pressure"], chart: {
title: `回放${metric}:${device}(${unit},上海时间 07-16 04:45–07:45)`, chart_type: "line",
x_axis_name: "上海时间", y_axis_name: `${metric}(${unit})`, x_data: labels,
series: [{ name: "当前回放(2026-07-16)", type: "line", data: labels.map((_, i) => 30 + i) },
...Array.from({ length: 7 }, (_, day) => ({ name: `参考 07-${String(9 + day).padStart(2, "0")}`, type: "scatter",
data: labels.map((_, i) => i < 10 ? null : 28 + day) }))]
} });
await expect(page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 回放${metric}:${device}(${unit},07-16 04:45–07:45)`, exact: true })).toBeVisible();
await openChild(page, `回放${metric}:${device}(${unit},07-16 04:45–07:45)`);
const popup = page.getByRole("dialog", { name: `回放${metric}:${device}(${unit},07-16 04:45–07:45)`, exact: true });
await expect(popup).toBeVisible();
await expect(popup.getByText(/上海时间|UTC\+8|Asia\/Shanghai/)).toHaveCount(0);
await expect(popup.getByRole("img", { name: `回放${metric}:${device}(${unit},07-16 04:45–07:45)`, exact: true })).toBeVisible();
await testInfo.attach(`scada-${device}-comparison`, { body: await popup.screenshot(), contentType: "image/png" });
await popup.getByRole("tab", { name: "数据", exact: true }).click();
const table = popup.getByRole("table");
await expect(table).toBeVisible();
await expect(table.getByRole("row")).toHaveCount(14);
await expect(table.getByRole("columnheader")).toHaveCount(9);
await popup.getByRole("button", { name: "关闭窗口", exact: true }).click();
}
});