Skip to content

Commit 67ac573

Browse files
committed
ci: centralize e2e Electron launch & temp lock node version
1 parent a676f2c commit 67ac573

7 files changed

Lines changed: 134 additions & 150 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
- uses: actions/checkout@v6
4747
- uses: actions/setup-node@v6
4848
with:
49-
node-version: 24
49+
node-version: 24.15.0 # workaround for https://github.com/max-mapper/extract-zip/issues/154
5050
cache: npm
5151
- run: npm ci
5252
- run: npm run build

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
},
4646
"devDependencies": {
4747
"@eslint/js": "^10.0.1",
48-
"@playwright/test": "^1.53.0",
48+
"@playwright/test": "^1.60.0",
4949
"@testing-library/jest-dom": "^6.6.3",
5050
"@testing-library/react": "^16.3.0",
5151
"@types/fluent-ffmpeg": "^2.1.27",

tests/e2e/app.spec.ts

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,7 @@
1-
import { _electron as electron, expect, test } from '@playwright/test';
1+
import { expect, test } from '@playwright/test';
22
import fs from 'node:fs/promises';
3-
import os from 'node:os';
43
import path from 'node:path';
5-
import type { CameraConfig } from '../../electron/types';
6-
import type { TestFixtures } from '../../electron/testing/fixtures';
7-
8-
const electronPath = path.join(
9-
process.cwd(),
10-
'node_modules',
11-
'.bin',
12-
process.platform === 'win32' ? 'electron.cmd' : 'electron',
13-
);
14-
const projectRoot = path.resolve(process.cwd());
15-
16-
async function launchElectronApp(options: { fixtures: TestFixtures; cameras?: CameraConfig[] }) {
17-
const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vigilatus-e2e-'));
18-
const fixturePath = path.join(userDataDir, 'fixtures.json');
19-
20-
await fs.writeFile(fixturePath, JSON.stringify(options.fixtures, null, 2), 'utf8');
21-
22-
if (options.cameras) {
23-
await fs.writeFile(
24-
path.join(userDataDir, 'cameras.json'),
25-
JSON.stringify(
26-
{
27-
cameras: options.cameras,
28-
uiDisplay: {
29-
previews: true,
30-
timeline: true,
31-
previewPosition: 'right',
32-
},
33-
},
34-
null,
35-
2,
36-
),
37-
'utf8',
38-
);
39-
}
40-
41-
const isCI = Boolean(process.env.CI);
42-
const isLinux = process.platform === 'linux';
43-
44-
const app = await electron.launch({
45-
executablePath: electronPath,
46-
args: [
47-
projectRoot,
48-
...(isLinux
49-
? ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--disable-setuid-sandbox']
50-
: []),
51-
],
52-
env: {
53-
...process.env,
54-
VIGILATUS_USER_DATA_DIR: userDataDir,
55-
VIGILATUS_TEST_FIXTURES: fixturePath,
56-
VIGILATUS_OPEN_DEVTOOLS: '0',
57-
...(isCI && isLinux ? { ELECTRON_ENABLE_LOGGING: '1' } : {}),
58-
},
59-
timeout: isCI ? 30_000 : 10_000,
60-
});
61-
62-
const window = await app.firstWindow();
63-
await window.waitForLoadState('domcontentloaded');
64-
65-
return { app, window, userDataDir };
66-
}
4+
import { launchElectronApp } from './helpers/launchElectronApp';
675

686
test('launches the real app and creates a camera through the UI', async () => {
697
const { app, window, userDataDir } = await launchElectronApp({ fixtures: { streams: {}, snapshots: {} } });
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { _electron as electron } from '@playwright/test';
2+
import fs from 'node:fs/promises';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
import type { ElectronApplication, Page } from '@playwright/test';
6+
import type { CameraConfig } from '../../../electron/types';
7+
import type { TestFixtures } from '../../../electron/testing/fixtures';
8+
9+
const projectRoot = path.resolve(process.cwd());
10+
11+
export interface LaunchElectronAppOptions {
12+
fixtures: TestFixtures;
13+
cameras?: CameraConfig[];
14+
}
15+
16+
export interface LaunchedElectronApp {
17+
app: ElectronApplication;
18+
window: Page;
19+
userDataDir: string;
20+
}
21+
22+
async function readTail(filePath: string, maxBytes: number): Promise<string | null> {
23+
try {
24+
const content = await fs.readFile(filePath, 'utf8');
25+
if (content.length <= maxBytes) return content;
26+
return content.slice(-maxBytes);
27+
} catch {
28+
return null;
29+
}
30+
}
31+
32+
function formatLaunchFailure(error: unknown, userDataDir: string): string {
33+
const baseError = error instanceof Error ? error.stack || error.message : String(error);
34+
const runtimeLogPath = path.join(userDataDir, 'vigilatus.log');
35+
const bootstrapLogPath = path.join(userDataDir, 'vigilatus-bootstrap.log');
36+
const logHeader = [
37+
`Electron launch failed`,
38+
`userDataDir: ${userDataDir}`,
39+
`logPath: ${runtimeLogPath}`,
40+
`bootstrapLogPath: ${bootstrapLogPath}`,
41+
].join('\n');
42+
43+
return `${logHeader}\n\n${baseError}`;
44+
}
45+
46+
export async function launchElectronApp(options: LaunchElectronAppOptions): Promise<LaunchedElectronApp> {
47+
const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vigilatus-e2e-'));
48+
const fixturePath = path.join(userDataDir, 'fixtures.json');
49+
50+
await fs.writeFile(fixturePath, JSON.stringify(options.fixtures, null, 2), 'utf8');
51+
52+
if (options.cameras) {
53+
await fs.writeFile(
54+
path.join(userDataDir, 'cameras.json'),
55+
JSON.stringify(
56+
{
57+
cameras: options.cameras,
58+
uiDisplay: {
59+
previews: true,
60+
timeline: true,
61+
previewPosition: 'right',
62+
},
63+
},
64+
null,
65+
2,
66+
),
67+
'utf8',
68+
);
69+
}
70+
71+
const isCI = Boolean(process.env.CI);
72+
const isLinux = process.platform === 'linux';
73+
74+
const launchArgs = [projectRoot];
75+
if (isLinux) {
76+
launchArgs.push('--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage');
77+
}
78+
79+
try {
80+
const app = await electron.launch({
81+
args: launchArgs,
82+
env: {
83+
...process.env,
84+
VIGILATUS_USER_DATA_DIR: userDataDir,
85+
VIGILATUS_TEST_FIXTURES: fixturePath,
86+
VIGILATUS_OPEN_DEVTOOLS: '0',
87+
...(isCI ? { ELECTRON_ENABLE_LOGGING: '1', ELECTRON_ENABLE_STACK_DUMPING: '1' } : {}),
88+
},
89+
timeout: isCI ? 30_000 : 10_000,
90+
});
91+
92+
const proc = app.process();
93+
proc.stdout?.on('data', (d: Buffer) => {
94+
const line = d.toString().trim();
95+
if (line) console.info(`[electron] ${line}`);
96+
});
97+
proc.stderr?.on('data', (d: Buffer) => {
98+
const line = d.toString().trim();
99+
if (line) console.info(`[electron:err] ${line}`);
100+
});
101+
102+
const window = await app.firstWindow();
103+
await window.waitForLoadState('domcontentloaded');
104+
105+
return { app, window, userDataDir };
106+
} catch (error) {
107+
const launchDetails = formatLaunchFailure(error, userDataDir);
108+
const runtimeLogTail = await readTail(path.join(userDataDir, 'vigilatus.log'), 20_000);
109+
const bootstrapLogTail = await readTail(path.join(userDataDir, 'vigilatus-bootstrap.log'), 20_000);
110+
const details = [launchDetails];
111+
if (bootstrapLogTail) {
112+
details.push(`--- vigilatus-bootstrap.log tail ---\n${bootstrapLogTail}`);
113+
}
114+
if (runtimeLogTail) {
115+
details.push(`--- vigilatus.log tail ---\n${runtimeLogTail}`);
116+
}
117+
throw new Error(details.join('\n\n'));
118+
}
119+
}

tests/e2e/live-stream.spec.ts

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,18 @@
77
* Requires real cameras in the user's cameras.json config.
88
* Set CAMERA_ID env var to target a specific camera, otherwise the first HTTP camera is used.
99
*/
10-
import { _electron as electron, expect, test } from '@playwright/test';
10+
import { expect, test } from '@playwright/test';
1111

1212
test.skip(!process.env.REAL_CAMERA, 'Requires a real camera (set REAL_CAMERA=1)');
1313
import { execFileSync } from 'node:child_process';
1414
import fs from 'node:fs';
15-
import fsp from 'node:fs/promises';
1615
import os from 'node:os';
1716
import path from 'node:path';
1817
import type { CameraConfig } from '../../electron/types';
18+
import { launchElectronApp } from './helpers/launchElectronApp';
1919

2020
test.setTimeout(120_000);
2121

22-
const electronPath = path.join(
23-
process.cwd(),
24-
'node_modules',
25-
'.bin',
26-
process.platform === 'win32' ? 'electron.cmd' : 'electron',
27-
);
28-
const projectRoot = path.resolve(process.cwd());
29-
3022
function loadCamerasJson(): { cameras: CameraConfig[] } {
3123
const configDir =
3224
process.platform === 'win32'
@@ -82,38 +74,10 @@ test('live HTTP stream produces HLS segments with video and audio (real camera)'
8274

8375
console.info(`[test] using camera: ${camera.name} (${camera.id})`);
8476

85-
const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'vigilatus-e2e-live-'));
86-
await fsp.writeFile(
87-
path.join(userDataDir, 'cameras.json'),
88-
JSON.stringify({
89-
cameras: config.cameras,
90-
uiDisplay: { previews: true, timeline: true, previewPosition: 'right' },
91-
}),
92-
);
93-
94-
const app = await electron.launch({
95-
executablePath: electronPath,
96-
args: [projectRoot],
97-
env: {
98-
...process.env,
99-
VIGILATUS_USER_DATA_DIR: userDataDir,
100-
VIGILATUS_OPEN_DEVTOOLS: '0',
101-
},
102-
timeout: 20_000,
103-
});
104-
105-
const proc = app.process();
106-
proc.stdout?.on('data', (d: Buffer) => {
107-
const line = d.toString().trim();
108-
if (line) console.info(`[electron] ${line}`);
77+
const { app, window } = await launchElectronApp({
78+
fixtures: { streams: {}, snapshots: {} },
79+
cameras: config.cameras,
10980
});
110-
proc.stderr?.on('data', (d: Buffer) => {
111-
const line = d.toString().trim();
112-
if (line) console.info(`[electron:err] ${line}`);
113-
});
114-
115-
const window = await app.firstWindow();
116-
await window.waitForLoadState('domcontentloaded');
11781

11882
try {
11983
// Select the target camera

tests/e2e/recording-playback.spec.ts

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,19 @@
88
* Set CAMERA_ID env var to target a specific camera, otherwise the first camera is used.
99
* Set RECORDING_DATE env var (YYYYMMDD) to pick a date, default is today.
1010
*/
11-
import { _electron as electron, expect, test } from '@playwright/test';
11+
import { expect, test } from '@playwright/test';
1212

1313
test.skip(!process.env.REAL_CAMERA, 'Requires a real camera (set REAL_CAMERA=1)');
1414
import { execFileSync } from 'node:child_process';
1515
import fs from 'node:fs';
16-
import fsp from 'node:fs/promises';
1716
import os from 'node:os';
1817
import path from 'node:path';
1918
import type { CameraConfig } from '../../electron/types';
19+
import { launchElectronApp } from './helpers/launchElectronApp';
2020

2121
// Allow plenty of time for real camera communication
2222
test.setTimeout(300_000);
2323

24-
const electronPath = path.join(
25-
process.cwd(),
26-
'node_modules',
27-
'.bin',
28-
process.platform === 'win32' ? 'electron.cmd' : 'electron',
29-
);
30-
const projectRoot = path.resolve(process.cwd());
31-
3224
function loadCamerasJson(): { cameras: CameraConfig[] } {
3325
const configDir =
3426
process.platform === 'win32'
@@ -85,39 +77,10 @@ test('recording playback produces a valid MP4 with video (real camera)', async (
8577
const date = process.env.RECORDING_DATE ?? todayStr();
8678

8779
// Launch the real app with the user's camera config (no test fixtures)
88-
const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'vigilatus-e2e-rec-'));
89-
await fsp.writeFile(
90-
path.join(userDataDir, 'cameras.json'),
91-
JSON.stringify({
92-
cameras: config.cameras,
93-
uiDisplay: { previews: true, timeline: true, previewPosition: 'right' },
94-
}),
95-
);
96-
97-
const app = await electron.launch({
98-
executablePath: electronPath,
99-
args: [projectRoot],
100-
env: {
101-
...process.env,
102-
VIGILATUS_USER_DATA_DIR: userDataDir,
103-
VIGILATUS_OPEN_DEVTOOLS: '0',
104-
},
105-
timeout: 20_000,
106-
});
107-
108-
// Capture main process output for diagnostics
109-
const proc = app.process();
110-
proc.stdout?.on('data', (d: Buffer) => {
111-
const line = d.toString().trim();
112-
if (line) console.info(`[electron] ${line}`);
80+
const { app, window, userDataDir } = await launchElectronApp({
81+
fixtures: { streams: {}, snapshots: {} },
82+
cameras: config.cameras,
11383
});
114-
proc.stderr?.on('data', (d: Buffer) => {
115-
const line = d.toString().trim();
116-
if (line) console.info(`[electron:err] ${line}`);
117-
});
118-
119-
const window = await app.firstWindow();
120-
await window.waitForLoadState('domcontentloaded');
12184

12285
try {
12386
// Select the target camera

0 commit comments

Comments
 (0)