Skip to content

Commit 8651b22

Browse files
committed
tests(e2e): add some basic e2e tests for ui
1 parent 883fef5 commit 8651b22

19 files changed

Lines changed: 292 additions & 8 deletions

File tree

e2e/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
playwright-report/
3+
test-results/

e2e/fixtures/electron.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
_electron as electron,
3+
test as base,
4+
type ElectronApplication,
5+
type Page,
6+
} from "@playwright/test";
7+
import path from "path";
8+
import { fileURLToPath } from "url";
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
const ELECTRON_APP_PATH = path.resolve(__dirname, "../../electron-app");
12+
13+
type ElectronFixtures = {
14+
app: ElectronApplication;
15+
page: Page;
16+
logPage: Page;
17+
};
18+
19+
export const test = base.extend<ElectronFixtures>({
20+
app: async ({}, use) => {
21+
const app = await electron.launch({
22+
args: [path.join(ELECTRON_APP_PATH, "main.js")],
23+
cwd: ELECTRON_APP_PATH,
24+
env: {
25+
...process.env,
26+
NODE_ENV: "test",
27+
},
28+
});
29+
30+
// Suppress backend error dialogs — binary may not be available in test env
31+
await app.evaluate(({ dialog }) => {
32+
dialog.showErrorBox = () => {};
33+
});
34+
35+
// Wait for both windows to open before yielding the app fixture,
36+
// so logPage and logWindow fixtures can safely index into app.windows()
37+
await app.firstWindow(); // Backend Logs — always first
38+
await app.waitForEvent("window"); // Control Station — always second
39+
40+
await use(app);
41+
await app.close();
42+
},
43+
44+
// Backend logs window — always opens first
45+
logPage: async ({ app }, use) => {
46+
const page = app.windows()[0];
47+
await page.waitForLoadState("domcontentloaded");
48+
await use(page);
49+
},
50+
51+
// Main control station window — always opens second
52+
page: async ({ app }, use) => {
53+
const page = app.windows()[1];
54+
await page.waitForLoadState("domcontentloaded");
55+
await use(page);
56+
},
57+
});
58+
59+
export { expect } from "@playwright/test";

e2e/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "e2e",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "pnpm --filter testing-view build:e2e && pnpm --filter hyperloop-control-station build:testing",
8+
"test": "pnpm run build && playwright test",
9+
"test:ui": "pnpm run build && playwright test tests/ui",
10+
"test:integration": "pnpm --filter hyperloop-control-station build:testing && playwright test tests/integration",
11+
"test:fast": "playwright test",
12+
"test:fast:ui": "playwright test tests/ui",
13+
"report": "playwright show-report"
14+
},
15+
"devDependencies": {
16+
"@playwright/test": "^1.50.0",
17+
"electron": "^40.1.0"
18+
}
19+
}

e2e/playwright.config.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { defineConfig } from "@playwright/test";
2+
import path from "path";
3+
import { fileURLToPath } from "url";
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
7+
export default defineConfig({
8+
testDir: "./tests",
9+
timeout: 30_000,
10+
retries: 0,
11+
workers: 1, // Electron tests must run serially — only one app instance at a time
12+
13+
reporter: [["list"], ["html", { outputFolder: "playwright-report", open: "never" }]],
14+
15+
projects: [
16+
{
17+
name: "ui",
18+
testDir: "./tests/ui",
19+
use: {
20+
// UI tests run in mock mode — no backend needed
21+
// Requires testing-view to be built with: pnpm --filter testing-view build:e2e
22+
},
23+
},
24+
{
25+
name: "integration",
26+
testDir: "./tests/integration",
27+
use: {
28+
// Integration tests require the Go backend binary to be built
29+
},
30+
},
31+
],
32+
});

e2e/tests/ui/charts.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect, test } from "../../fixtures/electron";
2+
3+
/** Wait for the Zustand persist middleware to rehydrate from localStorage. */
4+
async function waitForHydration(page: import("@playwright/test").Page) {
5+
await page.waitForSelector('html[data-store-hydrated="true"]');
6+
return await page.getByTestId("chart").count();
7+
}
8+
9+
test("add chart button is visible in toolbar", async ({ page }) => {
10+
await expect(page.getByTestId("add-chart-button")).toBeVisible();
11+
});
12+
13+
test("add chart button adds a chart", async ({ page }) => {
14+
const initialCount = await waitForHydration(page);
15+
16+
await page.getByTestId("add-chart-button").click();
17+
18+
await expect(page.getByTestId("chart")).toHaveCount(initialCount + 1);
19+
});
20+
21+
test("clicking add chart multiple times adds multiple charts", async ({
22+
page,
23+
}) => {
24+
const initialCount = await waitForHydration(page);
25+
26+
await page.getByTestId("add-chart-button").click();
27+
await page.getByTestId("add-chart-button").click();
28+
await page.getByTestId("add-chart-button").click();
29+
30+
await expect(page.getByTestId("chart")).toHaveCount(initialCount + 3);
31+
});
32+
33+
test("charts are restored from localStorage after reload", async ({ page }) => {
34+
const initialCount = await waitForHydration(page);
35+
36+
// Add two charts on top of whatever is already persisted
37+
await page.getByTestId("add-chart-button").click();
38+
await page.getByTestId("add-chart-button").click();
39+
await expect(page.getByTestId("chart")).toHaveCount(initialCount + 2);
40+
41+
// Reload — Zustand should restore all charts from localStorage
42+
await page.reload();
43+
await waitForHydration(page);
44+
45+
await expect(page.getByTestId("chart")).toHaveCount(initialCount + 2);
46+
});

e2e/tests/ui/filter-dialog.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect, test } from "../../fixtures/electron";
2+
3+
test("telemetry filter dialog opens with correct title", async ({ page }) => {
4+
await page.getByTestId("filter-button-telemetry").click();
5+
6+
const dialog = page.getByTestId("filter-dialog");
7+
await expect(dialog).toBeVisible();
8+
await expect(dialog.getByRole("heading")).toHaveText(
9+
"Filter telemetry packets",
10+
);
11+
});
12+
13+
test("commands filter dialog opens with correct title", async ({ page }) => {
14+
await page.getByTestId("filter-button-commands").click();
15+
16+
const dialog = page.getByTestId("filter-dialog");
17+
await expect(dialog).toBeVisible();
18+
await expect(dialog.getByRole("heading")).toHaveText("Filter commands");
19+
});
20+
21+
test("filter dialog select all and clear all buttons work", async ({
22+
page,
23+
}) => {
24+
await page.getByTestId("filter-button-telemetry").click();
25+
26+
const dialog = page.getByTestId("filter-dialog");
27+
await expect(dialog).toBeVisible();
28+
29+
await dialog.getByTestId("filter-clear-all").click();
30+
await dialog.getByTestId("filter-select-all").click();
31+
32+
// Dialog should still be open after interactions
33+
await expect(dialog).toBeVisible();
34+
});
35+
36+
test("filter dialog closes on overlay click", async ({ page }) => {
37+
await page.getByTestId("filter-button-telemetry").click();
38+
39+
const dialog = page.getByTestId("filter-dialog");
40+
await expect(dialog).toBeVisible();
41+
42+
// Press Escape to close
43+
await page.keyboard.press("Escape");
44+
await expect(dialog).not.toBeVisible();
45+
});

e2e/tests/ui/mode-badge.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect, test } from "../../fixtures/electron";
2+
3+
const VALID_MODES = ["mock", "active", "mock-active", "loading", "error"];
4+
5+
test("mode badge is visible with a valid mode", async ({ page }) => {
6+
const badge = page.getByTestId("mode-badge");
7+
await expect(badge).toBeVisible();
8+
9+
const mode = await badge.getAttribute("data-mode");
10+
expect(VALID_MODES).toContain(mode);
11+
});
12+
13+
test("mode badge shows active when backend is connected", async ({ page }) => {
14+
const badge = page.getByTestId("mode-badge");
15+
await expect(badge).toHaveAttribute("data-mode", "active");
16+
});

e2e/tests/ui/startup.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expect, test } from "../../fixtures/electron";
2+
3+
test("backend logs window opens with correct title", async ({ logPage }) => {
4+
await expect(logPage).toHaveTitle("Backend Logs");
5+
});
6+
7+
test("control station window opens with correct title", async ({ page }) => {
8+
await expect(page).toHaveTitle("Hyperloop Testing View");
9+
});

frontend/testing-view/.env.e2e

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_FORCE_DEV=true

frontend/testing-view/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Testing View</title>
7+
<title>Hyperloop Testing View</title>
88
</head>
99
<body>
1010
<div id="root"></div>

0 commit comments

Comments
 (0)