import { expect, test, type Page, type TestInfo } from "@playwright/test"; import { investigationFixture, mockInvestigationPresentation, selectInvestigation, showInvestigationMap } from "./support/mock-investigation-presentation"; const chart = (id: string, title: string) => ({ action: "open_chart", chart_id: id, evidence_refs: ["synthetic:reading"], chart: { title, chart_type: "line", x_data: ["08:00", "08:15"], series: [{ name: "压力(m)", data: [30, 28] }] } }); async function captureMenus(page: Page, testInfo: TestInfo, name: string) { for (const menu of await page.getByRole("menu").all()) { await menu.evaluate((node) => Promise.all(node.getAnimations().map((animation) => animation.finished.catch(() => undefined)))); } const boxes = await Promise.all((await page.getByRole("menu").all()).map((menu) => menu.boundingBox())); const visible = boxes.filter((box) => box !== null); const x = Math.min(...visible.map((box) => box.x)); const y = Math.min(...visible.map((box) => box.y)); const right = Math.max(...visible.map((box) => box.x + box.width)); const bottom = Math.max(...visible.map((box) => box.y + box.height)); await testInfo.attach(name, { body: await page.screenshot({ path: testInfo.outputPath(`${name}.png`), clip: { x, y, width: right - x, height: bottom - y } }), contentType: "image/png" }); } test("pins the latest original step while new live charts continue arriving", async ({ page }) => { const task = investigationFixture(1, "最新步骤固定回看"); const saved = [chart("P1", "压力 1"), chart("P2", "压力 2")].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, "keyboard"); await showInvestigationMap(page); const panel = page.getByLabel("调查实时展示", { exact: true }); await panel.getByRole("button", { name: "切换展示动作", exact: true }).click(); await page.getByRole("menuitem", { name: "展开图表步骤", exact: true }).click(); await page.getByRole("menuitem", { name: "2. 压力 2", exact: true }).click(); await expect(panel.getByRole("button", { name: "回到当前", exact: true })).toBeVisible(); await mock.emit(task, chart("P3", "压力 3")); await expect(panel.getByText("后续 1 个动作", { exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "压力 2", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "压力 3", exact: true })).toHaveCount(0); await panel.getByRole("button", { name: "回到当前", exact: true }).click(); await expect(panel.getByRole("group", { name: "压力 3", exact: true })).toBeVisible(); expect(mock.mutations).toEqual([]); }); test("hover keeps an expanded category stable across its title, arrow and original steps", async ({ page }, testInfo) => { const task = investigationFixture(1, "历史菜单悬停核查"); const overlay = { action: "set_overlay", overlay_id: "hover-area", title: "隔离影响范围", mode: "geometry", evidence_id: "synthetic:area", data: { type: "FeatureCollection", features: [{ type: "Feature", properties: {}, geometry: { type: "Polygon", coordinates: [[[121.48, 31.1], [121.49, 31.1], [121.49, 31.11], [121.48, 31.1]]] } }] } }; const saved = [chart("P1", "测点 P1 压力趋势"), chart("P2", "测点 P2 压力趋势"), overlay].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, "keyboard"); await showInvestigationMap(page); await page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: "切换展示动作", exact: true }).click(); const root = page.getByRole("menu", { name: "展示类别", exact: true }); const arrow = root.getByRole("menuitem", { name: "展开图表步骤", exact: true }); const title = root.getByRole("menuitem", { name: "图表", exact: true }); const steps = page.getByRole("menu", { name: "图表展示步骤", exact: true }); await arrow.hover(); await expect(steps).toBeVisible(); await page.evaluate(() => { const changes: string[] = []; const observer = new MutationObserver((records) => { for (const record of records) { if (record.type === "attributes" && record.target instanceof Element && record.target.getAttribute("aria-label") === "展开图表步骤") changes.push(record.target.getAttribute("data-state") ?? ""); } }); observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["data-state"] }); Object.assign(window, { historyHoverChanges: changes }); }); for (let repeat = 0; repeat < 3; repeat++) { await title.hover(); const titleBox = await title.boundingBox(); await page.mouse.move(titleBox!.x + 12, titleBox!.y + titleBox!.height / 2, { steps: 8 }); await expect(steps).toBeVisible(); await arrow.hover(); await expect(steps).toBeVisible(); await steps.getByRole("menuitem", { name: "1. 测点 P1 压力趋势", exact: true }).hover(); await expect(steps).toBeVisible(); await arrow.hover(); } const changes = await page.evaluate(() => (window as unknown as { historyHoverChanges: string[] }).historyHoverChanges); await testInfo.attach("hover-state-changes", { body: JSON.stringify(changes), contentType: "application/json" }); expect(changes).not.toContain("closed"); await captureMenus(page, testInfo, "stable-history-hover"); await root.getByRole("menuitem", { name: "展开覆盖层步骤", exact: true }).hover(); await expect(page.getByRole("menu", { name: "覆盖层展示步骤", exact: true })).toBeVisible(); await expect(steps).toHaveCount(0); await arrow.hover(); await expect(steps).toBeVisible(); await expect(page.getByRole("menu", { name: "覆盖层展示步骤", exact: true })).toHaveCount(0); await title.click(); await expect(root).toHaveCount(0); const panel = page.getByLabel("调查实时展示", { exact: true }); await expect(panel.getByRole("group", { name: "测点 P1 压力趋势", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "测点 P2 压力趋势", exact: true })).toBeVisible(); expect(mock.mutations).toEqual([]); }); test("adjacent historical rounds share one first-level group and keep their original steps below", async ({ page }, testInfo) => { const task = investigationFixture(1, "历史 SCADA 多轮调查"); task.execution_generation = 14; task.active_run!.generation = 14; const saved = [1, 7, 11].flatMap((generation, index) => [1, 2].map((step) => ({ task_id: task.task_id, run_id: task.active_run!.run_id, event_seq: index * 2 + step, execution_generation: generation, occurred_at: task.created_at, command: chart(`G${generation}-${step}`, `第 ${generation} 轮压力 ${step}`) }))); const mock = await mockInvestigationPresentation(page, [task], saved); await selectInvestigation(page, task.title, "keyboard"); await showInvestigationMap(page); const panel = page.getByLabel("调查实时展示", { exact: true }); const trigger = panel.getByRole("button", { name: "切换展示动作", exact: true }); await trigger.click(); const root = page.getByRole("menu", { name: "展示类别", exact: true }); await expect(root.locator("[data-history-group-row]")).toHaveText(["图表"]); await root.getByRole("menuitem", { name: "图表", exact: true }).focus(); await page.keyboard.press("ArrowRight"); const charts = page.getByRole("menu", { name: "图表展示步骤", exact: true }); await expect(charts.getByRole("menuitem")).toHaveText([ "6. 第 11 轮压力 2", "5. 第 11 轮压力 1", "4. 第 7 轮压力 2", "3. 第 7 轮压力 1", "2. 第 1 轮压力 2", "1. 第 1 轮压力 1" ]); await expect(page.getByText("合并查看", { exact: true })).toHaveCount(0); await captureMenus(page, testInfo, "adjacent-round-submenu"); await page.keyboard.press("ArrowLeft"); await expect(root).toBeVisible(); await root.getByRole("menuitem", { name: "图表", exact: true }).click(); await expect(trigger).toHaveText("图表"); await expect(panel.getByRole("group", { name: "第 7 轮压力 1", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "第 1 轮压力 1", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "第 11 轮压力 1", exact: true })).toBeVisible(); expect(mock.mutations).toEqual([]); }); test("first-level titles display merged adjacent steps and arrows expose only their own steps on desktop and narrow screens", async ({ page }, testInfo) => { test.setTimeout(60_000); const task = investigationFixture(1, "SCADA 调查相邻步骤核查"); const overlay = { action: "set_overlay", overlay_id: "test-area", title: "隔离影响范围", mode: "geometry", evidence_id: "synthetic:area", data: { type: "FeatureCollection", features: [{ type: "Feature", properties: {}, geometry: { type: "Polygon", coordinates: [[[121.48, 31.1], [121.49, 31.1], [121.49, 31.11], [121.48, 31.1]]] } }] } }; const saved = [chart("P1", "测点 P1 压力趋势"), chart("P2", "测点 P2 压力趋势"), overlay, chart("P3", "测点 P3 压力趋势"), chart("P4", "测点 P4 压力趋势")].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, "keyboard"); await showInvestigationMap(page); const panel = page.getByLabel("调查实时展示", { exact: true }); await expect(panel.getByRole("group", { name: "测点 P4 压力趋势", exact: true })).toBeVisible(); const closeWindow = page.getByRole("dialog").getByRole("button", { name: "关闭窗口", exact: true }); while (await closeWindow.count()) await closeWindow.first().click(); const trigger = panel.getByRole("button", { name: "切换展示动作", exact: true }); await trigger.click(); const root = page.getByRole("menu", { name: "展示类别", exact: true }); await expect(root.locator("[data-history-group-row]")).toHaveText(["图表", "覆盖层", "图表"]); await expect(root).not.toContainText("合并查看"); await expect(root).not.toContainText("第 1 轮"); await captureMenus(page, testInfo, "adjacent-groups-desktop"); await root.getByRole("menuitem", { name: "图表", exact: true }).first().click(); await expect(trigger).toHaveText("图表"); await expect(panel.getByText("历史回看", { exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "测点 P3 压力趋势", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "测点 P4 压力趋势", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "测点 P1 压力趋势", exact: true })).toHaveCount(0); await expect(panel.getByRole("group", { name: "测点 P2 压力趋势", exact: true })).toHaveCount(0); await expect(panel.getByRole("region", { name: "调查覆盖层" })).toHaveCount(0); await mock.emit(task, chart("P5", "测点 P5 压力趋势")); await expect(panel.getByText("后续 1 个动作", { exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "测点 P5 压力趋势", exact: true })).toHaveCount(0); await trigger.click(); await root.getByRole("menuitem", { name: "展开图表步骤", exact: true }).first().click(); const charts = page.getByRole("menu", { name: "图表展示步骤", exact: true }); await expect(charts.getByRole("menuitem")).toHaveText(["6. 测点 P5 压力趋势", "5. 测点 P4 压力趋势", "4. 测点 P3 压力趋势"]); await captureMenus(page, testInfo, "adjacent-group-submenu-desktop"); await page.keyboard.press("Escape"); await page.setViewportSize({ width: 375, height: 720 }); await showInvestigationMap(page); await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click())); await trigger.click(); await expect(root.locator("[data-history-group-row]")).toHaveText(["图表", "覆盖层", "图表"]); await root.getByRole("menuitem", { name: "展开图表步骤", exact: true }).last().click(); const compactCharts = page.getByRole("menu", { name: "图表展示步骤", exact: true }); await expect(compactCharts.getByRole("menuitem")).toHaveText(["返回类别", "2. 测点 P2 压力趋势", "1. 测点 P1 压力趋势"]); const bounds = await compactCharts.boundingBox(); expect(bounds!.x).toBeGreaterThanOrEqual(0); expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(375); expect(await compactCharts.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1); await captureMenus(page, testInfo, "adjacent-submenu-narrow"); await compactCharts.getByRole("menuitem", { name: "返回类别", exact: true }).click(); await expect(root.locator("[data-history-group-row]")).toHaveText(["图表", "覆盖层", "图表"]); await root.getByRole("menuitem", { name: "图表", exact: true }).last().click(); await expect(panel.getByRole("group", { name: "测点 P1 压力趋势", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "测点 P2 压力趋势", exact: true })).toBeVisible(); await expect(panel.getByRole("group", { name: "测点 P3 压力趋势", exact: true })).toHaveCount(0); await trigger.click(); await expect(trigger).toHaveAttribute("aria-expanded", "true"); await trigger.click(); await expect(trigger).toHaveAttribute("aria-expanded", "false"); await trigger.click(); await expect(trigger).toHaveAttribute("aria-expanded", "true"); await panel.getByRole("heading", { name: "调查实时展示", exact: true }).click(); await expect(trigger).toHaveAttribute("aria-expanded", "false"); await trigger.click(); await expect(trigger).toHaveAttribute("aria-expanded", "true"); await root.getByRole("menuitem", { name: "展开图表步骤", exact: true }).last().click(); await compactCharts.getByRole("menuitem", { name: "1. 测点 P1 压力趋势", exact: true }).click(); await expect(panel.getByRole("group", { name: "测点 P2 压力趋势", exact: true })).toHaveCount(0); await panel.getByRole("button", { name: "回到当前", exact: true }).click(); await expect(panel.getByRole("group", { name: "测点 P5 压力趋势", exact: true })).toBeVisible(); await trigger.click(); await root.getByRole("menuitem", { name: "覆盖层", exact: true }).click(); await expect(panel.getByRole("region", { name: "调查覆盖层" })).toContainText("隔离影响范围"); await expect(panel.getByRole("group", { name: "测点 P1 压力趋势", exact: true })).toHaveCount(0); expect(mock.mutations).toEqual([]); });