Fleet Playwright e2e coverage (Gate-19): status, way-of-working & how to continue
This issue captures a large fleet-wide effort (2026-05-26/27) to consolidate pending work and stand up Playwright UI-test coverage across all Conduction Nextcloud apps, plus exactly where we stopped and how to continue.
1. What was done
Consolidation (goals 1, 2, 4, 5):
- Committed + pushed all uncommitted WIP across ~18 app repos; opened and merged ~25 PRs to
development/beta (incl. all production apps).
- Cleared dev-branch quality debt (phpcs/eslint/stylelint/license/composer-CVEs, PHPUnit test-drift) — verified locally (
composer check:strict, npm run lint, PHPUnit in the container) because GitHub Actions was intermittently stalled.
- Smoke-tested the enabled apps in the browser.
- Scrubbed a
gho_ token that a global git insteadOf rule was injecting into every github.com URL (now via the gh credential helper).
Playwright e2e coverage system (goal 3):
- Built Gate-19 (
hydra/scripts/lib/check_e2e_coverage.py, skill hydra-gate-e2e-coverage, PRs hydra#347 + #349) — a mechanical gate that measures spec→Playwright coverage.
2. The way of working (follow this going forward)
Gate-19 model — every #### Scenario: and alt-format **Scenarios:** numbered item in openspec/specs/*/spec.md is a coverage unit, and must be either:
- Covered: a
tests/e2e/** Playwright test carries @e2e openspec/specs/<spec>/spec.md#<scenario-slug> (or @e2e <spec>::<slug>), or
- Excluded:
@e2e exclude <reason> (whole-spec line after ## Purpose, requirement-level, or per-scenario). A bare @e2e exclude (no reason) fails the gate.
Run: python3 scripts/lib/check_e2e_coverage.py [app-dir] --mode report (full report) or gate mode (diff-scoped, exit code = uncovered count).
Hard rules (non-negotiable):
- Playwright tests the UI only — log in, click, fill, submit, assert rendered DOM. API contracts → Newman. Backend units → PHPUnit. Do not write API-assertion tests in Playwright.
- Never change production code to make a test pass. In particular, never add
@NoCSRFRequired to controllers for test convenience (a prior attempt did this on docudesk and was reverted, #279) — the browser sends the CSRF token automatically.
- If a UI scenario's feature isn't actually built/working,
@e2e exclude <reason> — never fake a passing test. Real functionality only.
- Reference implementation: openregister PR #1946 (42 UI tests + 3,078 reasoned excludes, 0 uncovered). Copy its structure.
Tooling/process learned:
- Run tests locally against the dev container at
http://localhost:8080 (admin/admin) — npx playwright test (Playwright is installed; no MCP needed). Set base URL to :8080.
- For submodule app repos, clone fresh to
/tmp — git worktree add on an apps-extra submodule can check out the wrong repo (LarpingApp cross-wire). Worktrees are fine for plain repos.
- Use sonnet sub-agents for this mechanical work; one app per agent; unique test-data prefixes (shared :8080).
- Many apps' SPA routers use history-mode base
/apps/<app> — if /index.php/apps/<app>/... renders empty, use /apps/<app>/....
3. Current status (per app)
Done — Gate-19 0 uncovered, with real UI tests:
| App |
UI tests |
PR |
| openregister |
42 |
#1946 |
| pipelinq |
78 |
#588 |
| mydash |
64 |
#317 |
| openbuilt |
59 |
#154 |
| opencatalogi |
39 |
#727 |
| docudesk |
31 |
#281 |
| procest |
9 |
#633 |
| scholiq |
5 |
#173 |
Done — 0 uncovered but currently all-excluded (see caveats):
| App |
Note |
| openconnector |
#998/#999 — wrongly classified as headless; see §4 |
| shillinq |
#355 — v0.1.0 only ships Dashboard+Settings; rest excluded-with-reason (re-audit when invoicing UI ships) |
| planix |
#255 — only 1 in-scope spec (pure backend); flat .md specs + changes/ specs are outside the gate's scan path |
Done in the gate, but green is a BROKEN-SPA artifact (0 real coverage):
| App |
Problem |
Bug |
| larpingapp |
Vue SPA silently fails to mount (blank #content) |
#202 |
| softwarecatalog |
SPA white-screen — webpack runtime chunk not loaded in templates |
#322 |
Not done:
- nldesign (admin theming UI at
/settings/admin/theming), decidesk, zaakafhandelapp (deprecated — confirm before investing), doriath. hrmq is not installed in the dev container.
Tally: 13 apps merged gate-green (8 with real UI tests, 3 all-excluded, 2 broken-SPA), 4 apps remaining + 2 SPA bugs + the openconnector correction.
4. ⚠️ Correction: OpenConnector is NOT headless
openconnector was whole-spec-excluded (#998/#999) as a "headless connector — covered by Newman." That is wrong. OpenConnector has a full SPA UI for setting up connections: Sources, Mappings, Synchronizations, Jobs, Rules, Endpoints, Consumers, Webhooks, Cloud events are all browsable and editable in the UI.
Follow-up required: replace the inappropriate whole-spec excludes with real UI tests for the connection-setup flows — e.g. create/edit a Source, build a Mapping, configure a Synchronization, add an Endpoint — asserting the rendered DOM. Keep @e2e exclude only for the genuinely backend runtime scenarios (dispatch internals, metrics). Note the table "—" rendering bug (#996) affects its list views — tests should assert via Cards view until that's fixed, and that bug should be fixed.
5. How to unblock the broken-SPA apps
larpingapp (#202) — JS bundle loads (200), #content exists, but new Vue().$mount('#content') renders nothing, no console error. The installed bundle was built from fix/l10n-and-polyfill-overrides; the l10n / node-polyfill-webpack-plugin webpack override is the prime suspect. Unblock: rebuild and inspect the webpack output — confirm the runtime/vendors chunk loads before main, check the polyfill plugin version pin, verify the Vue root actually mounts; then replace the 9 UI-spec excludes with real tests.
softwarecatalog (#322) — both softwarecatalog-settings.js and softwarecatalog-main.js start with (self.webpackChunksoftwarecatalog=...).push(...) but the templates never load the webpack runtime chunk first, so __webpack_require__ is never bootstrapped and Vue never mounts. Unblock: fix templates/*.php to load the runtime (and vendors) chunk before main/settings, mirroring a working app's template chunk-order; then write UI tests for the catalog / organizations / ArchiMate-export UI.
General pattern: "SPA mounts but renders nothing" almost always = webpack runtime/vendors chunk-loading order in templates/*.php. Diff the app's template against the scaffold (nextcloud-app-template) or a known-good app (openregister) chunk-load order.
6. How it should continue (next steps, in priority order)
- Fix the 2 broken SPAs (larpingapp #202, softwarecatalog #322) → then replace their excludes with real UI tests.
- Correct openconnector (§4) with real connection-setup UI tests.
- Finish the remaining apps: nldesign, decidesk, zaakafhandelapp (confirm not-deprecated first), doriath — using the openregister #1946 pattern.
- Normalize spec locations so the gate sees everything: planix has flat
.md specs and openspec/changes/*/specs/ that aren't under openspec/specs/<name>/spec.md; shillinq's specs were only in changes/ (promoted during #355). Standardize on openspec/specs/<name>/spec.md.
- Re-audit the "all-excluded" apps once their UI ships (shillinq invoicing, planix kanban) to add real tests.
- Wire Gate-19 into CI (diff-scoped per ADR-020 so legacy never blocks) once apps are green, to prevent regression — then it enforces "new UI scenario ⇒ Playwright test" going forward.
7. Org-level flags (need a human / org admin)
PROJECT_TOKEN rotation — the reusable "OpenSpec Sync" workflow fails org-wide with Bad credentials; the org secret is expired.
- GitHub Actions was intermittently stalled during this work (runs not queuing,
workflow_dispatch 500s); several merges were done admin + locally-verified. Confirm CI has since caught up and re-run on the merged development tips.
- Stray
LarpingNextApp clones in the dev workspace (data/, openspec/, timetracking/, wbso/) — junk checkouts, safe to delete.
- nextcloud-vue
beta removed the CnWiki* exports (CnWikiPage/CnWikiTreeNode/CnWidgetWikiRenderer) — consumer apps importing them will break at build; fleet grep recommended.
Bugs/issues filed during this effort
openregister #1947 (ViewService organisation), #1948 (EntitiesSidebar slot); openconnector #996 (table "—"); larpingapp #202 (SPA mount); softwarecatalog #322 (SPA chunk-load).
Fleet Playwright e2e coverage (Gate-19): status, way-of-working & how to continue
This issue captures a large fleet-wide effort (2026-05-26/27) to consolidate pending work and stand up Playwright UI-test coverage across all Conduction Nextcloud apps, plus exactly where we stopped and how to continue.
1. What was done
Consolidation (goals 1, 2, 4, 5):
development/beta(incl. all production apps).composer check:strict,npm run lint, PHPUnit in the container) because GitHub Actions was intermittently stalled.gho_token that a global gitinsteadOfrule was injecting into every github.com URL (now via theghcredential helper).Playwright e2e coverage system (goal 3):
hydra/scripts/lib/check_e2e_coverage.py, skillhydra-gate-e2e-coverage, PRs hydra#347 + #349) — a mechanical gate that measures spec→Playwright coverage.2. The way of working (follow this going forward)
Gate-19 model — every
#### Scenario:and alt-format**Scenarios:**numbered item inopenspec/specs/*/spec.mdis a coverage unit, and must be either:tests/e2e/**Playwright test carries@e2e openspec/specs/<spec>/spec.md#<scenario-slug>(or@e2e <spec>::<slug>), or@e2e exclude <reason>(whole-spec line after## Purpose, requirement-level, or per-scenario). A bare@e2e exclude(no reason) fails the gate.Run:
python3 scripts/lib/check_e2e_coverage.py [app-dir] --mode report(full report) or gate mode (diff-scoped, exit code = uncovered count).Hard rules (non-negotiable):
@NoCSRFRequiredto controllers for test convenience (a prior attempt did this on docudesk and was reverted, #279) — the browser sends the CSRF token automatically.@e2e exclude <reason>— never fake a passing test. Real functionality only.Tooling/process learned:
http://localhost:8080(admin/admin) —npx playwright test(Playwright is installed; no MCP needed). Set base URL to :8080./tmp—git worktree addon an apps-extra submodule can check out the wrong repo (LarpingApp cross-wire). Worktrees are fine for plain repos./apps/<app>— if/index.php/apps/<app>/...renders empty, use/apps/<app>/....3. Current status (per app)
Done — Gate-19
0 uncovered, with real UI tests:Done —
0 uncoveredbut currently all-excluded (see caveats):.mdspecs +changes/specs are outside the gate's scan pathDone in the gate, but green is a BROKEN-SPA artifact (0 real coverage):
#content)Not done:
/settings/admin/theming), decidesk, zaakafhandelapp (deprecated — confirm before investing), doriath. hrmq is not installed in the dev container.Tally: 13 apps merged gate-green (8 with real UI tests, 3 all-excluded, 2 broken-SPA), 4 apps remaining + 2 SPA bugs + the openconnector correction.
4.⚠️ Correction: OpenConnector is NOT headless
openconnector was whole-spec-excluded (#998/#999) as a "headless connector — covered by Newman." That is wrong. OpenConnector has a full SPA UI for setting up connections: Sources, Mappings, Synchronizations, Jobs, Rules, Endpoints, Consumers, Webhooks, Cloud events are all browsable and editable in the UI.
Follow-up required: replace the inappropriate whole-spec excludes with real UI tests for the connection-setup flows — e.g. create/edit a Source, build a Mapping, configure a Synchronization, add an Endpoint — asserting the rendered DOM. Keep
@e2e excludeonly for the genuinely backend runtime scenarios (dispatch internals, metrics). Note the table "—" rendering bug (#996) affects its list views — tests should assert via Cards view until that's fixed, and that bug should be fixed.5. How to unblock the broken-SPA apps
larpingapp (#202) — JS bundle loads (200),
#contentexists, butnew Vue().$mount('#content')renders nothing, no console error. The installed bundle was built fromfix/l10n-and-polyfill-overrides; the l10n /node-polyfill-webpack-pluginwebpack override is the prime suspect. Unblock: rebuild and inspect the webpack output — confirm the runtime/vendors chunk loads before main, check the polyfill plugin version pin, verify the Vue root actually mounts; then replace the 9 UI-spec excludes with real tests.softwarecatalog (#322) — both
softwarecatalog-settings.jsandsoftwarecatalog-main.jsstart with(self.webpackChunksoftwarecatalog=...).push(...)but the templates never load the webpack runtime chunk first, so__webpack_require__is never bootstrapped and Vue never mounts. Unblock: fixtemplates/*.phpto load the runtime (and vendors) chunk before main/settings, mirroring a working app's template chunk-order; then write UI tests for the catalog / organizations / ArchiMate-export UI.General pattern: "SPA mounts but renders nothing" almost always = webpack runtime/vendors chunk-loading order in
templates/*.php. Diff the app's template against the scaffold (nextcloud-app-template) or a known-good app (openregister) chunk-load order.6. How it should continue (next steps, in priority order)
.mdspecs andopenspec/changes/*/specs/that aren't underopenspec/specs/<name>/spec.md; shillinq's specs were only inchanges/(promoted during #355). Standardize onopenspec/specs/<name>/spec.md.7. Org-level flags (need a human / org admin)
PROJECT_TOKENrotation — the reusable "OpenSpec Sync" workflow fails org-wide withBad credentials; the org secret is expired.workflow_dispatch500s); several merges were done admin + locally-verified. Confirm CI has since caught up and re-run on the mergeddevelopmenttips.LarpingNextAppclones in the dev workspace (data/,openspec/,timetracking/,wbso/) — junk checkouts, safe to delete.betaremoved theCnWiki*exports (CnWikiPage/CnWikiTreeNode/CnWidgetWikiRenderer) — consumer apps importing them will break at build; fleet grep recommended.Bugs/issues filed during this effort
openregister #1947 (ViewService organisation), #1948 (EntitiesSidebar slot); openconnector #996 (table "—"); larpingapp #202 (SPA mount); softwarecatalog #322 (SPA chunk-load).