diff --git a/src/features/workbench/components/presentation-history-menu.tsx b/src/features/workbench/components/presentation-history-menu.tsx index 1d652cd..d2072cf 100644 --- a/src/features/workbench/components/presentation-history-menu.tsx +++ b/src/features/workbench/components/presentation-history-menu.tsx @@ -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); diff --git a/tests/browser/button-system.e2e.ts b/tests/browser/button-system.e2e.ts index 4835042..f678dd9 100644 --- a/tests/browser/button-system.e2e.ts +++ b/tests/browser/button-system.e2e.ts @@ -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" }); diff --git a/tests/browser/investigation-merged-history.e2e.ts b/tests/browser/investigation-merged-history.e2e.ts index 8109326..d82dc4f 100644 --- a/tests/browser/investigation-merged-history.e2e.ts +++ b/tests/browser/investigation-merged-history.e2e.ts @@ -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((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) => {