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.
This commit is contained in:
@@ -15,7 +15,7 @@ function subscribeCompact(listener: () => void) {
|
||||
media.addEventListener("change", listener);
|
||||
return () => media.removeEventListener("change", listener);
|
||||
}
|
||||
const rowClass = "min-h-8 gap-2 text-xs active:bg-accent";
|
||||
const rowClass = "min-h-8 gap-2 text-xs transition-none active:bg-accent";
|
||||
|
||||
export function PresentationHistoryMenu({ entries, selectedId, mergedEntry, mergedEntries, onSelect }: PresentationHistoryControls) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -6,6 +6,40 @@ test.beforeEach(async ({ page }) => {
|
||||
await prepareWorkbench(page);
|
||||
});
|
||||
|
||||
for (const width of [1440, 1024, 375, 320]) {
|
||||
test(`header hides and restores the expanded timeline at ${width}px`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
const header = page.locator(".workbench-top-bar");
|
||||
const timeline = page.getByRole("region", {
|
||||
name: width >= 1024 ? "运行时间轴" : "移动端运行时间轴摘要", exact: true
|
||||
});
|
||||
const hide = header.getByRole("button", { name: "隐藏运行时间轴", exact: true });
|
||||
await expect(timeline).toBeVisible();
|
||||
await expect(hide).toHaveAttribute("aria-pressed", "true");
|
||||
const bounds = (await hide.boundingBox())!;
|
||||
expect(bounds.x).toBeGreaterThanOrEqual(0);
|
||||
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
|
||||
if (width >= 1024) await timeline.getByRole("button", { name: "展开时间轴", exact: true }).click();
|
||||
else {
|
||||
await timeline.getByRole("button", { name: "打开完整运行时间轴", exact: true }).click();
|
||||
await expect(page.getByRole("region", { name: "运行时间轴抽屉", exact: true })).toBeVisible();
|
||||
}
|
||||
await hide.click();
|
||||
await expect(timeline).toBeHidden();
|
||||
await expect(page.getByRole("region", { name: "运行时间轴抽屉", exact: true })).toBeHidden();
|
||||
const show = header.getByRole("button", { name: "显示运行时间轴", exact: true });
|
||||
await expect(show).toHaveAttribute("aria-pressed", "false");
|
||||
await show.click();
|
||||
await expect(timeline).toBeVisible();
|
||||
await expect(hide).toHaveAttribute("aria-pressed", "true");
|
||||
if (width >= 1024) await expect(timeline.getByRole("button", { name: "收起时间轴", exact: true })).toBeVisible();
|
||||
await testInfo.attach(`header-timeline-${width}`, {
|
||||
body: await header.screenshot(), contentType: "image/png"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("desktop controls share focus, hover, press and open states", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
||||
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))));
|
||||
@@ -56,8 +72,18 @@ test("hover keeps an expanded category stable across its title, arrow and origin
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user