Group adjacent live display steps into category menus with merged snapshots, original-step submenus and scoped playback. The submenu exposes the latest raw step, so explicit selections must remain pinned. Locate failed and cancelled steps using the full group sequence, and prevent an exiting menu from dismissing a trigger-owned reopen. Regression tests reproduce these paths before the fixes. Validation: lint, typecheck, 1178 unit tests and 10 focused browser cases passed; update the presentation history guide.
217 lines
15 KiB
TypeScript
217 lines
15 KiB
TypeScript
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([]);
|
||
});
|