diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..52c8ca8 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,62 @@ +name: e2e + +# Runs the Playwright-Electron setup-wizard suite against a real build of the app. +# Electron is a GUI process, so it needs an X server - we give it a virtual one +# via xvfb-run and launch with --no-sandbox/--disable-gpu (set in e2e/fixtures.ts). +# EDISON_DRY_RUN=1 (also in fixtures.ts) short-circuits the stdiod/detectord +# daemons so the app boots straight to the wizard with no native subprocesses. + +on: + workflow_dispatch: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + setup-wizard: + name: Setup wizard (Playwright-Electron) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci + + # System libraries the Electron/Chromium runtime needs (libnss3, libgbm, + # libasound, ...). Playwright ships the exact set for its Chromium, which is + # a superset of what Electron needs, so reuse it instead of a manual apt list. + - name: Install headless runtime deps + run: npx playwright install-deps chromium + + - name: Build the app (main + preload + renderer) + run: npm run build + + # xvfb-run -a provides the virtual X server on an auto-selected display. + - name: Run e2e under Xvfb + run: xvfb-run -a npm run test:e2e + + # On failure, upload the HTML report + per-test traces/screenshots so the + # failure is debuggable beyond the raw log (playwright.config.ts retains + # trace + screenshot on failure). + - name: Upload Playwright artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 7 + if-no-files-found: ignore diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts index d320818..33c3051 100644 --- a/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -6,7 +6,10 @@ import { join } from "path"; * the ElectronApplication and first window Page. * * Expects the app to be built first via `npm run build` (electron-vite build). - * In CI, set EDISON_TEST_MODE=1 to skip real auth and backend calls. + * EDISON_DRY_RUN=1 short-circuits stdiod/detectord subprocess spawning so the + * app boots to the wizard without native daemons (see src/main/stdiod/controller.ts, + * src/main/detectord/controller.ts). --no-sandbox/--disable-gpu let Electron run + * headless under Xvfb (e.g. CI, or as root in a sandbox). */ export const test = base.extend<{ electronApp: ElectronApplication; @@ -17,11 +20,11 @@ export const test = base.extend<{ const mainPath = join(__dirname, "../out/main/index.js"); const app = await electron.launch({ - args: [mainPath], + args: [mainPath, "--no-sandbox", "--disable-gpu"], env: { ...process.env, NODE_ENV: "test", - EDISON_TEST_MODE: "1", + EDISON_DRY_RUN: "1", }, }); diff --git a/e2e/setup-wizard.spec.ts b/e2e/setup-wizard.spec.ts index 1b517d2..3039367 100644 --- a/e2e/setup-wizard.spec.ts +++ b/e2e/setup-wizard.spec.ts @@ -5,17 +5,17 @@ test.describe("Setup Wizard", () => { // The wizard should show the welcome step with Edison branding await expect(firstWindow.locator("text=Edison Watch")).toBeVisible({ timeout: 10000 }); - // Step indicator should show step 1 as active - await expect(firstWindow.locator("text=Sign In")).toBeVisible(); - - // Sign in form should be present - await expect(firstWindow.locator('input[type="email"], input[placeholder*="email" i]')).toBeVisible(); + // Step indicator should show step 1 ("Welcome") as active (see StepIndicator.tsx) + await expect(firstWindow.locator("text=Welcome")).toBeVisible(); }); test("Step 1 shows sign-in options", async ({ firstWindow }) => { // Wait for the welcome step to render await expect(firstWindow.locator("text=Edison Watch")).toBeVisible({ timeout: 10000 }); + // The email form is behind the provider-select view - open it first (WelcomeStep.tsx) + await firstWindow.getByText("Continue with Email", { exact: false }).click(); + // Should show email input const emailInput = firstWindow.locator('input[type="email"], input[placeholder*="email" i]'); await expect(emailInput).toBeVisible(); diff --git a/playwright.config.ts b/playwright.config.ts index 4a0ffe1..a5a2caa 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,7 +5,12 @@ export default defineConfig({ timeout: 30000, retries: 0, workers: 1, // Electron tests must run serially + // HTML report + list output; the report is uploaded as a CI artifact on failure. + reporter: [["list"], ["html", { open: "never" }]], use: { - trace: "on-first-retry", + // retries: 0, so "on-first-retry" would never capture anything - retain on + // failure instead so CI has a trace + screenshot to inspect (see e2e.yml). + trace: "retain-on-failure", + screenshot: "only-on-failure", }, });