import { defineConfig, devices } from '@playwright/test'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; // __dirname under ESM. const __dirname = dirname(fileURLToPath(import.meta.url)); // Repo root is one level up from ui/. const repoRoot = resolve(__dirname, '..'); // Separate port from the auth-OFF harness (8788) so both configs can coexist. const PORT = Number(process.env.E2E_AUTH_PORT ?? 8790); const BASE_URL = `http://127.0.0.1:${PORT}`; // Isolated throwaway data + workspace dirs — never touches real ./data/maestro.db. // // DETERMINISTIC path (NOT mkdtemp): Playwright imports this config in the main // runner process AND re-imports it in each spec worker process. A random // mkdtemp would yield a DIFFERENT DB_PATH per import — the webServer (started // from the main-process value) and the spec's seeding code would then open // DIFFERENT databases, so seeded members would never appear. A fixed path makes // every import agree on one DB. The spec computes the SAME path independently // (see DB_PATH in the spec) and seeds into it. We do NOT delete it at import // time (a worker import would nuke the live DB); the server rebuilds its schema // on boot and seeding is idempotent, so a stale file from a prior run is benign. const e2eTmp = join(tmpdir(), 'maestro-e2e-auth'); mkdirSync(e2eTmp, { recursive: true }); const DB_PATH = join(e2eTmp, 'e2e-auth.db'); const WORKTREE_DIR = join(e2eTmp, 'workspaces'); // Absolute path to the local-auth config YAML (enables POST /auth/local login). const AAO_CONFIG = resolve(repoRoot, 'ui/e2e-auth/config.e2e-auth.yaml'); export default defineConfig({ testDir: resolve(__dirname, 'e2e-auth'), testMatch: '**/*.spec.ts', // Build+boot can be slow; roomy per-test budget. timeout: 60_000, expect: { timeout: 15_000 }, fullyParallel: false, workers: 1, retries: 0, reporter: [['list']], use: { baseURL: BASE_URL, headless: true, // NOTE: the wide viewport that keeps the desktop nav (data-testid="nav-*") // out of the hamburger drawer is set on the chromium project below, AFTER // the Desktop Chrome device spread (which would otherwise pin 1280×720). // Under LOCAL AUTH the TopBar carries extra right-side chrome (user chip, // change-password, logout) that collapses it to compact mode at 1280 — and // the drawer nav exposes no nav-* testids. // This sandbox disables the Chromium sandbox (bwrap/userns off). Launch // with --no-sandbox or every browser launch fails to spawn. launchOptions: { args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], }, trace: 'retain-on-failure', }, projects: [ { name: 'chromium', // Device spread sets 1280×720; override AFTER it so the wide viewport wins. use: { ...devices['Desktop Chrome'], viewport: { width: 1600, height: 900 } }, }, ], // Boot the real orchestrator (worker mode) with LOCAL AUTH ON (AAO_CONFIG // points at the local-auth YAML). The bootstrap admin is seeded at startup; // member users are seeded by the spec's beforeAll into the same temp DB. // dist/ must exist (npm run build:all). webServer: { command: 'node dist/main.js', cwd: repoRoot, url: `${BASE_URL}/ui`, reuseExistingServer: false, timeout: 120_000, stdout: 'pipe', stderr: 'pipe', env: { ...process.env, PORT: String(PORT), DB_PATH, WORKTREE_DIR, AAO_CONFIG, AAO_MODE: 'worker', LOG_LEVEL: 'warn', MCP_ENCRYPTION_KEY: '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', }, }, });