import { expect, test } 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] }] } }); test.setTimeout(60_000); 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.getByRole("dialog", { name: `合成压力分析 ${index}`, exact: true })).toBeVisible(); } 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)); const target = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; expect(await page.evaluate(({ x, y }) => document.elementFromPoint(x, y)?.closest("[data-floating-window]")?.getAttribute("aria-labelledby"), target)) .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(-32); 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 opens a window, preserves its position and respects closing and task switches", async ({ page }, testInfo) => { const task = investigationFixture(1, "图表自动展示核查"); const other = investigationFixture(2, "另一项调查"); const mock = await mockInvestigationPresentation(page, [task, other]); await selectInvestigation(page, task.title); const panel = page.getByLabel("调查实时展示", { exact: true }); await expect(panel.getByText("实时跟随", { exact: true })).toBeVisible(); const collapse = panel.getByRole("button", { name: "收起展示", exact: true }); await collapse.focus(); await mock.emit(task, chart(1)); let popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true }); await expect(popup).toBeVisible(); await expect(popup.getByRole("img", { name: "合成压力分析 1", exact: true })).toBeVisible(); await expect(collapse).toBeFocused(); const heading = popup.getByLabel("窗口标题栏,可用方向键移动,Shift 加方向键调整大小"); await heading.focus(); await page.keyboard.press("ArrowRight"); const moved = (await popup.boundingBox())!; await mock.emit(task, chart(2)); popup = page.getByRole("dialog", { name: "合成压力分析 2", exact: true }); await expect(popup).toBeVisible(); expect((await popup.boundingBox())!.x).toBe(moved.x); await page.keyboard.press("Escape"); await expect(page.locator("[data-floating-window]")).toHaveCount(0); await mock.emit(task, { action: "zoom_to_map", center: [121.48, 31.1], zoom: 13 }); await expect(panel.getByRole("combobox", { name: "切换展示动作" })).toContainText("3 个动作"); await expect(page.locator("[data-floating-window]")).toHaveCount(0); await panel.getByRole("button", { name: "放大查看 合成压力分析 2", exact: true }).click(); await expect(popup).toBeVisible(); await testInfo.attach("live-chart-window", { body: await page.screenshot({ path: testInfo.outputPath("live-chart-window.png") }), contentType: "image/png" }); await popup.getByRole("button", { name: "关闭窗口", exact: true }).click(); await mock.emit(task, chart(3)); await expect(page.getByRole("dialog", { name: "合成压力分析 3", exact: true })).toBeVisible(); await mock.emit(task, { action: "close_chart", chart_id: "pressure" }); await expect(page.locator("[data-floating-window]")).toHaveCount(0); await mock.emit(task, chart(4)); await expect(page.getByRole("dialog", { name: "合成压力分析 4", exact: true })).toBeVisible(); await page.getByRole("region", { name: "工况任务", exact: true }).getByText(other.title, { exact: true }).first().click(); await expect(page.locator("[data-floating-window]")).toHaveCount(0); expect(mock.mutations).toEqual([]); }); test("playback windows follow saved snapshots 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"); 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.getByRole("dialog", { 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(page.getByRole("dialog", { name: "合成压力分析 3", exact: true })).toBeVisible(); expect(mock.mutations).toEqual([]); }); test("structured analysis charts open responsive windows in local preview and 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 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 }); const preview = page.getByLabel("本地预览展示", { exact: true }); await preview.getByRole("button", { name: "播放历史", exact: true }).click(); await expect(popup).toBeVisible(); 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)); 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] }] } }); 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("*")) .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) }))] } }); 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(); } });