Skip to content

ci(e2e): turn on the shared "E2E Tests (Playwright)" job - #248

Merged
rubenvdlinde merged 6 commits into
developmentfrom
feat/enable-e2e-playwright
Aug 4, 2026
Merged

ci(e2e): turn on the shared "E2E Tests (Playwright)" job#248
rubenvdlinde merged 6 commits into
developmentfrom
feat/enable-e2e-playwright

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

Turns enable-playwright: true on in .github/workflows/code-quality.yml and ships the four artifacts the shared workflow needs to run the existing 10-file e2e suite honestly.

Before this PR the shared workflow reported quality / E2E Tests (Playwright) as skipped on every run — a conclusion indistinguishable, in every summary the pipeline prints, from a job that passed. The suite under tests/e2e/ (172 tests across 10 spec files) has been maintained for months and was never executed by CI.

Why each piece exists

1. tests/e2e/playwright.config.ts (new) — CI-only, ONE project

The shared workflow runs:

CONFIG="${{ inputs.playwright-test-path }}/playwright.config.ts"
if [ ! -f "$CONFIG" ] && [ -f "playwright.config.ts" ]; then CONFIG="playwright.config.ts"; fi
npx playwright test --config="$CONFIG"

No --project is passed, so every project in the chosen config runs. The root config declares three:

project why it must not run in CI
chromium this is the one CI wants
docs-capture re-shoots every documentation screenshot on every PR
visual its own README records that the committed PNG baselines are host-font/GPU specific and cannot byte-match a Linux runner

playwright-test-path: tests/e2e makes the workflow's first config lookup hit the new file, which declares exactly one project. The root config is deliberately untouched — the separate Journeydoc Capture job greps it for name: 'docs-capture' and would hard-fail if that project were removed.

testIgnore is spelled out at both top level and project level, because a project-level testIgnore replaces the top-level one rather than merging with it.

2. tests/e2e/_base-url.ts — accepts BASE_URL

BASE_URL is the name the shared workflow actually exports, and it was the one spelling this resolver omitted. openconnector shipped a PLAYWRIGHT_BASE_URL-only resolver and its E2E job has hard-failed on every run since with PLAYWRIGHT_BASE_URL is not set.

The localhost:8080 fallback is added only under CI / GITHUB_ACTIONS, where it is the runner's own throwaway php -S. Off CI it still throws — :8080 on a developer box is the shared dev container, and the no-default rule exists to protect it.

3. tests/e2e/ci-seed.sh (new) — explicit, forced, verified register import

occ app:enable larpingapp imports the register from an IRepairStep, which runs with no user session, so OpenRegister's RBAC denies it — and the repair step catches \Throwable and downgrades it to a warning, so occ app:enable still exits 0. The app enables cleanly, the SPA boots, and the register simply is not there.

So the seed does the import explicitly over the admin HTTP API:

  • POST /index.php/apps/larpingapp/api/settings/reimportloadSettings(force: true). Unlike OpenRegister's generic importer this goes through ConfigFileLoaderService, which merges the lib/Settings/register.d/*.json fragments into the base register first.
    ⚠️ larpingapp does not return \OCA\OpenRegister\AppHost\Routes::standard() from appinfo/routes.php, so there is no settings#load route here. settings/reimport is the real one.
    ⚠️ OCS-APIRequest: true is load-bearing — reimport() has no #[NoCSRFRequired] (removed deliberately in [HIGH] SettingsController::create and reimport POST endpoints have @NoCSRFRequired — admin CSRF-forgable #206), so without the header the POST is a CSRF failure.
  • Falls back to POST /apps/openregister/api/configurations/import (base file + each fragment) if that does not report an explicit "success": true. HTTP 200 alone is not sufficient: reimport() returns {"success": false} with a 200.
  • Then verifies the register plus all 11 schema slugs, read out of the repo's own JSON rather than derived. Three are prefixed on purpose: larping_item, larping_event, larping_attendance — the bare item slug collides globally with a foreign app's QTI schema, which is what used to make every fixture create 400.
  • Then probes the object collection and dumps the resolved <type>_schema ids, so "the fixtures fell back to bootstrap literals and wrote into a foreign schema" has a name in the log.
  • Finally gates on the bundle serving as JavaScript. A missing bundle does not 404 on Nextcloud — it returns HTTP 200 text/html, which every status-code check in the pipeline reads as success.

4. tests/e2e/workflows/fixtures.ts — the stat harness no longer assumes docker

runPhpHarness() ran CharacterService via docker exec … nextcloud with the bootstrap hardcoded to /var/www/html/lib/base.php. CI has no docker daemon and no container called nextcloud, so the harness returned null — which two tests treated as test.skip (reads as a pass) and the third as a failure blaming the stat arithmetic for an environment problem.

It now locates the Nextcloud root by walking up to lib/base.php + config/config.php and runs the bootstrap in-process (same PHP binary and config.php as the php -S instance under test), falling back to docker on a developer box.

The test.skip escape hatch is now gated off on CI: there a null harness result must fail, rather than silently turning the two highest-value correctness assertions in this repo into a green-looking no-op.

5. additional-apps

Added, pinned to ConductionNL/openregister@development. larpingapp is a thin client with no database tables of its own — every entity is an OpenRegister object, so without openregister there is nothing for the SPA to render or the fixtures to create. Pinned to development (not main) because that is the branch this app is developed against.

What was NOT done

  • No assertion weakened, no test skipped, no timeout raised, no error allow-list widened.
  • No spec deleted or rewritten.
  • enable-psalm: false left as-is — not part of this change.
  • enable-playwright-coverage left off.
  • Root playwright.config.ts, tests/e2e/docs-screenshots.spec.ts and tests/e2e/visual/** all left untouched.

🤖 Generated with Claude Code

The 10-file e2e suite under tests/e2e/ has existed and been maintained for
months, but `enable-playwright` was never set in the caller — so the shared
workflow reported "E2E Tests (Playwright)" as `skipped` on every run. A skipped
job looks exactly like a job with nothing to complain about, so the suite was
effectively dead while reading green.

Four things had to be true before the switch could be honest.

1. A CI-only config, tests/e2e/playwright.config.ts
   The shared workflow runs `npx playwright test --config=<path>` with NO
   `--project`, so every project in whichever config it picks runs. The root
   config declares three: `chromium`, `docs-capture` (re-shoots every
   documentation screenshot) and `visual` (whose committed PNG baselines are
   host-font/GPU specific and cannot byte-match a Linux runner — its own README
   says so). `playwright-test-path: tests/e2e` makes the workflow's FIRST config
   lookup hit the new file, which declares exactly one project. The root config
   is untouched: the separate "Journeydoc Capture" job greps it for
   `name: 'docs-capture'` and would break if that project were removed.

   `testIgnore` is spelled out at BOTH top level and project level, because a
   project-level `testIgnore` REPLACES the top-level one rather than merging.

2. tests/e2e/_base-url.ts now accepts BASE_URL
   That is the name the shared workflow actually exports, and it was the one
   spelling the resolver omitted. openconnector shipped a
   PLAYWRIGHT_BASE_URL-only resolver and its E2E job has hard-failed on every
   run since. The `localhost:8080` fallback is allowed ONLY when CI /
   GITHUB_ACTIONS is set (there it is the runner's own throwaway `php -S`); off
   CI it still throws, because :8080 on a developer box is the shared dev
   container.

3. tests/e2e/ci-seed.sh — explicit, forced, VERIFIED register import
   `occ app:enable larpingapp` imports the register from an IRepairStep, which
   runs with no user session, so OpenRegister's RBAC denies it — and the repair
   step swallows the exception as a warning, so `occ app:enable` still exits 0.
   The app enables cleanly and the register simply is not there.

   The seed posts to /apps/larpingapp/api/settings/reimport, which is
   loadSettings(force: true) and, unlike OpenRegister's generic importer, merges
   the lib/Settings/register.d/*.json fragments first. (larpingapp does NOT use
   `AppHost\Routes::standard()`, so there is no `settings#load` route here.) It
   then VERIFIES the register plus all 11 schema slugs, read out of the repo's
   own JSON rather than derived — three of them are prefixed (`larping_item`,
   `larping_event`, `larping_attendance`) precisely because the bare `item` slug
   collides with a foreign app's QTI schema.

   Finally it gates on the bundle SERVING as JavaScript. A missing bundle does
   not 404 on Nextcloud; it returns HTTP 200 text/html, which every status-code
   check in the pipeline reads as success.

4. The stat harness no longer assumes docker
   tests/e2e/workflows/fixtures.ts ran CharacterService via
   `docker exec … nextcloud` with the bootstrap hardcoded to
   /var/www/html/lib/base.php. CI has no docker daemon and no such container, so
   the harness returned null — which two tests treated as `test.skip` (reads as
   a pass) and the third as a failure blaming the arithmetic. It now finds the
   server root by walking up to `lib/base.php` + `config/config.php` and runs
   the bootstrap in-process, falling back to docker on a developer box.

   The `test.skip` escape hatch is now gated OFF on CI: there, a null harness
   result must fail rather than silently turn the two highest-value correctness
   assertions in this repo into a no-op.

Also adds `additional-apps` pinned to openregister@development. larpingapp is a
thin client with no tables of its own — without openregister there is nothing
for the SPA to render or the fixtures to create.

No assertion was weakened, no test skipped, and no timeout raised.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/larpingapp @ 81f06fe

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 101/101
npm ✅ 537/537
PHPUnit
Newman ⏭️
Playwright

Quality workflow — 2026-08-03 22:26 UTC

Download the full PDF report from the workflow artifacts.

…d the whole player schema

Found by the newly-enabled E2E job (run 30858375923), which is the first thing
in this repo to verify the register import on a CLEAN instance:

    [ci-seed] schemas present: [ability, character, condition, effect,
              larping_attendance, larping_event, larping_item, setting, skill, xpaward]
    ::error::LarpingApp schemas missing after import: ['player']

The cause, from the Nextcloud log in that same run:

    [ImportHandler] Failed to create schema (Pass 1) schemaKey=player
    Invalid string format 'user' at '/userUid'. Must be one of: , text, markdown,
    html, date-time, … , semver

`player.userUid` has carried `"format": "user"` since f434f76 (2026-07-08).
OpenRegister has never had a `user` format — see
OCA\OpenRegister\Service\Schemas\PropertyValidatorHandler::$validStringFormats —
and it rejects unknown formats at PASS 1, which drops the ENTIRE schema.

So for four weeks EVERY FRESH INSTALL of larpingapp has had no `player` schema:
no Players index, no player detail, no character→player link target. This is a
product bug, not a test-harness artefact.

What kept it invisible for four weeks is worth recording, because none of it is
specific to this property:

  * a dev instance already had a `player` schema created BEFORE the change, so
    the re-import failing changed nothing observable there;
  * the repair step catches \Throwable and downgrades it to a warning, so
    `occ app:enable larpingapp` still exits 0; and
  * `settings#reimport` returned HTTP 200 with `{"success": true}` while
    dropping the schema.

Three independent green signals over a schema that was not there.

Fix: `userUid` is a Nextcloud user UID — a plain string. The `format` is
removed, not translated: there is nothing in OpenRegister's vocabulary that
means "a Nextcloud user", and inventing one client-side is what broke it. The
schema version is bumped 1.1.0 → 1.1.1 so OpenRegister's version-gated import
re-applies it on instances that already carry the broken definition.

Regression gate: tests/validate-register.js now checks every string `format`
against a mirror of OpenRegister's allow-list, recursing into `properties`,
`items` and `$defs`, and covering the lib/Settings/register.d/*.json fragments
too (they are merged into the same import payload). Verified BOTH ways — it
passes on the fixed file, and re-injecting `"format": "user"` makes it exit 1
naming the property. A static check is the only thing that can catch this,
since a dev instance that already has the schema cannot.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/larpingapp @ 6e7369e

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 101/101
npm ✅ 537/537
PHPUnit
Newman ⏭️
Playwright

Quality workflow — 2026-08-04 05:16 UTC

Download the full PDF report from the workflow artifacts.

Baseline on this branch (run 30879501930, job 91897968947):
143 passed / 5 failed / 24 skipped in 7.4 min.

1. `character.ocName` is a RELATION, not a name  (product/schema vs fixtures)

   `lib/Settings/larpingapp_register.json` declares
   `ocName: {type: string, format: uuid, $ref: player}` and marks it
   `required` — "The player who plays this character". Two fixtures passed the
   character's own display name there, so every create returned
   HTTP 400 "Property 'ocName' should match format 'uuid'".

   * `workflows/fixtures.ts::seedStatScenario` — the direct cause of the three
     red character-stat specs.
   * `spec-coverage/detail-forms-admin.spec.ts::beforeAll` — this one failed
     SILENTLY: the seed helper returns null on error and the caller substituted
     the literal string `'seed-missing'`, so seventeen character-detail specs
     navigated to `#/characters/seed-missing` and still reported green.

   Both now seed a real `player` row first and reference its UUID.
   `x-allow-create: true` is a picker affordance for the Vue relation widget;
   it does not make the REST API accept a bare name.

   `ocName` was also being sent to player/ability/skill/item/condition/effect/
   event, which have no such property — dropped.

2. `gotoDetail()` asserted a heading that is never rendered  (test defect)

   A detail page renders the OBJECT's name as its `<h2>`; the entity type is a
   kicker paragraph above it. The old assertion — "some heading in .app-content
   matches /<type>/i" — passed for the wrong reasons:

   * it matched incidental widget titles ("Skills granting this effect"
     satisfies /Effect/i);
   * it matched the LIST heading ("Characters" satisfies /Character/i), which
     is exactly what kept the seventeen `seed-missing` specs green;
   * and it happened to hard-fail only on `events`, whose seeded name
     ("…-summer-larp") contains no occurrence of "event".

   It now asserts the seeded object's own name, which proves the hash route
   resolved AND the right object was fetched AND rendered. A missing seed is
   now a thrown error naming the type, not a vacuous pass.

3. Sidebar groups were collapsed on the dashboard  (product)

   `openspec/specs/dashboard/spec.md#sidebar-shows-all-entity-views` requires
   the sidebar to display Characters, Events, Players, Items, Conditions,
   Abilities, Skills and Effects. The manifest puts all eight inside three
   collapsible groups and `CnAppNav.isItemOpen()` falls back to
   `Boolean(item.open)` — default false — so on `/` none of the eight entries
   exist in the DOM at all. They appeared only on a route that is a child of
   the group (`hasActiveChild()`), which is why the same test ids assert fine
   from `/characters` and not from `/`.

   `"open": true` on CharactersGroup / MechanicsGroup / WorldGroup. The key is
   part of the v2 manifest menu schema; validated with Ajv against
   app-manifest-v2.schema.json 2.22.0 (PASS, and a deliberately injected bad
   key FAILs, so the validator is live).

Also: `retries: 0`. A retry can only turn red green, so it converts an
intermittent defect into a reported PASS, and it doubles the cost of the worst
case (a spec that fails by exhausting the 60 s timeout). The suite runs at
~37% of the 20 min cap, so there is room to see flake instead of hiding it.

No test skipped, no assertion weakened, no timeout raised, no allow-list
widened, no suppression added.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/larpingapp @ b3d9189

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 101/101
npm ✅ 537/537
PHPUnit
Newman ⏭️
Playwright

Quality workflow — 2026-08-04 08:13 UTC

Download the full PDF report from the workflow artifacts.

Round 1 of the enabled gate: 145 passed / 3 failed / 24 skipped in 8.3 min
(run 30890120431, job 91930353528). Two of the three are assertions that only
held while a defect was present, and went red the moment it was fixed.

1. character-stat-computation.workflow.spec.ts:164 — the last surviving
   `ocName: name`. This test builds its own character inline rather than going
   through seedStatScenario(), so the fixtures.ts fix did not reach it. Seeds a
   real `player` and references its UUID, same as the other two.

2. detail-forms-admin.spec.ts "character detail renders approval-capable
   shell" — asserted a second copy of the old type-heading check
   ("some heading matches /Character/i"). That was satisfied by the NOT-FOUND
   shell: when the object fails to load, CnDetailPage falls back to the
   manifest page title ("Character") as its heading. So the line passed
   precisely while the character seed was broken and stopped passing as soon as
   a real character loaded — exactly backwards. It now asserts the approval
   control the scenario ("approve a character") is actually about.

3. spa-ui.spec.ts "empty dashboard renders KPI 0 counts" — encoded the spec's
   "GIVEN no entities exist in the system" as toHaveText('0') on a shared
   instance that other specs in this suite seed into, without establishing the
   precondition. It passed only because the character seed was returning
   HTTP 400, so the count really was 0. Now reads the real total from
   OpenRegister (asserting the STATUS, not just the payload — a 403 body parses
   to {} and would become a silent "0") and asserts the KPI tile equals it,
   plus the empty-state row appears iff the collection is empty. Tiles are
   addressed by their manifest widget id via CnDashboardGrid's role=group
   aria-label instead of .first().

No test skipped, no assertion weakened, no timeout raised.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/larpingapp @ 7c68afc

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 101/101
npm ✅ 537/537
PHPUnit
Newman ⏭️
Playwright

Quality workflow — 2026-08-04 08:28 UTC

Download the full PDF report from the workflow artifacts.

…sted nothing

## Unpark (23 specs: skipped -> passing)

Their blocker annotation already ended with 'RESOLVED (verified 2026-08-01):
the cause is a MISSING player SCHEMA', and both halves of that are fixed on
this branch — 74fab69 restored the schema to the import, and the fixtures now
reference a real player UUID instead of a display name.

Measured before proposing it, on a throwaway PR cut from this branch's head
(#253, run 30892287689, job 91937270750): 171 passed / 0 failed / 1 skipped in
8.8 min. All 23 pass, including every per-object Actions-menu assertion.

The obsolete blocker docblock + its now-unreferenced DETAIL_ACTIONS_BLOCKER
const are replaced by a short record of what the cause actually was and how the
first two diagnoses were wrong.

## Two specs that asserted nothing (found by the truncation control)

The negative control (#251, job 91930520433) truncated every js/*.js to 0 bytes
and ran the suite unchanged: 118 of the 122 specs that got a verdict FAILED.
Four passed with no JavaScript at all. Two are legitimate backend probes
(event-runsheet-export, skill-requirement-enforcement — HTTP endpoint checks
that correctly need no SPA). The other two were dead:

* 'settings-management-ui > admin settings panel loads' asserted
  '.app-content, #app-content, .section' is visible — Nextcloud's OWN
  server-rendered settings chrome, present whether or not larpingapp mounts —
  plus a URL check that a goto cannot fail.
* 'admin-settings > admin opens larpingapp settings panel' asserted
  expect(page.locator('body')).toBeVisible(), true on any page that loads, plus
  the same tautological URL check.

Both now assert the app's own Vue admin panel (its heading and the 'Save All'
control), which exists only if larpingapp's bundle loaded and mounted.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/larpingapp @ 46e1ac3

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 101/101
npm ✅ 537/537
PHPUnit
Newman ⏭️
Playwright

Quality workflow — 2026-08-04 09:07 UTC

Download the full PDF report from the workflow artifacts.

…lity

E2E job 91942310154 (170 passed / 1 failed / 1 skipped, 10.2 min) lost
'character list renders view toggle and add controls' to a full 60 s timeout.
The log is unambiguous: the sidebar link was resolved, 'visible, enabled and
stable' on every one of 114 retry passes, and every click was intercepted by

  <div class="cn-walkthrough__dim cn-walkthrough__dim--full">
    from <div role="dialog" aria-modal="true" class="cn-walkthrough"
              aria-label="Welcome to LARPing">

i.e. the six-step first-visit onboarding tour declared in src/manifest.json,
whose dim layer covers the viewport and swallows pointer events.

Two changes, one cause:

1. ci-seed.sh now marks the tour as already seen for the CI admin user, by
   PUTting the manifest's own `walkthrough.completionConfigKey`
   (`walkthrough_completed_version`) — the store `useWalkthrough` treats as
   AUTHORITATIVE — with a version above every step's `sinceVersion`. It then
   READS IT BACK and fails the seed if it did not stick: a 200 alone proves
   nothing here, because an app not serving `/api/preferences/{key}` answers
   200 with the SPA's HTML, which useWalkthrough reads as 'no opinion' and
   which would leave the tour armed while the step looked successful.

   This suppresses a first-run affordance, not behaviour under test: no spec in
   tests/e2e asserts the walkthrough — every mention of it is a comment about
   fighting it.

2. dismissSupportDialog() now verifies its own postcondition. Every click in it
   is `.catch(() => {})`, so it returned 'successfully' with the overlay still
   up, and the caller's next click then burned 60 s and reported the failure
   against whatever element it was aiming at. It now throws with the actual
   cause named. Scoped to `.cn-walkthrough__dim` specifically rather than to
   every aria-modal dialog, so it cannot start failing on modals that are open
   but not blocking.

No timeout raised, no retry added, no assertion weakened.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/larpingapp @ 36b4c5e

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 101/101
npm ✅ 537/537
PHPUnit
Newman ⏭️
Playwright

Quality workflow — 2026-08-04 09:24 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 7bd47b6 into development Aug 4, 2026
34 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/enable-e2e-playwright branch August 4, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant