Trin 2. Serveren kunne allerede lade være med at montere opgaveruterne; fladen tilbød dem stadig, så en noter-instans havde links til sider, der ikke fandtes. --- hvordan fladen får det at vide --- `/auth/setup` bærer nu `edition`. Dér og ikke på `/auth/me`: udgaven er en egenskab ved instansen, ikke ved den, der er logget ind, og fladen skal kende den, før nogen er. Det er i forvejen kaldet, der spørger "hvad er det her for en instans", så den følger med uden en ekstra tur. Butikken henter formen ved siden af kontoen og tier, hvis den fejler: en 401 fra `me` må ikke tage udgaven med sig — login-skærmen er også en flade i en udgave. Afventet før `loading` slippes, så fladen aldrig tegnes, før den ved hvilket program den er; tegnet først og rettet bagefter ville vise opgaver i et sekund på en instans, der ikke har nogen. `full` som udgangspunkt, fordi det er det, programmet altid har været. --- hvad der forsvinder --- Menuens opgaveindgange er udeladt, ikke gråtonet. Forsiden er noterne. En gemt adresse til /upcoming, /kalender, /uddelegeret, /faerdige, /opgave, /filter eller /etiket fører til noterne frem for en blindgyde — der er ikke sket noget forkert, adressen hører bare til et andet program end det, der kører her. Listen står ét sted i layoutet frem for som en vagt pr. rute: listen ER beslutningen, og en vagt pr. fil er syv steder at glemme den ottende. --- et hul, prøven fandt --- /search er monteret i begge udgaver, fordi noter og notesbøger skal kunne findes — og den returnerede opgaver. Ikke teoretisk: en instans lavet om fra en fuld har opgaverne liggende i databasen endnu, så ⌘K ville være den ene dør, der stod åben ind til den halvdel, resten af programmet lige havde lukket. Lukket på serveren, hvor det hører hjemme, og ⌘K spørger også selv — en tom liste af den rigtige grund og en tom liste ved et tilfælde ser ens ud. Prøven har en kontrol: den fulde udgave SKAL finde opgaven, ellers beviser nullet i noter-udgaven ingenting. --- prøverne --- Et nyt Playwright-projekt med sin egen server, sin egen port og sin egen database. Ingen lånt session: den instans har aldrig set den konto, den fulde udgaves opsætning lavede. En skakt, der kun er prøvet ved at kalde en funktion, er prøvet på det, nogen mente — det her måler programmet, som en bruger møder det. Seks prøver, heriblandt forskellen på skjult og fraværende målt udefra: /api/v1/tasks, /labels, /upcoming og /filters skal svare 404, og /api/v1/notes skal svare 200 — ellers ville de fire bestå på en server, der bare var væk. `testIgnore` på chromium-projektet, fordi filen ellers også kørte dér, mod den fulde udgave, hvor den fejler med beskeder der læser som om skakten ikke virker. Den rigtige fejl stod i projektnavnet, ikke i påstanden. Go-suiten grøn. chromium + notes-edition: 110 bestået, 0 fejlet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
7.9 KiB
JavaScript
192 lines
7.9 KiB
JavaScript
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');
|
|
const notesDataDir = join(here, '.playwright-data-notes');
|
|
|
|
// 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 });
|
|
rmSync(notesDataDir, { recursive: true, force: true });
|
|
}
|
|
|
|
const PORT = 8097;
|
|
// 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;
|
|
// `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.
|
|
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,
|
|
// 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',
|
|
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',
|
|
// 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/,
|
|
use: { ...devices['Desktop Chrome'], storageState: 'e2e/.auth/user.json' },
|
|
dependencies: ['setup']
|
|
},
|
|
/**
|
|
* 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.
|
|
*/
|
|
/**
|
|
* 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}` }
|
|
},
|
|
|
|
{
|
|
name: 'webkit-forms',
|
|
grep: /@forms/,
|
|
use: { ...devices['Desktop Safari'], storageState: 'e2e/.auth/user.json' },
|
|
dependencies: ['setup']
|
|
}
|
|
],
|
|
|
|
webServer: [
|
|
{
|
|
// The whole chain, every run.
|
|
//
|
|
// 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'
|
|
}
|
|
}
|
|
]
|
|
});
|