Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 57 additions & 12 deletions e2e/fixtures/electron.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
_electron as electron,
expect,
test as base,
type ElectronApplication,
type Page,
Expand All @@ -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;
Expand All @@ -27,28 +42,58 @@ export const test = base.extend<ElectronFixtures>({
},
});

// 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);
},
});
Expand Down
16 changes: 16 additions & 0 deletions e2e/tests/ui/startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading