2026-08-17 10:40:34 +02:00
|
|
|
import { defineConfig, devices } from '@playwright/test';
|
|
|
|
|
import { rmSync } from 'node:fs';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
import { dirname, join } from 'node:path';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* End-to-end smoke tests.
|
|
|
|
|
*
|
|
|
|
|
* These drive the *whole* application — the Go binary with the frontend embedded
|
|
|
|
|
* in it — rather than the dev server. That is deliberate: the two bugs that
|
|
|
|
|
* survived longest in this project were a route written outside the routes tree
|
|
|
|
|
* and a manifest promising files that were never generated. Neither is visible to
|
|
|
|
|
* a Go test, and neither is visible to a Vite build either. Only something that
|
|
|
|
|
* opens the real binary and clicks the real links finds them.
|
|
|
|
|
*
|
|
|
|
|
* `go run` rather than a prebuilt binary: the build cache makes the second run
|
|
|
|
|
* fast, and there is no separate build step to forget.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
const repo = join(here, '..');
|
|
|
|
|
const dataDir = join(here, '.playwright-data');
|
2026-10-06 23:13:45 +02:00
|
|
|
const notesDataDir = join(here, '.playwright-data-notes');
|
2026-08-17 10:40:34 +02:00
|
|
|
|
|
|
|
|
// A clean instance every run: the first test creates the first account, which only
|
|
|
|
|
// works on an empty database.
|
|
|
|
|
//
|
|
|
|
|
// Only in the main process. Playwright re-imports this file in every worker, and a
|
|
|
|
|
// wipe there deletes the database out from under the running server — which does
|
|
|
|
|
// not fail loudly. Connections already open keep working against the unlinked
|
|
|
|
|
// inode, so most requests still succeed and only the ones that need a *new*
|
|
|
|
|
// connection fail, with SQLITE_CANTOPEN. It looks exactly like a flaky auth bug.
|
|
|
|
|
if (process.env.TEST_WORKER_INDEX === undefined) {
|
|
|
|
|
rmSync(dataDir, { recursive: true, force: true });
|
2026-10-06 23:13:45 +02:00
|
|
|
rmSync(notesDataDir, { recursive: true, force: true });
|
2026-08-17 10:40:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PORT = 8097;
|
2026-10-06 23:13:45 +02:00
|
|
|
// Noter-udgaven kører som sin egen server ved siden af den fulde. Den kan ikke
|
|
|
|
|
// dele port, og den må ikke dele database: de to er to instanser, og en prøve der
|
|
|
|
|
// lod dem skrive i den samme ville måle en tilstand, ingen af dem ejer.
|
|
|
|
|
const NOTES_PORT = 8098;
|
2026-08-18 11:56:54 +02:00
|
|
|
// `localhost`, not `127.0.0.1`. WebAuthn refuses an IP address as a relying party
|
|
|
|
|
// id — `SecurityError: This is an invalid domain` — and localhost is the exception
|
|
|
|
|
// every browser makes. The passkey test cannot run against a loopback address.
|
2026-08-17 10:40:34 +02:00
|
|
|
const baseURL = `http://127.0.0.1:${PORT}`;
|
|
|
|
|
|
|
|
|
|
export default defineConfig({
|
|
|
|
|
testDir: './e2e',
|
|
|
|
|
// One worker, and not parallel: there is one server, one database and one
|
|
|
|
|
// account. Parallelism here would only test whether the tests race.
|
|
|
|
|
workers: 1,
|
|
|
|
|
fullyParallel: false,
|
|
|
|
|
forbidOnly: !!process.env.CI,
|
|
|
|
|
retries: 0,
|
|
|
|
|
reporter: process.env.CI ? [['github'], ['list']] : 'list',
|
|
|
|
|
|
|
|
|
|
use: {
|
|
|
|
|
baseURL,
|
2026-09-04 08:12:15 +02:00
|
|
|
// Off by default. The app now ships a caching service worker, and once it
|
|
|
|
|
// controls a page Playwright's page.route() no longer sees the fetches it
|
|
|
|
|
// makes — so a test that forces a 500 to check the error path gets the real
|
|
|
|
|
// 200 through the worker instead, and reads as a broken feature. The one test
|
|
|
|
|
// that needs the real worker turns it back on for its own context.
|
|
|
|
|
serviceWorkers: 'block',
|
2026-08-17 10:40:34 +02:00
|
|
|
trace: 'retain-on-failure',
|
|
|
|
|
screenshot: 'only-on-failure',
|
|
|
|
|
// Pinned, because the app reads both. The sign-up form sends
|
|
|
|
|
// `navigator.language` as the account's locale — which decides whether
|
|
|
|
|
// "i morgen" or "tomorrow" parses — and every due date is resolved in the
|
|
|
|
|
// account's zone. On a CI runner in UTC with an en-US browser, an
|
|
|
|
|
// unpinned suite tests a different application than the one people use.
|
|
|
|
|
locale: 'da-DK',
|
|
|
|
|
timezoneId: 'Europe/Copenhagen'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
projects: [
|
|
|
|
|
{ name: 'setup', testMatch: /.*\.setup\.js/ },
|
|
|
|
|
{
|
|
|
|
|
name: 'chromium',
|
2026-10-06 23:13:45 +02:00
|
|
|
// Alt undtagen noter-udgaven, som har sin egen server på sin egen port.
|
|
|
|
|
// Uden den her linje kørte den prøve også her — mod den FULDE udgave, hvor
|
|
|
|
|
// den naturligvis fejler, og hvor fejlen læser som om skakten ikke virker.
|
|
|
|
|
testIgnore: /edition\.spec\.js/,
|
2026-08-17 10:40:34 +02:00
|
|
|
use: { ...devices['Desktop Chrome'], storageState: 'e2e/.auth/user.json' },
|
|
|
|
|
dependencies: ['setup']
|
2026-08-18 09:05:02 +02:00
|
|
|
},
|
|
|
|
|
/**
|
|
|
|
|
* A short WebKit pass over the forms, and nothing else.
|
|
|
|
|
*
|
|
|
|
|
* The rest of this suite is Chromium only on purpose: a smoke test exists to
|
|
|
|
|
* catch a build that does not work, and running it in three engines finds the
|
|
|
|
|
* same bug three times. Focus is the exception, and it cost a real report to
|
|
|
|
|
* learn it. Safari ignores the `autofocus` attribute on anything inserted
|
|
|
|
|
* after the page has loaded — so every inline form in this app opened with no
|
|
|
|
|
* cursor in it, and what you typed went nowhere. It does not look like a bug;
|
|
|
|
|
* it looks like the feature does not work. "Sections have no function,
|
|
|
|
|
* because you cannot create more than one" was this.
|
|
|
|
|
*
|
|
|
|
|
* One test, not four. The engines share this server and this database, so a
|
|
|
|
|
* second pass over tests that create projects by name collides with what the
|
|
|
|
|
* first pass already made — which fails as a name that is suddenly ambiguous
|
|
|
|
|
* rather than as anything about WebKit. The sections test is the one that
|
|
|
|
|
* earns the second engine, because it is the one the focus bug broke.
|
|
|
|
|
*
|
|
|
|
|
* WebKit here is the same engine Safari is, which is the whole point.
|
|
|
|
|
*/
|
2026-10-06 23:13:45 +02:00
|
|
|
/**
|
|
|
|
|
* Noter-udgaven, som sit eget program.
|
|
|
|
|
*
|
|
|
|
|
* Egen server, egen port, egen database, og ingen `storageState`: den her
|
|
|
|
|
* instans har aldrig set den konto, den fulde udgaves opsætning lavede, og
|
|
|
|
|
* at låne dens cookie ville prøve to instanser som var de én.
|
|
|
|
|
*
|
|
|
|
|
* Den deler ikke `setup` af samme grund — den laver sin egen første konto.
|
|
|
|
|
*/
|
|
|
|
|
{
|
|
|
|
|
name: 'notes-edition',
|
|
|
|
|
testMatch: /edition\.spec\.js/,
|
|
|
|
|
use: { ...devices['Desktop Chrome'], baseURL: `http://127.0.0.1:${NOTES_PORT}` }
|
|
|
|
|
},
|
|
|
|
|
|
2026-08-18 09:05:02 +02:00
|
|
|
{
|
|
|
|
|
name: 'webkit-forms',
|
|
|
|
|
grep: /@forms/,
|
|
|
|
|
use: { ...devices['Desktop Safari'], storageState: 'e2e/.auth/user.json' },
|
|
|
|
|
dependencies: ['setup']
|
2026-08-17 10:40:34 +02:00
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
|
2026-10-06 23:13:45 +02:00
|
|
|
webServer: [
|
|
|
|
|
{
|
|
|
|
|
// The whole chain, every run.
|
2026-08-18 11:56:54 +02:00
|
|
|
//
|
2026-10-06 23:13:45 +02:00
|
|
|
// The frontend is embedded in the binary at compile time, from a copy under
|
|
|
|
|
// cmd/verdande/webbuild — so a suite that merely started the server would
|
|
|
|
|
// test whatever frontend was copied there last, which during development is
|
|
|
|
|
// nearly always the wrong one. Testing a stale build is worse than not
|
|
|
|
|
// testing: it reports green on code that is not running.
|
|
|
|
|
command: [
|
|
|
|
|
'npm run build',
|
|
|
|
|
'rm -rf ../cmd/verdande/webbuild',
|
|
|
|
|
'cp -r build ../cmd/verdande/webbuild',
|
|
|
|
|
'cd .. && go run -tags embedweb ./cmd/verdande'
|
|
|
|
|
].join(' && '),
|
|
|
|
|
cwd: here,
|
|
|
|
|
url: `${baseURL}/healthz`,
|
|
|
|
|
reuseExistingServer: false,
|
|
|
|
|
// The first run compiles the binary and builds nothing else; a cold Go
|
|
|
|
|
// build on a CI runner is comfortably under two minutes.
|
|
|
|
|
timeout: 180_000,
|
|
|
|
|
stdout: 'pipe',
|
|
|
|
|
stderr: 'pipe',
|
|
|
|
|
env: {
|
|
|
|
|
VERDANDE_ADDR: `:${PORT}`,
|
|
|
|
|
// `localhost`, while the suite itself drives `127.0.0.1`. These are two
|
|
|
|
|
// different things: the suite's address is where the browser goes, and
|
|
|
|
|
// this one is what the server calls itself — which is now also the
|
|
|
|
|
// WebAuthn relying party id, and WebAuthn refuses an IP address.
|
|
|
|
|
//
|
|
|
|
|
// The passkey test navigates to localhost explicitly so its origin
|
|
|
|
|
// matches. Everything else stays on the loopback address, where it has
|
|
|
|
|
// always been.
|
|
|
|
|
VERDANDE_BASE_URL: `http://localhost:${PORT}`,
|
|
|
|
|
VERDANDE_DATA_DIR: dataDir,
|
|
|
|
|
// Off: an update check would reach out to GitHub from a test run.
|
|
|
|
|
VERDANDE_UPDATE_CHECK: 'false'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Den samme binær en gang til, med den anden udgave slået til. Bygningen er
|
|
|
|
|
// allerede lavet af serveren ovenfor — `go run` genbruger byggecachen — så
|
|
|
|
|
// det her koster en opstart og ikke en oversættelse.
|
|
|
|
|
{
|
|
|
|
|
command: 'cd .. && go run -tags embedweb ./cmd/verdande',
|
|
|
|
|
cwd: here,
|
|
|
|
|
url: `http://127.0.0.1:${NOTES_PORT}/healthz`,
|
|
|
|
|
reuseExistingServer: false,
|
|
|
|
|
timeout: 180_000,
|
|
|
|
|
stdout: 'pipe',
|
|
|
|
|
stderr: 'pipe',
|
|
|
|
|
env: {
|
|
|
|
|
VERDANDE_ADDR: `:${NOTES_PORT}`,
|
|
|
|
|
VERDANDE_BASE_URL: `http://localhost:${NOTES_PORT}`,
|
|
|
|
|
VERDANDE_DATA_DIR: notesDataDir,
|
|
|
|
|
VERDANDE_EDITION: 'notes',
|
|
|
|
|
VERDANDE_UPDATE_CHECK: 'false'
|
|
|
|
|
}
|
2026-08-17 10:40:34 +02:00
|
|
|
}
|
2026-10-06 23:13:45 +02:00
|
|
|
]
|
2026-08-17 10:40:34 +02:00
|
|
|
});
|