E2E & live smoke¶
Three Playwright suites and four gated shell scripts, plus a wrapper that
runs two of them as a signed-in person. None of them runs as part of
npm test, and one of the three suites runs in CI.
| Suite | Where | Specs | Tests | In CI? | Last measured |
|---|---|---|---|---|---|
| Frontend Playwright | packages/frontend/e2e/ |
15 | 47 | No | 9 Oct β 42 passed, 5 skipped, 2.6m |
| Public-site Playwright | packages/public-site/e2e/ |
1 | 6 | No | 9 Oct β 6/6 serially, 6.4s; 2 timeouts in the parallel run |
| PA-demo Playwright | packages/pa-demo/e2e/ |
1 | 11 | Yes β azure-pa-demo-acc.yml |
9 Oct β 11 passed, 8.7s |
| Live smoke scripts | scripts/*.sh |
4 scripts and a wrapper | β | No | never run for these pages |
64 end-to-end tests, all three suites green β the frontend with five
tests skipping themselves by design, and the public site green serially.
Measured on 9 October 2026 for v2026.10.1 (main at ebec288), from the
working checkout on acc at 0ea4985, a tree identical to ebec288,
against the full local stack the developer already had running; nothing was
started or stopped for the run.
Re-run on 9 October β four new specs, nineteen more tests
v2026.10.1 added four frontend specs and 19 tests, and the runner collected 47 tests across fifteen specs β 42 passed and five skipped, each skip one the spec declares:
landing-single-board(13 tests, 9 passed, 4 skipped) β the organisation landing pages at/amsterdamand/heusden: one exact "Inloggen", the caseworker login in the tenant's colours, the DigiD link pre-filling the tenant's test citizen, an old/?tenant=link redirected, and an unknown or Flevoland path sent to the grid at/. Its four link preview tests skip on a local target β the per-tenant pages exist only in a deployed build, and the dev server serves the rootindex.htmlfor every path.citizen-services(3, passed) β the cards each test citizen sees: Amsterdam exactly Zorgtoeslag, Heusden Zorgtoeslag and Heusdenpas, Flevoland Zorgtoeslag, Vergunningen and Subsidies β and no Mijn toestemming button.landing-access-denied(2, passed) β a board the role does not open ends in the no-access dialog on the landing page, and the boards with an Entra role offer the Flevoland account.heusdenpas-journey(1, passed, 11.8s) β Gemeente Heusden's Heusdenpas end to end:test-citizen-heusdenapplies, andtest-caseworker-heusdenchecks completeness, reviews the outcome of the untenanted SVB, SZW and Heusdenpas decisions, and informs the applicant, who then sees the decided application once in Mijn aanvragen.rip-r21-journeyskipped itself again, as on 3 October: the local stack signs with the real ValidSign (VALIDSIGN_STUB_MODE=false).
One existing spec changed: caseworker-journey.spec.ts now fills the
Kapvergunning forms by their Dutch labels β Stamdiameter (cm),
Aanvraag indienen, Beoordeling indienen, Bekendmaking bevestigen β
because linked-data-explorer's fixtures translated them
(linked-data-explorer#254 item 1); it passed. The
required-processes helper behind globalSetup now requires nine
process definitions, the two Heusdenpas ones under tenant heusden, and
three more decisions β see
Deploying the E2E fixtures.
The public-site suite went red once and green serially again, as on
3 October: four passed and the same two search-journey tests timed out in
its default parallel run, and it passed 6/6 in 6.4s with
--workers=1; see
Public-site Playwright suite. pa-demo
passed 11/11 in 8.7s.
The inventory is seventeen specs: fifteen in
packages/frontend/e2e/, plus packages/pa-demo/e2e/plato-demo.spec.ts
and packages/public-site/e2e/publiek.spec.ts. No playwright.config.ts
changed, and the workflow wiring is unchanged: the pa-demo spec runs in
azure-pa-demo-acc.yml and only there, and no workflow runs the other
two.
The fifteen: caseworker-journey, citizen-services,
heusdenpas-journey, infra-board-journey, landing-access-denied,
landing-single-board, login-redirect, pa-live-authoring,
pa-mock-journey, protected-route, rip-r21-journey, smoke,
tenant-isolation, thuisbatterij-journey, zorgtoeslag-journey.
History: re-run on 3 October β the frontend count is a total again
Every pass from 12 to 30 September left the Playwright suites alone, and
the 27-test frontend figure they repeated was measured when there were ten
specs, so it could not include thuisbatterij-journey.spec.ts. On
3 October the runner collected 28 tests across the eleven specs β
27 passed and one skipped:
thuisbatterij-journeyran for the first time on these pages: 1 test, passed, 8.4s.rip-r21-journeyskipped itself, by one of its two runtime guards: the local stack signs with the real ValidSign (VALIDSIGN_STUB_MODE=false), and the journey will not request a binding signature, so it refuses before sending a package and creates nothing. See below.
The public-site suite went red once and green serially, and is recorded
that way. Run with its config's default workers β six on that machine β
it returned 4 passed, 2 failed: both search-journey tests
(publiek.spec.ts:6 and :27) timed out after 10s waiting for the
search-result filter checkbox to appear. Re-run with --workers=1 against
the same running backend, it passed 6/6 in 7.8s. 6/6 serially; 2
timeouts in the parallel run β not a defect until it fails on its own;
see Public-site Playwright suite.
The inventory was thirteen specs then, unchanged since v2026.09.11:
eleven in packages/frontend/e2e/, plus
packages/pa-demo/e2e/plato-demo.spec.ts and
packages/public-site/e2e/publiek.spec.ts. v2026.10.0 changed no spec, no
playwright.config.ts and no helper:
git diff ae06c9e 0625d48 -- 'packages/*/e2e' is empty.
History: v2026.09.14 changed three specs and the login helper, none in
its test count β read from the source at ae06c9e:
e2e/helpers/auth.tsmatches the medewerker login button exactly (9f54e82). v2026.09.13 added "Inloggen met uw Flevoland-account" to the landing page, andloginAsMedewerkerlooked its button up by the name "Inloggen", which Playwright matches as a substring β so it resolved to both buttons and strict mode refused to click. The commit records that 26 of 28 E2E tests against ACC failed at that one line; that run is the commit's account, not a measurement these pages made. The helper now passesexact: true, because the plain "Inloggen" in the top bar is the Keycloak login it fills in, while the new button goes to Entra ID.caseworker-journey.spec.tsaccepts the Dutch Kapvergunning task names (10e83ce), andzorgtoeslag-journey.spec.tsandtenant-isolation.spec.tsthe Dutch Zorgtoeslag ones (1f0e52b) β the same patternthuisbatterij-journeyadopted in v2026.09.12; see below.
The three playwright.config.ts files are unchanged between 86af73e and
ebec288, and e2e/global-setup.ts changed only in v2026.09.13, when its
fix messages began naming npm run e2e:deploy-fixtures; what it requires
grew in v2026.10.1, through helpers/required-processes.ts β see
What each suite needs running.
What each suite needs running¶
The webServer block is declared per config, not per spec, so it applies to
every spec under that directory. This is the table to check before running
anything locally β it is what separates a suite you can start cold from one that
will fail its preconditions.
Re-read at 86af73e on 24 September 2026; the configs are unchanged at
2443adc (v2026.09.12), 963fe24 (v2026.09.13), ae06c9e (v2026.09.15),
0625d48 (v2026.10.0) and ebec288 (v2026.10.1).
| Config | Declares webServer? |
Has a globalSetup? |
What must already be up |
|---|---|---|---|
packages/frontend/e2e/playwright.config.ts |
No β none at all | Yes β ./global-setup.ts, plus a globalTeardown |
Four services: the frontend on :5173, the backend on :3002, Keycloak on :8080, and the sibling Linked Data Explorer backend on :3001 β plus docker compose up -d behind them, and a deployed process/decision bundle |
packages/pa-demo/e2e/playwright.config.ts |
Yes, conditionally β npm run dev on :5176 |
No | Nothing. No backend, database or Keycloak; plato issues no network requests at all |
packages/public-site/e2e/playwright.config.ts |
Yes, conditionally β npm run dev on :5175 |
No | The backend, on whatever VITE_API_URL points at. The config starts the site but not the API, and these specs hit real search results |
Four things follow from that table:
- The frontend suite is the one that needs a human, and its
globalSetupnow checks considerably more than three URLs. In order, it refuses to run against production withoutCONFIRM_PROD=1; launches and closes a Chromium to prove the browser binary is actually on the machine; probes the frontend, backend/v1/health, Keycloak and β locally, or wheneverLDE_URLis set β the LDE backend/v1/health; then callsverifyRequiredProcesses()and, separately,verifyRequiredDecisions()against the engine. Each failure throws naming what is missing and how to fix it, rather than surfacing later as a confusing connection error. It starts nothing itself, which is exactly why it is not in CI. - The decision check is a separate gate for a reason. Decisions deploy without an Organization so a tenant-scoped process can reach them, so a missing or tenant-pinned DMN does not show up as a missing process β it surfaces mid-journey as a 500 on process start, or on a citizen's screen as "probeer het opnieuw", neither of which mentions a decision.
- "Starts its own server" is not the same as "self-contained." public-site
declares a
webServerand still needs the backend; pa-demo declares one and needs nothing. Only pa-demo is genuinely cold-startable, which is why it is the suite that runs in CI. - Both conditional configs skip the server entirely when
E2E_BASE_URLis set, pointingbaseURLat a deployed site instead β the post-deploy verification path against ACC.reuseExistingServeris on outside CI, so a dev server you already have running is attached to rather than duplicated. The frontend config has its own equivalent ine2e/helpers/target.ts, which readsFRONTEND_URL,BACKEND_URL,KEYCLOAK_URL,LDE_URLandOPERATON_URLand falls back to localhost for each.
workers: 1 on the frontend suite is deliberate, not a leftover
The config pins a single worker and says why: every Operaton-touching spec
shares the same stateful local engine, and two files creating
identically-named tasks for the same caseworker β
tenant-isolation.spec.ts and zorgtoeslag-journey.spec.ts, both "Case
review: provisional entitlement decision" β raced when run in different
workers. A .first() task-list match grabbed the other file's task
mid-flight, producing a real Operaton save conflict ("Opslaan mislukt"),
not merely a bad selector. One worker serializes everything, trading suite
speed for correctness.
This is the one place in the repository where serial execution is the right answer, and it is worth contrasting with the unit suites, where it is not β see Overview. The difference is that here the shared state is real and external; there it is the machine's own CPU.
Count these with the runner, never with grep
A static count of test( calls across the eleven frontend specs gives
24 at 86af73e, and still 24 at 2443adc, ae06c9e and 0625d48;
across the fifteen at ebec288 it gives 34. The runner reported
27 across ten of them on 30 August, 28 across eleven on 3 October,
and 47 across fifteen on 9 October. login-redirect.spec.ts alone
declares one test( and runs five, because the cases are parameterised;
landing-single-board.spec.ts declares six and runs thirteen, from loops
over two tenants, two paths and two redirect targets; and citizen-services.spec.ts
declares one and runs three. rip-r21-journey.spec.ts contains two
test.skip(true, reason) calls inside test bodies, runtime skips that a
naive grep reads as skipped declarations, and one of which fired on 3 and
9 October; landing-single-board's test.skip(isLocalTarget, β¦) skips a
whole block, four tests, on a local target. None of that is visible from
the source text.
This is why thuisbatterij-journey.spec.ts was listed without a test count
until it was run, rather than with the 1 its source shows. The runner
agreed on 3 October, but only the run could say so.
The public-site suite needs the backend, and says nothing useful without it
Run with no backend on :3002, three of its six fail on timeouts β the two
search-journey tests and the detail-page axe scan, all of which need search
results. That is an unmet dependency, not a regression: the same specs at the
same commit pass 6/6 once the backend is up. Re-running serially reproduces
the same three, so it is not contention either.
3 and 9 October were the other case, and the check told them apart. With the backend up, the default parallel run failed two both times β the two search-journey tests, waiting 10s for the filter checkbox β while the detail-page axe scan, which also needs search results, passed. Re-run serially it was 6/6, in 7.8s and 6.4s. A missing backend fails the same three serially and in parallel; this failed two in parallel only. That is consistent with contention, not with an absent dependency, and a parallel-only failure is not a defect until it fails on its own β recorded as 6/6 serially; 2 timeouts in the parallel run.
The PA-demo suite is covered on its own page β see PA-demo suite. It is in CI because it needs no backend, database or Keycloak: Playwright starts its own dev server and that is the whole environment. The other two need a running stack, which is the whole of why they are not there yet β though the gap is narrower than it looks: the public-site suite needs one service, not five, and starts its own dev server already.
Deploying the E2E fixtures¶
Since v2026.10.1 the gate also needs the Heusden bundle, which this command does not deploy
e2e/helpers/required-processes.ts now lists nine process definitions
β HeusdenpasAanvraagProcess and HeusdenpasBeoordelingSubProcess under
tenant heusden beside the seven before β and three more untenanted
decisions, SVB_LeeftijdsInformatie, SZW_BijstandsnormInformatie and
RONL_HeusdenpasEindresultaat. Its own comment says why they are
different: there is no E2E fixture variant of that bundle. They are
deployed from linked-data-explorer's
examples/organizations/heusden/swimlanes-for-rba/, under the same keys
everywhere, with Organization heusden for the two processes and none for
the three decisions. npm run e2e:deploy-fixtures reads only
e2e-fixtures/manifest.json, so on a fresh engine globalSetup will
refuse to start until the Heusden bundle has been deployed by hand β see
Local development. The 9 October run passed
those gates on a stack where it already was.
New in v2026.09.13, and read at 963fe24 rather than run: the repository
root gained a script for the one precondition the table above describes but
cannot help you meet β the process and decision bundle that the frontend
suite's globalSetup refuses to start without.
It is a shim. The deployer lives in linked-data-explorer
scripts/deploy-e2e-fixtures.mjs in this repository deploys nothing. It
resolves a linked-data-explorer checkout β LDE_PATH, defaulting to a
sibling directory β and, if
<LDE>/scripts/deploy-e2e-fixtures.mjs is not there, exits 1 saying so and
naming the two ways to fix it. Otherwise it runs that script with
cwd set to the LDE checkout and passes arguments and the exit code
straight through.
The deployer moved because two copies had already drifted. Both read
ronl:documentRef out of the BPMN; when that attribute became a
comma-separated list, the copy here silently stopped matching any template
and the whole bundle would have failed to deploy. The fixtures, the
manifest and the deploy route all live in linked-data-explorer, so the
script does too β and the command name stays here, because this
repository's E2E gate is what needs the bundle and what prints the command
when it is missing.
What the real deployer does, and what it needs:
| Talks to | the linked-data-explorer backend at LDE_URL, default http://localhost:3001 β never straight to Operaton, so the bundle is also recorded in LDE's own store and the boardOwner tag is derived by the same code a Modeler deploy uses |
| Refuses | any target that is not on this machine. It asks GET /v1/dmns/process/deploy-target first and rejects a host that is not localhost, 127.0.0.1 or ::1 β a fixture bundle on a shared tier is drift, not a deployment |
| Reads | linked-data-explorer/e2e-fixtures/manifest.json, plus the external zorgtoeslag_resultaat rules set from TTL_EDITOR_REPO, default ../ttl-editor β the manifest names that decision but deliberately does not ship it |
| Step 1 | every file under sharedDecisions.files through POST /v1/dmns/deploy, without a tenant id, which is the point: each businessRuleTask resolves its decision with decisionRefTenantId="${null}" |
| Step 2 | one POST /v1/dmns/process/deploy per top-level manifest entry β sub-processes, forms and documents in the same request, organization set to the tenant directory it lives in β one request per entry, as one Modeler action is |
| Exit | 0 when everything deployed, 1 on the first failure. Every run deploys a new version of everything, since LDE disables Operaton's duplicate filtering; the gate reads the latest version, so a run always leaves the engine matching the fixtures on disk |
The gate prints this command, which is why the shim had to stay
packages/frontend/e2e/global-setup.ts checks four things, in order, and
throws on the first that fails with a message naming the fix:
- Chromium actually launches. It launches a browser and closes it
rather than comparing version strings β Playwright keeps its browsers
outside
node_modules, sonpm ciinstalls a new Playwright without fetching the build it needs, and every spec would otherwise die inbrowserType.launchwith the one explanatory line buried in the first of N identical failures. The fix it names isnpx playwright install chromium. - Four services answer: the frontend, the backend's
/v1/health, Keycloak, and β locally, or wheneverLDE_URLis set β the LDE backend's/v1/health. Three seconds each locally, fifteen against a remote tier. - Every process in the manifest is deployed under its tenant
(
verifyRequiredProcesses()). - Every decision those processes call is deployed without an
Organization (
verifyRequiredDecisions()), so a tenant-scoped process can reach it.
Failures 3 and 4 both print npm run e2e:deploy-fixtures as the fix β
which is precisely why the shim stays in this repository after the deployer
left it. A gate that names a command has to name one that exists.
Checks 3 and 4 are separate on purpose. A missing or tenant-pinned DMN does not show up as a missing process; it surfaces mid-journey as a 500 on process start, or on a citizen's screen as "probeer het opnieuw", neither of which mentions a decision.
What a full local E2E run actually needs
Five things, and none of them is started by Playwright for this suite:
docker compose up -dat this repository's root β Keycloak, Postgres, Redis;npm run devat this repository's root β frontend on:5173and backend on:3002;npm run dev:backendin the linked-data-explorer checkout β the LDE backend on:3001, which is also what the fixture deploy goes through;- a ttl-editor checkout beside them, for the one external decision;
npx playwright install chromium, thennpm run e2e:deploy-fixtures.
Since v2026.10.1, also the Heusden bundle β see the warning at the top of
this section. Only then does npm run test:e2e --workspace=@ronl/frontend
get past its own preconditions. That list is the whole reason this suite
is not in CI, and the reason its figures went unmeasured from 30 August
until a pass on 3 October found the stack already up.
Frontend Playwright suite¶
packages/frontend/e2e/, its own playwright.config.ts
(npm run test:e2e --workspace=@ronl/frontend). Chromium only, workers: 1.
The single worker is deliberate: two specs race to claim an identically-named task for the same caseworker against the shared local Operaton engine, so the suite trades parallelism for correctness.
Measured 9 October 2026: 47 tests across 15 specs β 42 passed, 5 skipped,
2.6m, one worker, against the developer's already-running local stack.
Nothing failed and nothing was retried. The previous run, on 3 October 2026,
was 28 tests across the 11 specs there were then: 27 passed, 1 skipped, 2.8m;
before that, on 30 August 2026 against acc at 15dfbf9, 27 tests across
10 specs, 27 passed, 1.9m.
| Spec | Tests | 9 October | Covers |
|---|---|---|---|
landing-single-board.spec.ts |
13 | 9 passed, 4 skipped β the link-preview tests, on a local target | New in v2026.10.1. The organisation landing pages /amsterdam and /heusden: the single-board layout, the caseworker login in the tenant's colours, the DigiD link with the tenant's test citizen, the per-tenant link preview, and the redirects from /?tenant= and to the grid |
infra-board-journey.spec.ts |
7 | 7 passed | The Infra-board, including a full sweep asserting no failed request and no console error |
pa-mock-journey.spec.ts |
5 | 5 passed | PA cockpit mock mode against the real store |
login-redirect.spec.ts |
5 | 5 passed | Role-based landing, parameterised per role |
citizen-services.spec.ts |
3 | 3 passed | New in v2026.10.1. The service cards each organisation's test citizen sees, parameterised per citizen (#344) |
protected-route.spec.ts |
3 | 3 passed | Route guards |
pa-live-authoring.spec.ts |
2 | 2 passed | Authoring against the live backend and a real database |
landing-access-denied.spec.ts |
2 | 2 passed | New in v2026.10.1. The no-access dialog on the landing page, and the Flevoland-account button on the boards with an Entra role |
rip-r21-journey.spec.ts |
1 | skipped β real ValidSign on the stack | The R2.1 phase, all twelve tasks, ending in the signing panel |
caseworker-journey.spec.ts |
1 | passed, 15.0s | The caseworker journey end to end β since v2026.10.1 through the Dutch Kapvergunning forms |
heusdenpas-journey.spec.ts |
1 | passed, 11.8s | New in v2026.10.1. A fourth deep two-persona journey: a Heusden citizen applies for a Heusdenpas, three untenanted decisions grant it, and the Heusden caseworker decides and informs |
thuisbatterij-journey.spec.ts |
1 | passed, 7.7s | A citizen applies for a Thuisbatterij subsidy, the six-decision RechtEnHoogteSubsidieThuisbatterij DRD evaluates, and the caseworker reviews the resulting task |
tenant-isolation.spec.ts |
1 | passed, 7.5s | Tenant scoping |
zorgtoeslag-journey.spec.ts |
1 | passed, 6.2s | The zorgtoeslag journey |
smoke.spec.ts |
1 | passed | Boot and render |
What the Heusdenpas journey guards beyond its own service
Read from the spec at ebec288. Every step of it submits a form with a
field that is required but hidden β the partner's date of birth for a
single applicant, the missing details when the application is complete,
the motivation when the caseworker accepts the outcome β so the journey
stalls on a submit that never goes through if form-js ever validates a
hidden field. Its applicant is single, 21 or over, without children and on
a Baanbrekers benefit, so the rules grant the pass automatically
(linked-data-explorer's verified case 6). It is also the one journey that
ends back on the citizen's side, checking that the decided application
appears once in Mijn aanvragen, not once per sub-process.
The thuisbatterij journey accepts both task names β edited in v2026.09.12
0e71fd3, "match the Thuisbatterij tasks by their Dutch names as well",
widened the two task matchers the caseworker half of the journey depends
on, because the Thuisbatterij process definitions β which come from
linked-data-explorer β rename those tasks in the swimlane redesign. Each
regex now accepts the old English name or the new Dutch one:
| Task | Before the redesign | After it |
|---|---|---|
| The review | Case review: recht en hoogte subsidie⦠| Beoordeling behandelaar: recht en hoogte subsidie⦠|
| The follow-up notify task | Phase 6: Notify applicant of decision | Fase 6: Aanvrager informeren over besluit |
Accepting both rather than switching to the new names is deliberate, and the
commit says why: the journey should pass on engines still running the old
definitions and on those running the new ones. The test count is
unchanged β one test(). Read from the spec at 2443adc; the spec was
first run by these pages on 3 October 2026, and passed.
Three more specs accept the Dutch names, and the login helper matches exactly β v2026.09.14
linked-data-explorer redrew the Kapvergunning and Zorgtoeslag processes in swimlanes with Dutch task names, and three specs followed the Thuisbatterij one, each regex accepting the old English name or the new Dutch one so the journeys pass against either deployment:
| Spec | Commit | Review task, after the redesign | Notify task, after it |
|---|---|---|---|
caseworker-journey.spec.ts |
10e83ce |
Beoordeling behandelaar: besluit kapvergunning | Fase 6: Aanvrager informeren over besluit |
zorgtoeslag-journey.spec.ts |
1f0e52b |
Beoordeling behandelaar: besluit voorlopige aanspraak | Fase 6: Aanvrager informeren over besluit |
tenant-isolation.spec.ts |
1f0e52b |
Beoordeling behandelaar: besluit voorlopige aanspraak | Fase 6: Aanvrager informeren over besluit |
In tenant-isolation.spec.ts the change matters more than a selector
usually does: REVIEW_TASK_NAME became a regex so that the check that a
Flevoland caseworker cannot see the task stays meaningful after the
rename, instead of passing on a name that no longer exists. A negative
assertion against a stale name is green for the wrong reason.
Separately, e2e/helpers/auth.ts now finds the medewerker login button
with { name: 'Inloggen', exact: true } (9f54e82), because the landing
page's new "Inloggen met uw Flevoland-account" button made the substring
match ambiguous and Playwright's strict mode refused to click either. Every
spec that signs a medewerker in goes through that helper. Test counts are
unchanged in all four files; read from the source at ae06c9e, and all
four passed when run on 3 October.
What the thuisbatterij journey is actually guarding
Read from the spec at 86af73e, not run; the passage below is unchanged at 2443adc. Its processes deploy under
tenant-id flevoland while the DMNs they call deploy without a tenant,
so every business-rule task carries
camunda:decisionRefTenantId="${null}" to reach them. Drop that attribute
and the engine refuses to instantiate at all β "no decision definition
deployed with key 'AwbCompletenessCheck' and tenant-id 'flevoland'" β
which reaches a user as a 500 from POST /v1/process/:key/start and an
unexplained "aanvraag kon niet worden ingediend". That is an ACC outage
that already happened once; this spec exists to catch the next one before a
deploy repeats it, which is also why globalSetup gained its separate
decision check.
rip-r21-journey.spec.ts now starts R2.1 with a project identity β issue #165 is closed
Fixed in aadedfc on 20 September 2026 and closed as completed the same
day. Established by reading the spec and the component at 86af73e on
24 September, not by running either.
Through v2026.09.9 the spec clicked the R2.1 start button without filling the two fields v2026.09.8 had made required, so it waited ninety seconds for a control that could never become enabled. The spec had not been updated alongside its component; the component was right.
What it does now (rip-r21-journey.spec.ts:617-630):
const startForm = page.locator('.pb-new-project');
const startButton = page.locator('.pb-new-project + button');
await expect(startForm).toBeVisible();
await expect(startButton).toHaveText(/R2\.1 starten/);
await startForm.getByLabel('Projectnummer').fill(PROJECT_NUMBER);
await startForm.getByLabel('Projectnaam').fill(PROJECT_NAME);
await expect(startButton).toBeEnabled();
Three things in that block are worth copying rather than merely reading:
- Both fields are filled from module-scope fixtures β
E2E-26014and "E2E β R2.1 journey (test, safe to delete)" β deliberately unmistakable rather than realistic, so a project this spec leaves behind is obvious in the board. toBeEnabled()comes before the click. That turns the gate into an assertion instead of an implicit wait, so the next regression here names the gate rather than timing out on it.- The locator is pinned structurally to the block the step means, and
what it found is asserted before it is clicked. Two buttons in this tab
read "R2.1 starten" β this one and the bulk "start the selected
projects" β in different branches of
PhaseDetail's ternary. They cannot render together today, so a baregetByRoleresolves; pinning keeps that true if the branches ever converge.
A further assertion switches to the WIP tab and checks the started instance carries the number and name it was started with. Without it the spec would pass just as happily against a build that dropped both values on the floor.
The component confirms it, read at the same commit:
PhaseDetail.tsx renders both inputs as required inside
<div className="pb-new-project">, and the sibling button is
Spec and component now agree.
The R2.1 journey is the one to watch after a signing change. Because the
approval task carries ronl:signatureRef, the board renders the
signing panel where a form used to be β and the
journey previously drove every task by filling a form, so it failed on the last
one reporting that a form never rendered. A true statement about a task that no
longer has one. Issue #165 was the same failure mode one step earlier in the
journey: the UI grew a precondition and the spec did not hear about it. Twice in
two releases, in one spec, is the pattern to take from it β this journey drives
more UI surface than any other here, so it is the first to notice when that
surface moves.
It also carries two test.skip(true, reason) calls inside test bodies,
which skip the run when preconditions are not met and log the reason first.
There was one at v2026.09.9; the second arrived in 097ff84, "skip rather than
fail where a tier signs for real" β a tier with real signing configured cannot
complete the journey's approval task, and skipping with a reason is the honest
outcome there rather than a red run. Neither skipped in the 30 August
measurement, which predates both. On 3 October the second one fired, on a
local stack configured with VALIDSIGN_STUB_MODE=false, and logged:
[rip-r21-journey] SKIPPED β local dev stack signs with the real ValidSign
(VALIDSIGN_STUB_MODE=false), and this journey will not request a binding
signature. Nothing was created β the refusal happens before
POST /task/:id/package. Run it against a target where VALIDSIGN_STUB_MODE=true.
That is the outcome the commit designed for, and it is why the 3 October run read 27 passed, 1 skipped rather than 28 passed. On 9 October it fired again, with the same message. It also means the R2.1 work β and v2026.10.0's signing changes, the panel now shared with the caseworker inbox and signing state kept per task, and v2026.10.1's signer recorded as the eDOCS author β was not driven in a browser by either pass. A run against a target in stub mode is what would.
Coverage per board¶
The 47 tests do not spread evenly, and seven of the fifteen specs belong to no board. This table is the one to check before claiming a board has or lacks end-to-end coverage β the per-board pages defer to it.
| Board | Specs | Tests | Which |
|---|---|---|---|
| Infra-board | 2 | 8 | infra-board-journey (7, the shell), rip-r21-journey (1, the work β skipped on 3 and 9 October, see above) |
| PA cockpit | 2 | 7 | pa-mock-journey (5), pa-live-authoring (2) |
| Caseworker | 4 | 4 | caseworker-journey (1), zorgtoeslag-journey (1), thuisbatterij-journey (1), heusdenpas-journey (1, new in v2026.10.1) β each ending in a caseworker's task; the first three match the Dutch task names as well as the English ones since v2026.09.14 |
| Woo-dashboard | 0 | 0 | β |
| Citizen portal and landing pages | 3 | 18 | landing-single-board (13, four of them skipped on a local target), citizen-services (3), landing-access-denied (2) β all new in v2026.10.1 |
| No single board | 4 | 10 | login-redirect (5), protected-route (3), tenant-isolation (1), smoke (1) |
The last two rows are the reason a naive per-board sum of the boards alone does not reach 47. The citizen's dashboard and the organisation landing pages are not boards and have no board page of their own β their unit tests are counted on Overview among the frontend files that belong to no board β and authentication redirects, route guards, tenant scoping and the boot smoke test cut across every board and belong to none.
The Caseworker row is the board row that moved: heusdenpas-journey.spec.ts
is a fourth deep journey ending in a caseworker's decision, and the first for
an organisation other than Flevoland and Dienst Toeslagen.
No spec drives Besluitvorming, the caseworker section v2026.10.0 added; it is covered by unit tests only β see Caseworker.
Re-derive this table from the spec directory, not from the release being synced
The Infra-board specs landed on 24 August 2026 and this documentation
continued to record the board as having no end-to-end coverage at all
through two subsequent syncs. Nothing in a changelog-driven pass pointed at
them, because neither release that added them was the one being documented.
A per-board E2E claim is only as current as the last time somebody listed
packages/frontend/e2e/.
What it needs running¶
docker compose up -d at the repo root (Keycloak + Postgres + Redis), the
backend and frontend dev servers, and a sibling linked-data-explorer repo's
backend on :3001 β the last is required for the Procesbibliotheek journey.
e2e/global-setup.ts, re-read at 86af73e, probes four URLs before any
test runs β http://localhost:5173, http://localhost:3002/v1/health,
http://localhost:8080 for Keycloak, and http://localhost:3001/v1/health β
and throws with each missing one named:
E2E preconditions not met β target: local dev stack.
- Frontend not reachable at http://localhost:5173
- Backend not reachable at http://localhost:3002/v1/health
- Keycloak not reachable at http://localhost:8080
- LDE backend not reachable at http://localhost:3001/v1/health
The LDE probe runs locally, or whenever LDE_URL is set explicitly β a shared
tier has no LDE backend under a predictable name, so it is skipped there rather
than failed. Against a remote target the per-probe timeout rises from 3s to 15s,
because a cold-started App Service is slower to answer than loopback.
Two further gates run after the probes, and each throws with its own
instructions: verifyRequiredProcesses() checks the fixture bundle is deployed
under the right tenant, and verifyRequiredDecisions() checks the DMN
definitions are deployed without an Organization. A decision listed under
an Organization is the failure mode here, not an absent one.
Before any of that, globalSetup launches and closes a Chromium. Playwright
keeps its browsers outside node_modules, so npm ci installs a new Playwright
without fetching the build it needs; every spec would otherwise die in
browserType.launch and the one line explaining it would be buried in the first
of N identical failures. The launch costs about a second and is the thing that
actually has to work, which is why it is preferred to comparing versions or
guessing at paths.
It also refuses to run against production unless CONFIRM_PROD=1 is set. These
journeys are not read-only β they start real process instances and complete real
tasks, which against production is real case data in the audit log.
It does not start anything itself, and playwright.config.ts declares no
webServer, which is why this suite is not wired into CI: there is no human
to start the stack on a runner. The PA-demo suite has no such dependency and
does run in CI β the difference is the stack, not the tooling. See
Overview β Roadmap for what closing that would take.
The probe deliberately builds its own AbortController rather than using
AbortSignal.timeout(): the latter did not always clean up its internal timer
before the fetch settled, crashing Node on Windows with a libuv
UV_HANDLE_CLOSING assertion during process exit. That crash used to be listed
as a blocker for putting this suite in CI and no longer is.
Getting JSON output¶
The config declares list and html reporters, not json. To capture machine
-readable results:
PLAYWRIGHT_JSON_OUTPUT_NAME=../../playwright-report/frontend-e2e.json \
npm run test:e2e --workspace=@ronl/frontend -- --reporter=list,json
Three things will bite otherwise. PLAYWRIGHT_JSON_OUTPUT_NAME resolves
against the cwd, which under --workspace is packages/frontend, hence the
../../. Passing --reporter replaces the configured list, so the
auto-opening HTML report is lost unless you add it back. And do not redirect
stdout to a file: globalTeardown prompts interactively there
(Clean up Operaton history� [y/N]), so the prompt would be swallowed into
the file and the terminal would appear to hang.
The HTML report also embeds the same result data, which is where the 20 August figures came from when no JSON reporter was configured.
Public-site Playwright suite¶
Covered on Public site suite β six tests including three axe-core accessibility scans, and the one suite here that starts its own dev server.
Re-run on 9 October 2026, against the developer's already-running backend β and, as on 3 October, the one suite in the pass that went red before it went green, the same two tests in the same way:
| Run | Workers | Result | Time |
|---|---|---|---|
9 Oct, default (fullyParallel: true, no workers set) |
6 | 4 passed, 2 failed β publiek.spec.ts:6 search β filter β detail β back preserves the filtered URL and :27 a deep link with filters pre-applied renders those filters checked, each a TimeoutError after 10s waiting for getByRole('checkbox', { name: /Regel/ }) |
16.8s |
9 Oct, --workers=1 |
1 | 6 passed | 6.4s |
| 3 Oct, default | 6 | 4 passed, 2 failed β the same two, the same wait | 17.2s |
3 Oct, --workers=1 |
1 | 6 passed | 7.8s |
Recorded as 6/6 serially, with 2 timeouts in the parallel run β not as a
defect. Both failures are the same wait in the two tests that need a
filtered result list; the detail-page axe scan, which also needs search
results, passed in the same parallel run, and the serial run passed all six
against the same backend. That is consistent with contention, not with a
missing backend (which fails three, serially too), and nothing failed on its
own. It is worth
watching rather than dismissing β two passes a week apart now agree on
exactly which wait gives way: if this suite is wired into CI β see
Overview β Roadmap β that 10-second wait under
parallel workers is the first thing the run will test. The spec is unchanged
since 2443adc and still in no workflow. See
Public site suite.
Live smoke suite (shell scripts, cross-app)¶
Four gated shell scripts under scripts/, deliberately kept out of npm test β
they hit real running services over the network, mutate real data in three
cases (one of them only data it created itself), and need real credentials for
some tiers β and, new in v2026.10.1, a fifth that runs two of them as a
signed-in person.
These were not run for this page. They are described from their
configuration and source only β test-m2m-routes.sh, test-smoke-live.sh
and test-edocs-person.sh read at ebec288.
| Script | Covers | Mutates? |
|---|---|---|
test-smoke-live.sh |
Cross-app health: Operaton, Keycloak, LDE, TriplyDB, CPRMV, media store, eDOCS reach/status, MCP layer. Since v2026.10.1 Tier 2b also asks /v1/edocs/status as the seeded caseworker, who has no Entra identity and must be reported as such (skipped in stub mode), and an optional Tier 2c runs when PERSON_TOKEN holds a Flevoland-signed-in person's Keycloak token, passing only if eDOCS knows that person |
No |
test-edocs-live.sh |
eDOCS through /v1/edocs and through the MCP route: workspaces listed read-only, standalone documents uploaded and read back β see eDOCS β Live Testing |
Yes β every run leaves its uploaded documents behind; nothing is deleted |
test-doccle-live.sh |
Doccle sender API β see Doccle β Live Testing | Yes β not yet live-tested, still DOCCLE_STUB_MODE=true in every run so far |
test-m2m-routes.sh |
The active /v1/m2m operations: the reads, decision.evaluate, and β since v2026.09.14 (#214) β a write lifecycle of start, claim, complete and delete. Since v2026.10.0 it also asserts that POST /v1/m2m/process/history filters β a request filtered on one processDefinitionKey must return at least one instance and none of another key, since a dropped filter answers 200 with the whole history (#263); since v2026.10.1 that the GET spelling answers 404, removed after one deprecated release β against an older backend that check fails, which says which build you are talking to (in v2026.10.0 it asserted Deprecation: @1790985600 instead); and that start and complete refuse a caller-supplied municipality with 400 RESERVED_VARIABLE (#261), the refused completion leaving the task open for the real one. Its tenant-isolation check reads the problem's code. Local by default, reading the seeded client secret from the realm file; TARGET=acc needs an explicit CLIENT_SECRET, and on ACC the M2M surface now uses ACC's main engine (#262) |
Yes, but only its own: the lifecycle starts two instances and removes both, and never writes to an instance it did not create β if the reserved-variable guard on start ever failed, the stray instance is cancelled at once |
| test-edocs-person.sh | New in v2026.10.1. Not a suite of its own: it reads a Flevoland-signed-in person's Keycloak token from the clipboard (or an exported PERSON_TOKEN), checks it, prints only its claims, and hands it to test-edocs-live.sh (live, sections 1b and 1c) or test-smoke-live.sh (smoke, Tier 2c); diag asks Keycloak's broker endpoint for the person's stored Entra token β status and key names only β and shows what the backend makes of the person. Targets local and acc, and prod for smoke. See eDOCS β Live Testing | Only through live, as test-edocs-live.sh does |
bash scripts/test-smoke-live.sh # local, full run
CLIENT_SECRET=<secret> TARGET=acc bash scripts/test-smoke-live.sh # against ACC
bash scripts/test-edocs-live.sh # eDOCS, mutating
bash scripts/test-edocs-person.sh live|smoke|diag [local|acc] # as a signed-in person
CLIENT_SECRET=<secret> bash scripts/test-doccle-live.sh # Doccle, mutating
bash scripts/test-m2m-routes.sh # M2M routes, local
TARGET=acc CLIENT_SECRET=<secret> bash scripts/test-m2m-routes.sh # M2M routes vs ACC
Exit 0 when nothing failed, 1 on any real failure β a dependency that is
intentionally off (stub mode, no CLIENT_SECRET) skips with a ~ note, never
a red fail. curl http://localhost:3002/v1/health | jq ., or the ACC
equivalent, is the fastest single check of a running instance's dependency
status, independent of the smoke scripts.
Rate limiting will masquerade as an outage¶
The backend rate-limits per IP. A short authoring journey measures ~21 requests
to /v1/pa/*, so two specs back to back can exhaust a low budget, and the UI
renders the resulting 429 as "Kon dossiers niet laden" β indistinguishable
from a backend that is down.
e2e/helpers/rate-limit.ts records the first 429 of a run and fails the test
with a message naming the throttle. It deliberately does not retry or wait
it out. The shipped default was raised to 1000/min in config.ts; note the
limiter keys on IP, so TRUST_PROXY decides whether that budget is per user or
per deployment.
The full account of how that was diagnosed β including two confident wrong answers before anyone looked at the response codes β is on Writing tests.