mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
The v0.1.13 tag went red here, and none of it was the release. WALL CLOCKS Snooze times are computed in the BROWSER, deliberately — the server is never told the session's zone — but the row is rendered server-side in the user's zone, which falls back to Europe/Berlin. The spec then read the hour back with Node's getHours(), which answers in the RUNNER's zone. All three agree on a developer's machine, which is why this passed eighteen consecutive local runs. On a GitHub runner the browser and Node are UTC while the server is still Berlin: the mail came back at 18:00, the row said 20:00, and the assertion reported the wrong TIME being shown when the truth was the wrong CLOCK being read. APP_TIMEZONE now names the zone rendered times are in, in one documented place; the assertion converts into it rather than trusting the process. And playwright.config pins the browser to it, so a menu that offers six in the evening cannot produce a row that says eight — the divergence is an artefact of an unpinned runner and no real user has it, since their browser and their configured zone are the same place. Reproduced before fixing: reverting the conversion under TZ=UTC fails with the CI failure exactly. TWO FIXTURES LEFT TO CHANCE, both found by running under TZ=UTC at CI's two workers. calendar.spec seeds calendars and never seeded a mailbox, but its pane cases assert on the mail list beside the calendar. Those rows were whatever the last spec on the worker left: bulk-select-all archives two hundred messages and then clears them, after which `[data-row-layout]` matches nothing and the failure reads as a broken container query when there is simply no row to lay out. admin-panels assumed the Processes panel starts open. Collapse is stored per admin user, the admin is shared rather than per worker, and a run that fails anywhere after the first toggle leaves it collapsed — so the next run failed on its opening assertion, reporting a broken feature and really reporting its own earlier failure. What collapsed it this time is not established; the test establishes its own precondition now, which is the part that was missing either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
7.6 KiB
TypeScript
151 lines
7.6 KiB
TypeScript
import { defineConfig, devices } from "@playwright/test";
|
|
import { APP_TIMEZONE, BASE_URL } from "./tests/e2e/support/config";
|
|
|
|
/**
|
|
* plMail end-to-end configuration.
|
|
*
|
|
* The suite runs against the compose.test.yaml stack — the app's own FrankenPHP
|
|
* image, which is what production runs and what CI runs:
|
|
*
|
|
* npm run test:e2e:docker — brings that stack up, then runs the suite
|
|
* npm run test:e2e — runs the suite against a stack already up
|
|
*
|
|
* There is no second serving path any more. This config used to be able to
|
|
* start a `php -S` dev server for CI, through a router script in
|
|
* tests/e2e/support/, and every accommodation that server needed — a router to
|
|
* stop index.php answering asset requests, a copy of the environment into
|
|
* $_SERVER because the cli-server SAPI does not provide one, a php.ini the
|
|
* runner had to be given by hand, and finally a restart loop because the thing
|
|
* segfaulted mid-suite — was an accommodation for a server no user will ever
|
|
* meet. CI now boots the same stack this file points at, so the suite proves
|
|
* something about the app as shipped. See .github/workflows/e2e.yml.
|
|
*
|
|
* Every worker owns a dedicated user and signs in once for itself, through a
|
|
* worker-scoped fixture in tests/e2e/support/test.ts. That is why specs import
|
|
* `test` from there rather than from @playwright/test, and why there is no
|
|
* `setup` project any more — one shared storage-state file cannot serve
|
|
* workers that are logged in as different people.
|
|
*/
|
|
|
|
// How to reach the stack, resolved once, here.
|
|
//
|
|
// Some specs have to reach past the browser: seeding fixtures through the
|
|
// console, and — in mercure.spec.ts — stopping the hub to prove the stream
|
|
// comes back. Both go through these, so pointing the suite at a different
|
|
// stack is one variable rather than a hunt through the specs.
|
|
//
|
|
// The `-p` case is why this is shared rather than repeated. compose.test.yaml
|
|
// pins `name: pl_mail_test`, so a hardcoded `docker compose -f
|
|
// compose.test.yaml` is right exactly until somebody runs a second stack under
|
|
// another project name — a worktree, a second port — at which point it talks
|
|
// to the wrong project or to none at all. `stop` against a project that does
|
|
// not exist exits 0, so mercure.spec.ts spent that case silently not stopping
|
|
// the hub and then failing because the stream never dropped, and
|
|
// admin-queue.spec.ts seeded a database the browser was not looking at.
|
|
process.env.E2E_COMPOSE ??= "docker compose -f compose.test.yaml";
|
|
process.env.E2E_CONSOLE ??= `${process.env.E2E_COMPOSE} exec -T app php bin/console`;
|
|
|
|
// Storage state is per worker now and lives in tests/e2e/support/test.ts —
|
|
// there is no single path for the config to name.
|
|
|
|
export default defineConfig({
|
|
testDir: "./tests/e2e",
|
|
// Parallel across FILES, serial within one. Deliberately not fullyParallel:
|
|
// several specs depend on the order of tests inside their own file — the
|
|
// integration describes build on each other, twofactor enrols before it
|
|
// signs in — and file granularity preserves that for free, without any
|
|
// spec having to declare serial mode. It also means one file sees one
|
|
// worker, so the per-worker user (tests/e2e/support/config.ts) is stable
|
|
// for the whole file.
|
|
fullyParallel: false,
|
|
forbidOnly: !!process.env.CI,
|
|
retries: process.env.CI ? 1 : 0,
|
|
// Was 1 everywhere the specs owned the database, because they all shared one
|
|
// user and `seed-mail` wipes the mailbox it seeds. Each worker now owns a
|
|
// separate user, so they no longer collide. CI gets 2 (ubuntu-latest is 2
|
|
// vCPU on private repos); locally 4, above which the single app container
|
|
// rather than the runner becomes the limit.
|
|
workers: process.env.CI ? 2 : 4,
|
|
reporter: process.env.CI
|
|
? [["github"], ["html", { open: "never" }]]
|
|
: [["list"], ["html", { open: "never" }]],
|
|
|
|
// A locator that will never match should fail in seconds, not stall for the
|
|
// 30s test budget. Costs a green run nothing; makes a red one far quicker to
|
|
// read.
|
|
expect: { timeout: 5_000 },
|
|
|
|
use: {
|
|
baseURL: BASE_URL,
|
|
// The browser keeps the same clock the server renders in.
|
|
//
|
|
// Times reach a page two ways: formatted server-side in the user's zone,
|
|
// which falls back to Europe/Berlin, and computed in the browser — the
|
|
// snooze and schedule menus work out "later today" and "tomorrow morning"
|
|
// locally, on purpose, because the server is never told the session's zone.
|
|
// With the browser on UTC those two disagree by the offset: the menu offers
|
|
// six in the evening, the row that comes back says eight.
|
|
//
|
|
// Unpinned this is the runner's zone, so it is Europe/Berlin on a
|
|
// developer's machine and UTC on a GitHub runner — which is the difference
|
|
// between a suite that passes locally and a tag that fails, two hours out,
|
|
// for reasons that look like flakiness. Pinning it also makes the suite
|
|
// describe the ordinary case, where a person's browser and their configured
|
|
// zone are the same place.
|
|
timezoneId: APP_TIMEZONE,
|
|
trace: "on-first-retry",
|
|
screenshot: "only-on-failure",
|
|
// Off, not "retain-on-failure": that still screencasts every test and then
|
|
// throws the recording away on the ~97 that pass. The trace above is the
|
|
// more useful artifact and costs nothing unless a test is retried. For a
|
|
// local failure, re-run the one spec with `--video on`.
|
|
video: "off",
|
|
},
|
|
|
|
projects: [
|
|
{
|
|
name: "chromium",
|
|
use: { ...devices["Desktop Chrome"] },
|
|
// integrations.spec.ts and mercure.spec.ts are handled separately below.
|
|
testIgnore: /(integrations|mercure)\.spec\.ts/,
|
|
// No `storageState` here, and no `setup` project: signing in is now a
|
|
// worker-scoped fixture in tests/e2e/support/test.ts, because the path
|
|
// has to differ per worker and project config is static.
|
|
},
|
|
{
|
|
// The specs per-worker users cannot isolate.
|
|
//
|
|
// integrations: IntegrationProviderConfig and MailProviderConfig are both
|
|
// unique on `provider` with no user column — genuinely install-wide
|
|
// state. Two workers touching it would race, and it reaches further than
|
|
// it looks: whether the onboarding wizard offers its integrations step
|
|
// depends on what an admin has configured globally, so this spec can
|
|
// change what onboarding.spec.ts sees.
|
|
//
|
|
// mercure: it stops and restarts the hub container to prove the stream
|
|
// recovers on its own. There is one hub for the whole stack, so running
|
|
// that alongside anything else takes live updates away from specs that
|
|
// did not ask for it — which is exactly how it first showed up, as an
|
|
// integrations failure that passed on its own.
|
|
//
|
|
// One worker, and `dependencies` so it starts only once everything else
|
|
// has finished.
|
|
name: "chromium-exclusive",
|
|
use: { ...devices["Desktop Chrome"] },
|
|
testMatch: /(integrations|mercure)\.spec\.ts/,
|
|
dependencies: ["chromium"],
|
|
},
|
|
],
|
|
|
|
// No `webServer`. The app is the compose stack, and this config never starts
|
|
// it — `npm run test:e2e:docker` does, and CI does, with the same command.
|
|
//
|
|
// Deliberate, and the reason the old one is worth remembering: a server this
|
|
// config started was a server nothing else in the project used, so it needed
|
|
// its own front controller, its own copy of the environment, its own php.ini
|
|
// and eventually its own crash-restart loop, and a green suite said nothing
|
|
// about the FrankenPHP the app actually ships on. Booting the stack outside
|
|
// Playwright also means one running app for the whole suite rather than a
|
|
// second one racing the first onto the same fixtures.
|
|
});
|