Skip to content

Commit e9473e2

Browse files
committed
test: guarantee e2e teardown, bound the job, skip unused browsers
Address review feedback. Close the Electron app in a finally block in the shutdown test so the process is always terminated even if an earlier step throws. Add a job timeout-minutes so a stuck launch/close fails fast instead of tying up the runner. Set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 since only Electron is launched, never Playwright's bundled browsers, so their download during yarn install is wasted time and network flakiness. Rename the launch helper's `home` to `userDataDir` since it is a temp userData dir, not the home path. Note: AI-assisted (Claude Code). Manually verified: 3 e2e tests pass locally; tsc, eslint, prettier clean.
1 parent 70c787b commit e9473e2

3 files changed

Lines changed: 30 additions & 21 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@ jobs:
1717
e2e:
1818
name: 'Playwright (Linux)'
1919
runs-on: ubuntu-latest
20+
# Bound the job so a stuck Electron launch/close fails fast instead of
21+
# tying up a runner; this suite specifically guards shutdown hangs.
22+
timeout-minutes: 15
23+
env:
24+
# Only Electron is launched here, never Playwright's bundled browsers, so
25+
# skip their download during yarn install (faster, fewer network flakes).
26+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
2027
steps:
2128
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
2229
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
2330
with:
24-
# Node 24 (active LTS), not 26 (Current): keep the e2e runner on the
25-
# stable line for Electron's install tooling and Playwright.
31+
# Match the Node that the target Electron bundles (Electron 42 -> Node
32+
# 24); do not run ahead of it. Node 26 broke Electron's install tooling.
2633
node-version: '24.x'
2734
cache: 'yarn'
2835
# Cache the @electron/get binary download so launch does not depend on a

test/e2e/helpers.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,28 @@ import { tmpdir } from 'os';
77
// A fresh userData dir per launch keeps tests independent. Electron's native
88
// --user-data-dir flag is honored because the app reads app.getPath('userData')
99
// (see getUserDataDir in utils) and only overrides it on Snap. Returns the app
10-
// plus the temp dir so the caller can clean it up in a finally block. If launch
11-
// fails, the temp dir is removed here so repeated runs don't accumulate dirs.
10+
// plus the temp userData dir so the caller can clean it up in a finally block.
11+
// If launch fails, the temp dir is removed here so repeated runs don't
12+
// accumulate dirs.
1213
export async function launchApp(): Promise<{
1314
app: ElectronApplication;
14-
home: string;
15+
userDataDir: string;
1516
}> {
16-
const home = mkdtempSync(join(tmpdir(), 'jlab-e2e-'));
17+
const userDataDir = mkdtempSync(join(tmpdir(), 'jlab-e2e-'));
1718
try {
1819
const app = await electron.launch({
19-
args: ['.', `--user-data-dir=${home}`]
20+
args: ['.', `--user-data-dir=${userDataDir}`]
2021
});
2122
await stubAllDialogs(app);
22-
return { app, home };
23+
return { app, userDataDir };
2324
} catch (error) {
24-
cleanup(home);
25+
cleanup(userDataDir);
2526
throw error;
2627
}
2728
}
2829

29-
export function cleanup(home: string): void {
30-
rmSync(home, { recursive: true, force: true });
30+
export function cleanup(userDataDir: string): void {
31+
rmSync(userDataDir, { recursive: true, force: true });
3132
}
3233

3334
// The app opens several windows (titlebar, welcome, session, manager) and most

test/e2e/smoke.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ import { expect, test } from '@playwright/test';
22
import { cleanup, launchApp, pageByTitle } from './helpers';
33

44
test('app launches and opens at least one window', async () => {
5-
const { app, home } = await launchApp();
5+
const { app, userDataDir } = await launchApp();
66
try {
77
await app.firstWindow();
88
expect(app.windows().length).toBeGreaterThan(0);
99
} finally {
1010
await app.close();
11-
cleanup(home);
11+
cleanup(userDataDir);
1212
}
1313
});
1414

1515
test('on first run the Welcome window renders', async () => {
16-
const { app, home } = await launchApp();
16+
const { app, userDataDir } = await launchApp();
1717
try {
1818
// Selecting by title rather than firstWindow(): the app opens several
1919
// windows and only this one is the welcome view.
@@ -24,19 +24,20 @@ test('on first run the Welcome window renders', async () => {
2424
await expect(welcome.locator('#new-notebook-link')).toBeVisible();
2525
} finally {
2626
await app.close();
27-
cleanup(home);
27+
cleanup(userDataDir);
2828
}
2929
});
3030

3131
test('app shuts down cleanly without hanging', async () => {
32-
const { app, home } = await launchApp();
32+
const { app, userDataDir } = await launchApp();
3333
try {
3434
await app.firstWindow();
35-
// app.close() resolves once the process exits; a hang would fail via the
36-
// suite timeout. Assert close completes and the windows are gone.
37-
await app.close();
38-
expect(app.windows().length).toBe(0);
3935
} finally {
40-
cleanup(home);
36+
// Close in finally so the Electron process is always terminated, even if
37+
// an earlier step throws. A hang here surfaces via the job timeout.
38+
await app.close();
39+
cleanup(userDataDir);
4140
}
41+
// close() has resolved, so the process exited and the windows are gone.
42+
expect(app.windows().length).toBe(0);
4243
});

0 commit comments

Comments
 (0)