From 839123caf2db6c19fc3b8922fda572cf0f446516 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:41:22 +0800 Subject: [PATCH] fix(run): use 16px test status icons and the shared running indicator (#2487) Run and Services test trees now use 16px status and filter icons and the shared eight-spoke running indicator. The fixed 11/13px glyphs made columns uneven, and a status change shifted the name; the running indicator now follows the tree's icon size. --- e2e/testRunner-ui.spec.ts | 128 ++++++++++++++++++++++++++ src/features/run/Run.module.css | 7 +- src/features/run/TestTreeView.tsx | 18 ++-- src/ui/expui/run/showPassed.svg | 4 + src/ui/expui/run/showPassed_dark.svg | 4 + src/ui/expui/run/testFailed.svg | 4 + src/ui/expui/run/testFailed_dark.svg | 4 + src/ui/expui/run/testIgnored.svg | 5 + src/ui/expui/run/testIgnored_dark.svg | 5 + src/ui/expui/run/testPassed.svg | 4 + src/ui/expui/run/testPassed_dark.svg | 4 + src/ui/icons.tsx | 6 ++ 12 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 e2e/testRunner-ui.spec.ts create mode 100644 src/ui/expui/run/showPassed.svg create mode 100644 src/ui/expui/run/showPassed_dark.svg create mode 100644 src/ui/expui/run/testFailed.svg create mode 100644 src/ui/expui/run/testFailed_dark.svg create mode 100644 src/ui/expui/run/testIgnored.svg create mode 100644 src/ui/expui/run/testIgnored_dark.svg create mode 100644 src/ui/expui/run/testPassed.svg create mode 100644 src/ui/expui/run/testPassed_dark.svg diff --git a/e2e/testRunner-ui.spec.ts b/e2e/testRunner-ui.spec.ts new file mode 100644 index 000000000..4fecb0aca --- /dev/null +++ b/e2e/testRunner-ui.spec.ts @@ -0,0 +1,128 @@ +import { expect, test } from "./fixtures"; + +for (const theme of ["light", "dark"] as const) { + for (const width of [140, 320]) { + test(`test runner keeps status columns aligned in ${theme} at ${width}px`, async ({ workbench: page }) => { + const id = await page.evaluate(async (theme) => { + const storePath = "/src/features/run/store.ts"; + const resultsPath = "/src/features/run/testing/index.ts"; + const themePath = "/src/workbench/theme.ts"; + const run = await import(storePath); + const { TestResults } = await import(resultsPath); + const { previewTheme } = await import(themePath); + previewTheme(theme); + localStorage.setItem("openidea.run.testFilters", JSON.stringify({ showPassed: true, showIgnored: true })); + await run.startSampleOutput(0, 1); + const id = run.useRun.getState().activeSession; + const results = new TestResults(); + results.apply([ + { type: "result", path: ["AlignmentSuite", "passed case"], status: "passed", durationMs: 12 }, + { type: "result", path: ["AlignmentSuite", "failed case"], status: "failed", durationMs: 23 }, + { type: "result", path: ["AlignmentSuite", "ignored case"], status: "skipped" }, + { type: "start", path: ["AlignmentSuite", "running case"] }, + ]); + results.node(["AlignmentSuite", "pending case"]); + run.runtime(id).tests = results; + const state = run.useRun.getState(); + run.useRun.setState({ + sessions: state.sessions.map((s: { id: string; config: object }) => + s.id === id ? { ...s, status: "running", config: { ...s.config, isTest: true } } : s, + ), + versions: { ...state.versions, [id]: (state.versions[id] ?? 0) + 1 }, + }); + return id; + }, theme); + + const run = page.getByRole("region", { name: "Run", exact: true }); + const panel = run.getByTestId("tests"); + const tree = panel.getByRole("tree", { name: "Test results" }); + await expect(tree.getByText("pending case", { exact: true })).toBeVisible(); + const bounds = (await panel.boundingBox())!; + const handle = (await run.getByRole("separator").boundingBox())!; + const y = handle.y + handle.height / 2; + await page.mouse.move(handle.x + handle.width / 2, y); + await page.mouse.down(); + await page.mouse.move(bounds.x + width, y, { steps: 4 }); + await page.mouse.up(); + await expect.poll(async () => Math.abs((await panel.boundingBox())!.width - width)).toBeLessThan(2); + if (process.env.OPENIDEA_TEST_RUNNER_SHOTS) + await panel.screenshot({ path: `${process.env.OPENIDEA_TEST_RUNNER_SHOTS}/${theme}-${width}.png` }); + + const cases = ["passed case", "failed case", "ignored case", "running case", "pending case"]; + const row = (name: string) => tree.getByRole("treeitem").filter({ hasText: name }); + const checkColumns = async (size: number) => { + const starts: number[] = []; + for (const name of cases) { + const icon = row(name).locator('svg[role="img"], svg[role="status"]'); + const iconBounds = (await icon.boundingBox())!; + const dimensions = await icon.evaluate((node) => { + const style = getComputedStyle(node); + return { width: Number.parseFloat(style.width), height: Number.parseFloat(style.height) }; + }); + expect.soft(dimensions.width).toBe(size); + expect.soft(dimensions.height).toBe(size); + const label = (await row(name).getByText(name, { exact: true }).boundingBox())!; + const rowBounds = (await row(name).boundingBox())!; + starts.push(label.x); + expect + .soft(Math.abs(iconBounds.y + iconBounds.height / 2 - (rowBounds.y + rowBounds.height / 2))) + .toBeLessThan(1); + } + expect.soft(Math.max(...starts) - Math.min(...starts)).toBeLessThan(1); + }; + await checkColumns(16); + const paneBounds = (await panel.boundingBox())!; + for (const title of ["Show Passed", "Show Ignored"]) { + const button = panel.getByRole("button", { name: title, exact: true }); + const rect = (await button.boundingBox())!; + expect(rect.x).toBeGreaterThanOrEqual(paneBounds.x); + expect(rect.x + rect.width).toBeLessThanOrEqual(paneBounds.x + paneBounds.width + 1); + expect.soft((await button.locator("svg").boundingBox())!.width).toBe(16); + } + const fills = await row("failed case") + .getByRole("img", { name: "Failed", exact: true }) + .locator("[fill]") + .evaluateAll((nodes) => + nodes.filter((node) => node.getBoundingClientRect().width > 0).map((node) => getComputedStyle(node).fill), + ); + expect.soft(fills).toContain(theme === "light" ? "rgb(255, 175, 15)" : "rgb(242, 197, 92)"); + + await page.evaluate(async () => { + const path = "/src/ui/iconAppearance.ts"; + (await import(path)).setIconAppearance({ iconSize: 20, rowHeight: 28 }); + }); + await checkColumns(20); + const before = (await row("running case").getByText("running case", { exact: true }).boundingBox())!.x; + await page.evaluate(async (id) => { + const path = "/src/features/run/store.ts"; + const run = await import(path); + run + .runtime(id) + .tests.apply([ + { type: "result", path: ["AlignmentSuite", "running case"], status: "passed", durationMs: 34 }, + ]); + const state = run.useRun.getState(); + run.useRun.setState({ versions: { ...state.versions, [id]: (state.versions[id] ?? 0) + 1 } }); + }, id); + await expect(row("running case").getByRole("img", { name: "Passed", exact: true })).toBeVisible(); + expect.soft((await row("running case").getByText("running case", { exact: true }).boundingBox())!.x).toBe(before); + await page.evaluate(async () => { + const path = "/src/ui/iconAppearance.ts"; + (await import(path)).setIconAppearance(null); + }); + + const showPassed = panel.getByRole("button", { name: "Show Passed", exact: true }); + await showPassed.focus(); + await page.keyboard.press("Enter"); + await expect(row("passed case")).toHaveCount(0); + await expect(row("running case")).toHaveCount(0); + await showPassed.click(); + await expect(row("passed case")).toBeVisible(); + const showIgnored = panel.getByRole("button", { name: "Show Ignored", exact: true }); + await showIgnored.click(); + await expect(row("ignored case")).toHaveCount(0); + await showIgnored.click(); + await expect(row("ignored case")).toBeVisible(); + }); + } +} diff --git a/src/features/run/Run.module.css b/src/features/run/Run.module.css index 06c897e5c..4d191a74f 100644 --- a/src/features/run/Run.module.css +++ b/src/features/run/Run.module.css @@ -222,6 +222,10 @@ min-width: 0; flex: 1; } +.testRow .testStatus { + width: var(--oi-tree-icon-size, 16px); + height: var(--oi-tree-icon-size, 16px); +} .testName { flex: 1; min-width: 0; @@ -242,9 +246,6 @@ font-size: var(--fs-ui); } -.passed { - color: var(--success); -} .failed { color: var(--danger); } diff --git a/src/features/run/TestTreeView.tsx b/src/features/run/TestTreeView.tsx index 29f432541..ce5f17b39 100644 --- a/src/features/run/TestTreeView.tsx +++ b/src/features/run/TestTreeView.tsx @@ -72,15 +72,15 @@ function totalDuration(node: TestNode): number | undefined { export function StatusIcon({ status }: { status: TestStatus | null }) { switch (status) { case "passed": - return ; + return ; case "failed": - return ; + return ; case "skipped": - return ; + return ; case "running": - return ; + return ; default: - return ; + return ; } } @@ -225,17 +225,17 @@ export function TestTreeView({ toggle("showPassed")} /> toggle("showIgnored")} diff --git a/src/ui/expui/run/showPassed.svg b/src/ui/expui/run/showPassed.svg new file mode 100644 index 000000000..5cba21822 --- /dev/null +++ b/src/ui/expui/run/showPassed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/expui/run/showPassed_dark.svg b/src/ui/expui/run/showPassed_dark.svg new file mode 100644 index 000000000..c48940b4e --- /dev/null +++ b/src/ui/expui/run/showPassed_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/expui/run/testFailed.svg b/src/ui/expui/run/testFailed.svg new file mode 100644 index 000000000..0948312d7 --- /dev/null +++ b/src/ui/expui/run/testFailed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/expui/run/testFailed_dark.svg b/src/ui/expui/run/testFailed_dark.svg new file mode 100644 index 000000000..e3cad5dc2 --- /dev/null +++ b/src/ui/expui/run/testFailed_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/expui/run/testIgnored.svg b/src/ui/expui/run/testIgnored.svg new file mode 100644 index 000000000..bac178f99 --- /dev/null +++ b/src/ui/expui/run/testIgnored.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ui/expui/run/testIgnored_dark.svg b/src/ui/expui/run/testIgnored_dark.svg new file mode 100644 index 000000000..504b6a8cd --- /dev/null +++ b/src/ui/expui/run/testIgnored_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ui/expui/run/testPassed.svg b/src/ui/expui/run/testPassed.svg new file mode 100644 index 000000000..46fefe92b --- /dev/null +++ b/src/ui/expui/run/testPassed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/expui/run/testPassed_dark.svg b/src/ui/expui/run/testPassed_dark.svg new file mode 100644 index 000000000..4a5672667 --- /dev/null +++ b/src/ui/expui/run/testPassed_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/icons.tsx b/src/ui/icons.tsx index 882261022..c12d29d09 100644 --- a/src/ui/icons.tsx +++ b/src/ui/icons.tsx @@ -373,6 +373,12 @@ const ICONS = { success: "status/success", // Run / debug run: "run/run", + "test-passed": "run/testPassed", + "test-failed": "run/testFailed", + "test-ignored": "run/testIgnored", + "test-pending": "run/testNotRunYet", + "test-show-passed": "run/showPassed", + "test-show-ignored": "run/showIgnored", debug: "run/debug", "run-coverage": "run/runWithCoverage", profile: "run/profile",