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",