diff --git a/.gitignore b/.gitignore index cb8d310d..65c1f668 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,10 @@ yarn-error.log* /dist-electron/ /dist-electron-builds/ +# playwright output +/test-results/ +/playwright-report/ + # typescript *.tsbuildinfo next-env.d.ts diff --git a/e2e/electron-smoke.spec.ts b/e2e/electron-smoke.spec.ts new file mode 100644 index 00000000..8d93cc3b --- /dev/null +++ b/e2e/electron-smoke.spec.ts @@ -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([]); + }); +}); diff --git a/playwright.electron.config.ts b/playwright.electron.config.ts new file mode 100644 index 00000000..d0e0a598 --- /dev/null +++ b/playwright.electron.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@playwright/test'; + +// Separate from playwright.config.ts on purpose: the Electron smoke suite +// launches its own app (which boots its own standalone Next.js server via +// electron/main.ts - see scripts/build-electron.mjs), so it must NOT inherit +// the main config's `webServer` (which starts `npm run dev` on :3000 for the +// browser-based e2e/*.spec.ts suite) - the two would fight over nothing but +// still waste time starting a server this suite never touches. +export default defineConfig({ + testDir: './e2e', + testMatch: 'electron-smoke.spec.ts', + timeout: 60000, + retries: 0, + use: { + trace: 'retain-on-failure', + }, + // Electron tests drive their own app windows via the `_electron` fixture, + // not a browser project - one worker keeps main-process/server startup + // logs and any zombie processes easy to reason about. + workers: 1, +});