Compare commits

...
Author SHA1 Message Date
l0ng-ai faae2b36ac fix(run): align test runner status icons and filters 2026-11-10 14:09:09 +08:00
12 changed files with 181 additions and 12 deletions

No files matched your search

+128
View File
@@ -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();
});
}
}
+4 -3
View File
@@ -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);
}
+9 -9
View File
@@ -72,15 +72,15 @@ function totalDuration(node: TestNode): number | undefined {
export function StatusIcon({ status }: { status: TestStatus | null }) {
switch (status) {
case "passed":
return <Icon name="check" size={13} className={styles.passed} label="Passed" />;
return <Icon name="test-passed" className={styles.testStatus} label="Passed" />;
case "failed":
return <Icon name="close" size={13} className={styles.failed} label="Failed" />;
return <Icon name="test-failed" className={styles.testStatus} label="Failed" />;
case "skipped":
return <Icon name="minus" size={13} className={styles.skipped} label="Ignored" />;
return <Icon name="test-ignored" className={styles.testStatus} label="Ignored" />;
case "running":
return <Spinner size={11} />;
return <Spinner size={16} variant="spokes" className={styles.testStatus} />;
default:
return <Icon name="clock" size={13} className={styles.skipped} label="Pending" />;
return <Icon name="test-pending" className={styles.testStatus} label="Pending" />;
}
}
@@ -225,17 +225,17 @@ export function TestTreeView({
</span>
<span className={styles.spacer} />
<IconButton
icon="check"
icon="test-show-passed"
size="small"
iconSize={13}
iconSize={16}
title="Show Passed"
pressed={showPassed}
onClick={() => toggle("showPassed")}
/>
<IconButton
icon="minus"
icon="test-show-ignored"
size="small"
iconSize={13}
iconSize={16}
title="Show Ignored"
pressed={showIgnored}
onClick={() => toggle("showIgnored")}
+4
View File
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 8.25L6 11.75L13.5 4.25" stroke="#6C707E" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

+4
View File
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 8.25L6 11.75L13.5 4.25" stroke="#CED0D6" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

+4
View File
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM5.81588 4.68431C5.50346 4.37189 4.99693 4.37189 4.68451 4.68431C4.37209 4.99673 4.37209 5.50326 4.68451 5.81568L6.86882 8L4.68451 10.1843C4.37209 10.4967 4.37209 11.0033 4.68451 11.3157C4.99693 11.6281 5.50347 11.6281 5.81588 11.3157L8.0002 9.13137L10.1845 11.3157C10.4969 11.6281 11.0035 11.6281 11.3159 11.3157C11.6283 11.0033 11.6283 10.4967 11.3159 10.1843L9.13157 8L11.3159 5.81568C11.6283 5.50326 11.6283 4.99673 11.3159 4.68431C11.0035 4.37189 10.4969 4.37189 10.1845 4.68431L8.0002 6.86863L5.81588 4.68431Z" fill="#FFAF0F"/>
</svg>

After

Width:  |  Height:  |  Size: 917 B

+4
View File
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM5.81588 4.68431C5.50346 4.37189 4.99693 4.37189 4.68451 4.68431C4.37209 4.99673 4.37209 5.50326 4.68451 5.81568L6.86882 8L4.68451 10.1843C4.37209 10.4967 4.37209 11.0033 4.68451 11.3157C4.99693 11.6281 5.50347 11.6281 5.81588 11.3157L8.0002 9.13137L10.1845 11.3157C10.4969 11.6281 11.0035 11.6281 11.3159 11.3157C11.6283 11.0033 11.6283 10.4967 11.3159 10.1843L9.13157 8L11.3159 5.81568C11.6283 5.50326 11.6283 4.99673 11.3159 4.68431C11.0035 4.37189 10.4969 4.37189 10.1845 4.68431L8.0002 6.86863L5.81588 4.68431Z" fill="#F2C55C"/>
</svg>

After

Width:  |  Height:  |  Size: 917 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.25" stroke="#818594" stroke-width="1.5"/>
<path d="M3.5 12.5L12.5 3.5" stroke="#818594" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.25" stroke="#CED0D6" stroke-width="1.5"/>
<path d="M3.5 12.5L12.5 3.5" stroke="#CED0D6" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

+4
View File
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8.5L6 11.5L12.5 5" stroke="#55A76A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

+4
View File
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8.5L6 11.5L12.5 5" stroke="#57965C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

+6
View File
@@ -371,6 +371,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",