playwright.config.ts
Rama main · réplica de lectura del repositorio original.
import { defineConfig, devices } from '@playwright/test'
import { STORAGE_STATE } from './e2e/paths'
const PORT = Number(process.env.E2E_PORT ?? 3210)
const COLLAB_PORT = Number(process.env.GW_COLLAB_PORT ?? 3211)
// localhost, not 127.0.0.1. Browsers treat localhost as the one insecure
// origin where WebAuthn still works, and they refuse an IP address as a
// Relying Party ID outright -- so the passkey tests cannot run against the
// loopback address, however equivalent the two look.
const baseURL = `http://localhost:${PORT}`
/**
* E2E is the narrow top of the pyramid: only flows that cannot be proven a
* level down. Schedule arithmetic, ordering and duration parsing are unit
* tested; what E2E adds is proof that the whole pipeline survives a real
* browser -- server render, CSS tokens, responsive collapse, accessibility tree.
*
* See docs/testing-conventions.md for the flow list.
*/
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
// Capped rather than "one per core". Every authenticated test now seeds its
// own workshop through the collaboration room, and a dozen rooms opening at
// once is what turned a green suite into a differently-red one on every run.
// Fewer workers is also FASTER here -- less contention, less retrying.
workers: process.env.CI ? 2 : 4,
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : [['list']],
use: {
baseURL,
/**
* Pinned, and this is load-bearing rather than tidy.
*
* The application resolves its language from Accept-Language when there is
* no session and no cookie. Playwright sends the host's locale, which in a
* CI container is en-US -- so without this line the suite would render in
* English and every German accessible name in e2e/ would stop matching at
* once, with a failure that looks like broken markup rather than a language
* switch. German is the source text; the suite asserts against it.
*
* English is covered deliberately, by e2e/locale.spec.ts, which sets its
* own locale rather than relying on the environment.
*
* `locale` and not an explicit Accept-Language header: Playwright derives
* the header from this, and a header set here would override -- silently --
* the locale that project sets for itself.
*/
locale: 'de-DE',
// Only on a retry: traces and video on every run turn a fast suite into a
// slow one and bury the interesting artefact among hundreds of boring ones.
trace: 'on-first-retry',
video: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
// What is left of the database-free half: the door. Everything about the
// agenda itself moved -- the derivations into component tests, the layout
// and interaction assertions into the authenticated projects below --
// when the public demo page that used to host them was removed.
{
name: 'desktop',
use: { ...devices['Desktop Chrome'] },
testMatch: /(entry|setup)\.spec\.ts/,
},
{ name: 'mobile', use: { ...devices['Pixel 5'] }, testMatch: /(entry|setup)\.spec\.ts/ },
/**
* The one project that is deliberately NOT German.
*
* Everything else pins de-DE so the suite keeps asserting the source text.
* This one sets its own locale and no cookie, which is the only way to
* exercise the Accept-Language leg of the resolution -- the leg a signed-out
* first-time visitor actually arrives on.
*/
{
name: 'locale',
testMatch: /locale\.spec\.ts/,
use: { ...devices['Desktop Chrome'], locale: 'en-US' },
},
// Everything past the login needs a database, so the authenticated tests
// are skipped where there is none rather than failing confusingly.
...(process.env.DATABASE_URL
? [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{
name: 'authenticated',
testMatch: /(workshop|agenda)\.spec\.ts/,
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE },
dependencies: ['setup'],
},
// The reading view is the most-used screen of this product per
// workshop -- a facilitator reads the agenda on a phone, in the room.
// It gets its own project rather than a viewport tweak inside one
// test, and it needs a signed-in session now that the agenda it reads
// is a real one.
//
// agenda.spec.ts deliberately does NOT run here. Its dragging and
// in-row editing skip themselves below 1024px because the editor does
// not mount there, and what remains -- horizontal overflow, the
// footer -- has its own assertions in reading-view.spec.ts. Running
// it anyway meant seeding a workshop through a layout that has no
// editor, which is what timed out.
// The website's pictures, taken from the running product. Its own
// project because it writes files and is not part of a test run --
// the spec skips itself unless GW_CAPTURE is set.
{
name: 'marketing',
testMatch: /marketing\.capture\.ts/,
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE },
dependencies: ['setup'],
},
{
name: 'authenticated-mobile',
testMatch: /reading-view\.spec\.ts/,
use: { ...devices['Pixel 5'], storageState: STORAGE_STATE },
dependencies: ['setup'],
},
]
: []),
],
webServer: [
{
// Builds first, then runs the standalone server -- byte for byte the
// artifact the Docker image ships. Testing against `next dev` would prove
// the wrong thing, and `next start` does not work with output: standalone
// at all (see scripts/start-standalone.mjs).
// In CI the build is its own step, so a failing build reads as a failing
// build rather than as a mysterious webServer timeout.
command: process.env.E2E_SKIP_BUILD
? `PORT=${PORT} pnpm start`
: `pnpm build && PORT=${PORT} pnpm start`,
// No proxy in the harness, so the browser is told where the
// collaboration server actually is.
env: {
// Where the app thinks it lives. Magic-link verification redirects
// against this, so getting it from the ambient environment means a
// developer's .env.local can send the browser to a port that is not
// listening -- which reads as "the login is broken".
GW_APP_URL: baseURL,
// localhost, like baseURL: the session cookie is set for the host the
// page was loaded from, and a socket to 127.0.0.1 is a different host
// as far as the browser is concerned -- it would carry no cookie, and
// the document would sit at "offline" with nothing in the log.
GW_COLLAB_URL: `ws://localhost:${COLLAB_PORT}/collab`,
// The address the app itself uses when an MCP write joins a room.
// Separate from the one above because the browser goes through a
// proxy in a real deployment and this process is inside it.
GW_COLLAB_INTERNAL_URL: `ws://localhost:${COLLAB_PORT}/collab`,
// Set here so the footer assertion has something to recognise. It also
// proves the thing a component test cannot: that the layout is rendered
// per request and not prerendered into the build, which in the Docker
// image would freeze the version at whatever the builder stage knew.
//
// Both, because they are read by different things: GW_VERSION by
// /api/health, GW_BUILD by the footer. See displayVersion in
// src/lib/version.ts.
GW_VERSION: 'e2e',
GW_BUILD: 'e2e',
},
// The root, not /api/health. Health is honest about needing a database --
// it returns 503 without one -- while "is the server listening" and "can
// this container serve the app" are different questions, and the harness
// is asking the first one. A redirect answers it just as well, which is
// what `/` is now that the public demo page is gone.
url: `${baseURL}/`,
reuseExistingServer: !process.env.CI,
timeout: 180_000,
stdout: 'ignore',
stderr: 'pipe',
},
// The collaboration server. Without it a signed-in editor has nowhere to
// write, so the authenticated tests would be asserting against a page that
// silently keeps everything in the browser.
...(process.env.DATABASE_URL
? [
{
command: 'node dist/collab-server.mjs',
url: `http://127.0.0.1:${COLLAB_PORT}/health`,
reuseExistingServer: !process.env.CI,
timeout: 60_000,
stdout: 'ignore' as const,
stderr: 'pipe' as const,
env: {
GW_COLLAB_PORT: String(COLLAB_PORT),
// The socket refuses an upgrade from a foreign Origin, and it
// works out what "foreign" means from GW_APP_URL. In the image
// this process shares an environment with the app; here it is a
// separate one, so it has to be told -- otherwise it compares
// the browser's Origin against the default localhost:3000 and
// every authenticated test sees a document stuck at "offline".
GW_APP_URL: baseURL,
// Short enough that a test can wait for a write instead of
// guessing at one.
GW_COLLAB_PERSIST_MS: '50',
GW_COLLAB_MATERIALIZE_MS: '150',
GW_COLLAB_GRACE_MS: '200',
},
},
]
: []),
],
})