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.
243 lines
16 KiB
TypeScript
243 lines
16 KiB
TypeScript
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([]);
|
||
});
|