import { existsSync } from "node:fs"; import { join } from "node:path"; import { defineConfig, devices } from "@playwright/test"; import { FIXTURES, HARNESS, HOST, PLAIN, ROOT } from "./servers"; /* * Refuse a client that is not there, here rather than in a global setup. * * The web servers are started *before* `globalSetup` runs, so a check there is * too late to be read: the server answers 404 for a `dist` it cannot find, * Playwright polls it for a minute and then reports a timeout against a URL, * and the setup that would have explained why never runs at all. * * Nothing here builds it either. A build racing servers already answering * would put the suite on the previous bundle — green, about code nobody * changed — so `bun run e2e` builds first and this only insists on the result. */ if (!existsSync(join(ROOT, "apps/web/dist/index.html"))) { throw new Error( "chopin: no built client to test. Run `bun run e2e`, which builds first," + " or `bun run build` before invoking Playwright directly.", ); } function server(port: number, database: string, extra: Record) { return { /* * Spawned directly rather than through `bun run start`, for the reason * `scripts/dev.ts` gives: the wrapper does not exit when the thing it * started dies, so Playwright would end up supervising nothing. */ command: "bun --preload ./e2e/github.ts" + (process.env.E2E_RESEARCH_OFFERS === "1" ? " --preload ./e2e/jev.ts" : "") + " apps/server/src/main.ts", cwd: ROOT, url: `http://${HOST}:${port}/`, env: { PORT: String(port), SERVER_HOST: HOST, AGENT: "off", CONVERSATION_PLAN: process.env.E2E_RESEARCH_OFFERS === "1" ? "on" : "off", JEV_API_KEY: "e2e-jev-only", JEV_MODEL: "jev-e2e", BACKGROUND_JOBS: "on", WEB_RESEARCH: "on", STORAGE_DRIVER: "postgres", DATABASE_URL: database, APP_ORIGIN: `http://${HOST}:${port}`, GITHUB_APP_SLUG: "chopin-e2e", GITHUB_APP_CLIENT_ID: "e2e", GITHUB_APP_CLIENT_SECRET: "e2e", GITHUB_ALLOWED_USERS: "", GITHUB_ALLOWED_ORGANIZATIONS: "githubnext", SESSION_ENCRYPTION_KEY: process.env.SESSION_ENCRYPTION_KEY!, // Named even when off, so an exported flag in somebody's shell // cannot quietly put a questionnaire in every room. DEV_QUESTIONS: "", DEV_COMMENTS: "", ...extra, }, reuseExistingServer: !process.env.CI, gracefulShutdown: { signal: "SIGTERM" as const, timeout: 2_000 }, }; } function harnessServer(port: number, database: string) { return { command: "bun --preload ./e2e/github.ts --preload ./e2e/harness/preload.ts" + " apps/server/src/main.ts", cwd: ROOT, url: `http://${HOST}:${port}/`, env: { PORT: String(port), SERVER_HOST: HOST, AGENT: "on", CONVERSATION_PLAN: "off", HARNESS: "e2e-fake", BACKGROUND_JOBS: "off", WEB_RESEARCH: "off", STORAGE_DRIVER: "postgres", DATABASE_URL: database, APP_ORIGIN: `http://${HOST}:${port}`, GITHUB_APP_SLUG: "chopin-e2e", GITHUB_APP_CLIENT_ID: "e2e", GITHUB_APP_CLIENT_SECRET: "e2e", GITHUB_ALLOWED_USERS: "", GITHUB_ALLOWED_ORGANIZATIONS: "githubnext", SESSION_ENCRYPTION_KEY: process.env.SESSION_ENCRYPTION_KEY!, DEV_QUESTIONS: "", DEV_COMMENTS: "", }, reuseExistingServer: !process.env.CI, gracefulShutdown: { signal: "SIGTERM" as const, timeout: 2_000 }, }; } export default defineConfig({ testDir: ".", testIgnore: [ join(ROOT, "e2e/design/*.e2e.ts"), join(ROOT, "e2e/diagram-gallery/*.e2e.ts"), ], /* * Bun's test runner claims `*.test.*` and `*.spec.*`. A Playwright file * under either name would be collected by `bun test` and fail there, so * these are named for the runner that can actually run them. */ testMatch: "**/*.e2e.ts", outputDir: "test-results", fullyParallel: true, // Some motion assertions poll a layer that exists only for its 180-250ms exit; // at the default worker count on a developer machine they outlast that window. workers: process.env.CI ? undefined : 4, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? [["github"], ["html", { open: "never", outputFolder: "playwright-report" }]] : [["list"]], expect: { // A websocket round trip, a 5ms edit batch and a 500ms save debounce // all sit under some of these assertions. timeout: 10_000, }, use: { trace: process.env.CI ? "on-first-retry" : "retain-on-failure", }, projects: [ { name: "chromium", testIgnore: [ join(ROOT, "e2e/design/*.e2e.ts"), // Project settings override the top-level list; keep dev-only gallery tests excluded. join(ROOT, "e2e/diagram-gallery/*.e2e.ts"), join(ROOT, "e2e/conversation-plan-heading.e2e.ts"), join(ROOT, "e2e/conversation-plan-jobs.e2e.ts"), join(ROOT, "e2e/decision-prose.e2e.ts"), join(ROOT, "e2e/conversation-plan-runtime.e2e.ts"), join(ROOT, "e2e/conversation-plan-prompts.e2e.ts"), ...(process.env.E2E_RESEARCH_OFFERS === "1" ? [] : [ join(ROOT, "e2e/research-offers.e2e.ts"), join(ROOT, "e2e/research-offers-ui.e2e.ts"), ]), join(ROOT, "e2e/decision-anchor-stress.e2e.ts"), join(ROOT, "e2e/decision-evidence.e2e.ts"), join(ROOT, "e2e/sidecar-card-states.e2e.ts"), join(ROOT, "e2e/conversation-plan-layout.e2e.ts"), join(ROOT, "e2e/conversation-plan-stress.e2e.ts"), join(ROOT, "e2e/conversation-plan-ui.e2e.ts"), join(ROOT, "e2e/document-tab-stability.e2e.ts"), join(ROOT, "e2e/discarded-mobile-geometry.e2e.ts"), join(ROOT, "e2e/sidecar-document-spacing.e2e.ts"), join(ROOT, "e2e/sidecar-evidence-absence.e2e.ts"), join(ROOT, "e2e/sidecar-draft-reopen.e2e.ts"), join(ROOT, "e2e/sidecar-option-async.e2e.ts"), join(ROOT, "e2e/sidecar-option-contracts.e2e.ts"), join(ROOT, "e2e/sidebar-decision-counts.e2e.ts"), "**/comment-motion.e2e.ts", "**/responsive*.e2e.ts", "**/sidecar.e2e.ts", "**/harness.e2e.ts", "**/auth-lifecycle.e2e.ts", ], use: { ...devices["Desktop Chrome"], baseURL: `http://${HOST}:${PLAIN}` }, }, { name: "fixtures", testMatch: [ "**/comment-motion.e2e.ts", "**/responsive*.e2e.ts", "**/sidecar.e2e.ts", "**/sidecar-draft-reopen.e2e.ts", "**/sidecar-option-async.e2e.ts", "**/sidecar-option-contracts.e2e.ts", "**/sidecar-card-states.e2e.ts", "**/sidecar-document-spacing.e2e.ts", "**/sidecar-evidence-absence.e2e.ts", "**/document-tab-stability.e2e.ts", "**/discarded-mobile-geometry.e2e.ts", "**/sidebar-decision-counts.e2e.ts", ], use: { ...devices["Desktop Chrome"], baseURL: `http://${HOST}:${FIXTURES}` }, }, { name: "harness", testMatch: ["**/harness.e2e.ts"], use: { ...devices["Desktop Chrome"], baseURL: `http://${HOST}:${HARNESS}` }, }, { name: "auth", testMatch: "**/auth-lifecycle.e2e.ts", use: { ...devices["Desktop Chrome"], baseURL: "http://127.0.0.1:8791" }, }, ], webServer: [ server(PLAIN, process.env.E2E_DATABASE_URL_0!, {}), server(FIXTURES, process.env.E2E_DATABASE_URL_1!, { DEV_QUESTIONS: "1", DEV_COMMENTS: "1", }), harnessServer(HARNESS, process.env.E2E_DATABASE_URL_2!), ], });