Files
next-tjwater-frontend/tests/browser/investigation-merged-history.e2e.ts
T
jiang 3de96f9d7b
Generic Container CI/CD / test-build-publish (push) Successful in 1m10s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m10s
fix(workbench): stabilize presentation history hover
Rapid pointer changes repeatedly restarted inherited color transitions in both presentation menu levels. Disable transitions on these rows while preserving focus and submenu behavior. Previous coverage checked open state only; the browser regression now compares next-frame highlight colors against the theme and fails with the old transition restored.

Also verify header timeline hide/restore after expansion at 320, 375, 1024 and 1440 pixels.

Validation: lint, typecheck, 1178 unit tests, five focused browser cases and production build passed.
2026-11-12 13:16:15 +08:00

243 lines
16 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 Locator, type Page, type TestInfo } from "@playwright/test";
import { investigationFixture, mockInvestigationPresentation, selectInvestigation, showInvestigationMap } from "./support/mock-investigation-presentation";
import { readThemeColor } from "./support/theme-colors";
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 sampleFastHoverBackground(items: Locator[]) {
const samples: Array<{ actual: string; expected: string }> = [];
for (let repeat = 0; repeat < 3; repeat++) {
for (const item of items) {
const expected = await readThemeColor(item, "--action-selection-soft");
await item.hover();
const actual = await item.evaluate((node) => new Promise<string>((resolve) => {
requestAnimationFrame(() => resolve(getComputedStyle(node).backgroundColor));
}));
samples.push({ actual, expected });
}
}
return samples;
}
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 });
const firstLevel = await sampleFastHoverBackground([title, root.getByRole("menuitem", { name: "覆盖层", exact: true })]);
await arrow.hover();
await expect(steps).toBeVisible();
const secondLevel = await sampleFastHoverBackground([
steps.getByRole("menuitem", { name: "1. 测点 P1 压力趋势", exact: true }),
steps.getByRole("menuitem", { name: "2. 测点 P2 压力趋势", exact: true })
]);
const backgrounds = [...firstLevel, ...secondLevel];
await testInfo.attach("fast-hover-backgrounds", { body: JSON.stringify({ firstLevel, secondLevel }), contentType: "application/json" });
expect(backgrounds).toHaveLength(12);
expect(backgrounds.every((sample) => sample.expected !== "rgba(0, 0, 0, 0)")).toBe(true);
expect(backgrounds.map((sample) => sample.actual)).toEqual(backgrounds.map((sample) => sample.expected));
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([]);
});