diff --git a/e2e/README.md b/e2e/README.md index b3556080e..32a913afc 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -27,10 +27,15 @@ These tests launch the full Electron app and drive it through Playwright. They c | Fixture | Description | | :--------- | :------------------------------------------------------------------ | -| `app` | Launches the Electron app and waits for both windows to be ready | +| `app` | Launches Electron, selects Testing View, and waits for the Testing View and Backend Logs windows | | `page` | The main Control Station window — waits for the app to leave loading state | | `logPage` | The Backend Logs window | +The fixture clicks **Testing View** on the startup mode selector. Builds containing +only Testing View skip the selector automatically, which the fixture also supports. +Windows are located by their loaded URL, not their creation order. Electron is +closed even if startup or a test fails. + --- ## Scripts diff --git a/e2e/fixtures/electron.ts b/e2e/fixtures/electron.ts index 22ea3d4ab..3c8a6a08a 100644 --- a/e2e/fixtures/electron.ts +++ b/e2e/fixtures/electron.ts @@ -1,5 +1,6 @@ import { _electron as electron, + expect, test as base, type ElectronApplication, type Page, @@ -10,6 +11,20 @@ import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ELECTRON_APP_PATH = path.resolve(__dirname, "../../electron-app"); +function findWindow(app: ElectronApplication, pathname: string) { + return app.windows().find((page) => { + return !page.isClosed() && new URL(page.url()).pathname.endsWith(pathname); + }); +} + +async function waitForWindow(app: ElectronApplication, pathname: string) { + // Windows can be created before they navigate, or before a fixture subscribes. + await expect + .poll(() => Boolean(findWindow(app, pathname)), { timeout: 15_000 }) + .toBe(true); + return findWindow(app, pathname)!; +} + type ElectronFixtures = { app: ElectronApplication; page: Page; @@ -27,28 +42,58 @@ export const test = base.extend({ }, }); - // Wait for both windows to open before yielding the app fixture, - // so logPage and logWindow fixtures can safely index into app.windows() - await app.firstWindow(); // Backend Logs — always first - await app.waitForEvent("window"); // Control Station — always second + try { + // A build containing only Testing View selects it automatically. Otherwise + // enter through the same mode selector button used during normal startup. + await expect + .poll( + async () => { + if (findWindow(app, "/testing-view/index.html")) return true; + + const selector = findWindow(app, "/mode-selector/index.html"); + if (!selector) return false; + try { + if ((await selector.locator("#button-grid button").count()) > 1) { + await selector + .getByRole("button", { name: "Testing View", exact: true }) + .click(); + return true; + } + } catch (error) { + // Single-view builds close the selector automatically. It can + // disappear during any awaited locator call, including click. + // Retry discovery so the Testing View window confirms startup. + if (!selector.isClosed()) throw error; + } + return false; + }, + { timeout: 15_000 }, + ) + .toBe(true); - await use(app); - await app.close(); + await waitForWindow(app, "/testing-view/index.html"); + await waitForWindow(app, "/log-viewer/index.html"); + await use(app); + } finally { + await app.close(); + } }, - // Backend logs window — always opens first + // Locate windows by their loaded view, independently of creation order. logPage: async ({ app }, use) => { - const page = app.windows()[0]; + const page = await waitForWindow(app, "/log-viewer/index.html"); await page.waitForLoadState("domcontentloaded"); await use(page); }, - // Main control station window — always opens second - // Waits for the app to reach "active" mode before yielding + // Wait for Testing View to leave its loading state before yielding. page: async ({ app }, use) => { - const page = app.windows()[1]; + const page = await waitForWindow(app, "/testing-view/index.html"); await page.waitForLoadState("domcontentloaded"); - await page.waitForSelector('[data-testid="mode-badge"]:not([data-mode="loading"])', { timeout: 15000 }); + await page.waitForSelector( + '[data-testid="mode-badge"]:not([data-mode="loading"])', + { timeout: 15000 }, + ); await use(page); }, }); diff --git a/e2e/tests/ui/startup.test.ts b/e2e/tests/ui/startup.test.ts index 4bbc087ac..030ffbfb4 100644 --- a/e2e/tests/ui/startup.test.ts +++ b/e2e/tests/ui/startup.test.ts @@ -7,3 +7,19 @@ test("backend logs window opens with correct title", async ({ logPage }) => { test("control station window opens with correct title", async ({ page }) => { await expect(page).toHaveTitle("Hyperloop Testing View"); }); + +test("selecting Testing View closes the selector and opens its windows", async ({ + app, + page, + logPage, +}) => { + await expect(page).toHaveURL(/\/testing-view\/index\.html$/); + await expect(logPage).toHaveURL(/\/log-viewer\/index\.html$/); + await expect + .poll(() => + app + .windows() + .some((window) => window.url().includes("/mode-selector/index.html")), + ) + .toBe(false); +});