1import { defineConfig, devices } from "@playwright/test"; 2 3/** 4 * AI Software Factory — Playwright configuration. 5 * 6 * IMPORTANT: there is intentionally NO `webServer` block. The full stack 7 * (web :3000, api :4000, worker, realtime :4001 + postgres/redis/minio/ 8 * opensearch) is started EXTERNALLY before this suite runs: 9 * 10 * local dev: pnpm docker:up && pnpm db:deploy && pnpm dev 11 * CI: see .github/workflows/ci.yml (e2e job: services + turbo dev) 12 * 13 * The suite fails fast (with a clear message) in global-setup if the API 14 * is not reachable. 15 */ 16export default defineConfig({ 17 testDir: "./specs", 18 19 // Global setup registers/logs in the demo user and writes .auth/user.json. 20 globalSetup: require.resolve("./global-setup"), 21 22 // One browser context per test (isolation); authenticated state comes from 23 // fixtures (tests/e2e/fixtures.ts), not from here. 24 fullyParallel: true, 25 forbidOnly: !!process.env.CI, 26 retries: process.env.CI ? 2 : 0, 27 // Cap at 2 in ALL environments (decision TASK-0128, C-13.5): a single 28 // spec file with `fullyParallel` and the default (≈ CPU) worker count 29 // launches 4+ browser contexts (projects × tests) against the shared, 30 // unbuilt dev stack at once. Note: the owner-test flake that first 31 // pointed at this cap was actually the ready-wait reload loop 32 // livelocking the ~3s auth bootstrap (see C-13.5 in 33 // docs/tasks/phase-13-api.md for the full corrected diagnosis); the 34 // cap is retained as a reasonable bound on concurrent contexts for the 35 // dev stack (CI parity) — it is not the root cause of that flake. 36 workers: 2, 37 38 reporter: process.env.CI 39 ? [["list"], ["html", { open: "never" }]] 40 : [["list"], ["html", { open: "on-failure" }]], 41 42 timeout: 60_000, 43 expect: { timeout: 10_000 }, 44 45 use: { 46 // The web app origin. API calls in specs go through fixtures (which 47 // target the API URL from env, default http://localhost:4000). 48 baseURL: process.env.E2E_BASE_URL ?? "http://localhost:3000", 49 actionTimeout: 15_000, 50 navigationTimeout: 30_000, 51 trace: "on-first-retry", 52 screenshot: "only-on-failure", 53 video: "retain-on-failure", 54 }, 55 56 projects: [ 57 { 58 name: "chromium-desktop", 59 use: { 60 ...devices["Desktop Chrome"], 61 viewport: { width: 1440, height: 900 }, 62 }, 63 }, 64 { 65 name: "chromium-mobile", 66 use: { 67 // MOBILE-AS-CHROMIUM — decision 2024-TASK-0128 (recorded in 68 // docs/tasks/phase-13-api.md): 69 // 70 // We test the MOBILE LAYOUT (375×812 viewport, dpr 3 — the exact 71 // iPhone 13 geometry, so the app's < 640 px single-column rules 72 // and the `sm:`/`md:` Tailwind breakpoints fire at the right 73 // widths), but with a PLAIN desktop chromium context: 74 // 75 // viewport: { width: 375, height: 812 }, deviceScaleFactor: 3 76 // 77 // We deliberately do NOT spread devices["iPhone 13"] because its 78 // `isMobile: true` makes Chromium process the page's 79 // `width=device-width` viewport meta with a separate visual 80 // viewport. Playwright 1.49's CDP actionability hit-test then 81 // intermittently desyncs on `isMobile` pages (a button's center 82 // resolves onto the backdrop/footer while the page's own 83 // `elementFromPoint` returns the button; `innerWidth` even reads 84 // ~670 on a 375px page in some runs), which hangs every 85 // locator.click()/focus() on mobile-only runs. Without isMobile 86 // the layout viewport IS the 375px viewport, the hit-test is 87 // stable, and the rendered layout is pixel-identical — the 88 // viewport meta matters for the app's CSS, and the app renders 89 // the same at 375px either way. What we lose: the iPhone user 90 // agent and hasTouch (this app does no UA sniffing and no 91 // touch-only interactions; the DoD row under test is "Mobile 92 // behavior" = the < 640 px layout, which is fully covered). 93 // 94 // (Historically this project spread devices["iPhone 13"] pinned 95 // to chromium because the profile's defaultBrowserType is 96 // webkit, whose system deps are not installed on this host or in 97 // CI — plain chromium needs none.) 98 browserName: "chromium", 99 viewport: { width: 375, height: 812 }, 100 deviceScaleFactor: 3, 101 }, 102 // Mobile is a coverage check, not the primary surface; keep the same 103 // spec set but allow a slightly higher flake budget. 104 retries: process.env.CI ? 3 : 0, 105 }, 106 ], 107 108 outputDir: "test-results", 109}); 110