test(electron): smoke test as the regression gate for the desktop shell

Phase 1 step 2 of VNCprodbuild. e2e/electron-smoke.spec.ts uses Playwright's
_electron.launch() to boot the real skeleton (dist-electron/main.js from
step 1) and asserts:
  - the login screen renders (same input[type="text"]/[type="password"]
    selectors as e2e/login.spec.ts's browser-based check)
  - zero uncaught page errors fire during load

Sets JMAP_SERVER_URL (any non-empty value) so the app reaches
lib/setup/state.ts's "env-managed" state and serves the normal login screen
instead of 302ing to the first-run /setup wizard - no live mail server or
mock JMAP build flag needed just to prove the shell renders.

playwright.electron.config.ts is deliberately separate from
playwright.config.ts: it has no `webServer` block, since this suite's app
boots its own server and would otherwise race pointlessly with `npm run dev`
starting on :3000 for the browser-based e2e/*.spec.ts suite.

Wired as `npm run test:electron`. Verified green locally (2 passed) after
`npm run build:standalone && npm run build:electron`; every later step in
the Electron rollout must keep this passing before moving on.
This commit is contained in:
Bernd Rodler
2026-08-04 12:45:42 +02:00
parent 4ff15fffaa
commit 9254a7fa20
3 changed files with 87 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import { test, expect, _electron as electron } from '@playwright/test';
import type { ElectronApplication, Page } from '@playwright/test';
import path from 'node:path';
// Regression gate for the Electron desktop shell (electron/main.ts +
// electron/preload.ts). Launches the real skeleton - the same standalone
// Next.js server artifact the Dockerfile produces, booted as a child
// process by main.ts, with a real BrowserWindow on top - and asserts the
// login screen renders with zero uncaught page errors. Every later step in
// the Electron rollout (notification bridge, realtime sync, packaging) must
// keep this green; run it before touching anything else.
//
// Requires `npm run build:standalone && npm run build:electron` to have run
// first (see package.json's `electron:dev`/`test:electron` scripts, which
// this suite assumes but does not itself trigger, matching how
// playwright.config.ts's browser suite assumes `npm run build` for its own
// prod-mode runs).
const projectRoot = path.resolve(__dirname, '..');
test.describe('Electron desktop shell', () => {
let electronApp: ElectronApplication;
let window: Page;
const pageErrors: Error[] = [];
test.beforeAll(async () => {
electronApp = await electron.launch({
args: [projectRoot],
env: {
...process.env,
// Bypass the first-run setup wizard (lib/setup/state.ts's
// "bootstrap" state, which 302s everything to /setup) without
// needing a reachable JMAP server just to prove the login screen
// renders - any non-empty JMAP_SERVER_URL is enough to reach
// "env-managed" state and serve the normal app shell.
JMAP_SERVER_URL: 'https://stalwart.sandbox.vnc.de',
SESSION_SECRET: 'electron-smoke-test-not-for-production',
NODE_ENV: 'production',
},
});
window = await electronApp.firstWindow();
window.on('pageerror', (error) => {
pageErrors.push(error);
});
await window.waitForLoadState('domcontentloaded');
});
test.afterAll(async () => {
await electronApp?.close();
});
test('boots the standalone server and renders the login screen', async () => {
// Same selectors as e2e/login.spec.ts's browser-based check - the
// shell should render the identical login form, not a different view.
await expect(window.locator('input[type="text"]')).toBeVisible({ timeout: 20000 });
await expect(window.locator('input[type="password"]')).toBeVisible();
});
test('produces zero uncaught page errors', () => {
expect(pageErrors).toEqual([]);
});
});