Skip to content

test(e2e): gate-19 round 3 — 143 → 128 with 14 real tests and zero exclusions, plus the quick-search fix (#95) - #99

Merged
rubenvdlinde merged 3 commits into
developmentfrom
fix/gate-19-round-3
Aug 11, 2026
Merged

test(e2e): gate-19 round 3 — 143 → 128 with 14 real tests and zero exclusions, plus the quick-search fix (#95)#99
rubenvdlinde merged 3 commits into
developmentfrom
fix/gate-19-round-3

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

gate-19 round 3 — launchpad: 143 → 128, all real tests, zero exclusions added

Lead with the negative control, because the gate cell on this PR is not evidence

Gate-19 is diff-scoped. This branch changes no spec file, so its gate-19 scope here is empty and a PASS on this PR would be a pass over nothing. The evidence is the control, which is scope-independent — run unscoped from the app dir, the same invocation the orchestrator makes:

with tests/e2e/conditional-visibility-editor.spec.ts    128
file removed                                            142   (+14)
restored                                                128

+14 is exactly the number of @e2e anchors in that file, and all fourteen reappear in conditional-visibility-editor.

Every number, and the scope it came from

All measured with the canonical hydra-gates/scripts/lib/check_e2e_coverage.py from a fresh clone of ConductionNL/.github, unscoped, from the app directory.

tree scope gate-19
8bb18844 (pre-#92) unscoped 176
66957c17 = origin/development (post-#92) unscoped 143
this branch unscoped 128

The 143 was taken twice — once in my working worktree and once in a pristine detached checkout of origin/development, to rule out working-tree contamination. Both 143.

Round 2's disputed number, resolved

PR #92's title says 176 → 151; its body says 176 → 143. The body is right and the title is stale — it was written mid-branch and never updated. Independently confirmed by measuring both SHAs above. Round 2's own negative control also reproduces exactly on the committed tree: 143 → 154 → 143 on removing and restoring tests/e2e/ci/dashboard-share-api.spec.ts.

Exclusion accounting, per .github#356

launchpad's spec corpus carries 264 @e2e exclude annotations across 241 distinct reasons. The sibling-exemption problem is therefore mild here, unlike openbuild's 92.5% — the largest single reason covers 21 scenarios ("pure backend — all scenarios are PHP/service/API/data-layer; no UI surface"), and 23 annotations in total share a reason.

This branch adds none of them. No spec file is touched.

What landed

1. #95 — quick-search: Enter opens the tile, and only non-matches dim (143 → 142)

Two shipped defects, one root cause: a placement id is an integer and a DOM attribute is a string.

  • activateSearchResult() called .replace() on the id. Number.prototype.replace does not exist, so every activation threw a TypeError inside a Vue event handler — where nothing surfaces it — and Enter silently did nothing.
  • applySearchDimming() compared getAttribute('data-placement-id') against the raw ids with Array.includes, which uses SameValueZero. [7].includes('7') is false, so every tile dimmed on every query, including the matches.

Why the unit suite was green throughout: all five existing fixtures in WorkspaceApp.spec.js seed the id as a string ('p1'), a type the API never sends. Re-run with integer ids against the unfixed source, the same assertions fail twice — one with the TypeError verbatim. Those integer-id regressions are added.

The dimmed > 0 assertion round 2 left live passing for the wrong reason lands here with its fix, as it was disclosed it must. It is now per-element and by identity — the non-matching tile carries the class and the matching one does not — with the aggregate count kept as a control so a refactor that stops applying the class at all cannot pass.

2. conditional-visibility-editor — 14 scenarios (142 → 128)

13 tests, 14 anchors. Operand scenarios assert the request body, not the re-rendered row: the requirement is about the canonical shape on the wire, and a purely local component could fake the row.

Three defects found while writing the tests — filed, not papered over

#96 No EXCLUDE rule can be created at all. isInclude: false is a bool property bound PARAM_BOOL into a smallint column, so POST and PUT both answer 400 on PostgreSQL — the shared workflow's default database. Identical body with true → 201. Also: the controller reports it through ResponseHelper::error() without a logger, so nothing reaches nextcloud.log; the error path is undiagnosable from either end.
#97 The group picker is always empty. Views.vue never passes :available-groups, so both the rule operand and the preview-audience picker report "No results", and neither is taggable.
#98 Conditional visibility is NEVER ENFORCED. checkRulesForPlacement() has exactly one production caller — RuleApiController::getRules(), the editor's own read — and isWidgetVisible(), the render-time entry point REQ-CVUI-005 names, does not exist. The editor previews a rule as Hidden and the dashboard renders the widget anyway.

#98 was proven three ways before filing: in the browser; by GET /api/dashboard/{id} returning 200 with the ruled-out placement still at isVisible: 1; and by a caller search taken with a positive control (the same grep finds previewRules's caller immediately). Two docblocks describe "render-time callers (ConditionalService::checkRulesForPlacement())" — a caller that was never written, which is probably how it survived review.

Nine scenarios left uncovered, and deliberately NOT excluded

Every one is browser-observable; what a browser observes is that the feature does not work. Per .github#345 the gate reads an @e2e exclude as positive coverage, so excluding them would have bought nine findings with a false statement. Each is recorded in the file where its test would sit, with its evidence.

includeexclude-toggle is covered, narrowly: that requirement is about what the row emits and where it moves, both observable before the request leaves the browser. It deliberately does not assert persistence — that would be asserting #96 is fixed.

Two traps worth carrying to other repos

NcCheckboxRadioSwitch has no <label>. The data-test lands on a visually hidden <input>, and the natural fallback label[for=…] matches nothing — the association is aria-labelledby only. The clickable element is #${inputId}-label. Both wrong locators fail as a 10s timeout that reads like a missing or disabled control. force: true is not used: it dispatches at an element no user can reach, so the test would stay green if the control became genuinely unreachable.

A non-matching INCLUDE rule hides the tile whose editor you then need to reopen. Include is OR, so a placement with no matching include rule is correctly hidden — and a hidden tile cannot be right-clicked, so the fixture locks itself out of the surface under test. Every non-matching rule is seeded alongside one that keeps the subject reachable.

Constraints held

No test.skip / describe.skip, no deleted or weakened assertions, no grepInvert, no raised timeouts, no widened testIgnore, no mode: 'serial', no waitForLoadState('networkidle'), no baselines. No sed/awk/scripted edits to code. No Co-Authored-By trailers. Every test was shown to be able to fail: the two unit regressions were run red against the unfixed source and restored byte-identically, and each e2e assertion carries its own control.

Closes #95.

Conduction Release Bot added 2 commits August 11, 2026 15:22
Two shipped defects, one root cause: a WidgetPlacement id is an integer and
a DOM attribute is a string.

  * `activateSearchResult()` called `.replace()` on the id.
    `Number.prototype.replace` does not exist, so every activation threw a
    TypeError inside a Vue event handler — where nothing surfaces it — and
    pressing Enter on a search result silently did nothing. The truthiness
    guard above it could not help: a non-zero integer is truthy.
  * `applySearchDimming()` compared `getAttribute('data-placement-id')`
    against the raw ids with `Array.includes`, which uses SameValueZero and
    does not coerce. `[7].includes('7')` is false, so EVERY tile dimmed on
    every query, including the matches the user was searching for.

Why the unit suite was green throughout: all five existing fixtures in
WorkspaceApp.spec.js seed a placement id as a STRING ('p1', 'match'), a
type the API never sends. Re-running the same assertions with integer ids
against the unfixed source fails twice, one of them with the TypeError
verbatim. Those integer-id regressions are added here.

The e2e side lands with the fix rather than before or after it:

  * REQ-QSEARCH-003 enter-opens-the-selected-tile now has a real test. It
    records the anchor's href from a capture-phase click listener with
    preventDefault, because the seeded links point at example.invalid and a
    real navigation would fail for a network reason instead of a product
    one. The two splitting probes from the investigation are kept, so a
    regression names its own cause.
  * The filtering test's dimming assertion was 'dimmed > 0', which 'every
    tile is dimmed' satisfies — it was passing for the wrong reason, and
    was disclosed as such rather than quietly tightened, because tightening
    it before the fix would simply have been red. It is now per-element and
    by identity: the non-matching tile carries the class and the matching
    one does not, with the aggregate count kept as a control so a refactor
    that stops applying the class at all cannot pass.

No @e2e exclude was added while the feature was broken. Per .github#345 the
gate reads an exclusion as positive coverage, so excluding a scenario the
browser can plainly observe buys a green with a false statement.

gate-19: 143 -> 142. Verified against a local NC fixture: 9/9 Playwright,
26/26 vitest.

Closes #95
… three defects doing it

gate-19: 142 -> 128, all of it real tests. Zero @e2e exclusions added, and no
spec file touched.

Negative control, same tree, one command apart:
  with the file     128
  file removed      142   (+14, exactly its anchor count, all in this capability)
  restored          128

Verified against a local NC 34 + PostgreSQL 16 fixture: 13/13 Playwright.
PostgreSQL matters here — it is the shared workflow's default database, and
two of the three defects below are invisible on SQLite and MySQL.

WHAT IS COVERED (14 of the capability's 23 scenarios)

  REQ-CVUI-001  the section loads and renders stored rules; edit sends
                PUT /api/rules/{id} with the updated ruleConfig; remove sends
                DELETE and the row goes.
  REQ-CVUI-002  time / date / attribute operands, asserted on the REQUEST BODY
                rather than on the re-rendered row — the requirement is about
                the canonical shape on the wire, and a purely local component
                could fake the row. Includes the open-ended date range, where
                the point is that `endDate` is ABSENT, not empty.
  REQ-CVUI-003  the include/exclude distinction survives every colour in the
                document being overridden to one value; the empty state, with
                a control proving it disappears when a rule exists.
  REQ-CVUI-004/5 preview evaluates an unsaved row and persists nothing (with a
                positive control, because "no rules stored" is what a broken
                editor also produces); preview and save send byte-identical
                ruleConfig; an unknown ruleType is refused 400 while a valid
                one is accepted; an anonymous caller is refused while the same
                request with credentials succeeds.

THREE DEFECTS FOUND, ALL FILED, NONE PAPERED OVER

  #96  No EXCLUDE rule can be created at all. `isInclude: false` is a boolean
       bound into a `smallint` column, so POST and PUT both answer 400 on
       PostgreSQL. The controller reports it via ResponseHelper::error()
       WITHOUT a logger, so nothing reaches nextcloud.log either.
  #97  The group picker is always empty — Views.vue never passes
       :available-groups — so group rules cannot be authored or previewed.
  #98  Conditional visibility is NEVER ENFORCED. checkRulesForPlacement() has
       one production caller and it is the editor's own read; isWidgetVisible()
       does not exist. Preview says Hidden and the dashboard renders the widget
       anyway. Proven three ways: browser, a 200 from GET /api/dashboard/{id}
       still carrying the placement at isVisible:1, and a caller search taken
       with a positive control.

NINE SCENARIOS LEFT UNCOVERED AND DELIBERATELY NOT EXCLUDED

Every one of them is browser-observable; what a browser observes is that the
feature does not work. Per .github#345 the gate scores an `@e2e exclude` as
POSITIVE coverage, so excluding them would have bought nine findings with a
false statement. Each is recorded in the file where its test would sit, with
the evidence, so the next person does not rediscover it.

`includeexclude-toggle` IS covered, narrowly: that requirement is about what
the row EMITS and where it MOVES, both observable before the request leaves
the browser. It deliberately does not assert persistence, which would be
asserting #96 is fixed.

TWO TRAPS WORTH CARRYING ELSEWHERE, both recorded in the file

  * NcCheckboxRadioSwitch has NO <label>. The data-test lands on a visually
    hidden <input>, and `label[for=...]` matches nothing — association is
    aria-labelledby only. The clickable element is `#${inputId}-label`. Both
    wrong locators fail as a 10s timeout that reads like a missing control.
    `force: true` is not used: it would dispatch at an element no user can
    reach and stay green if the control became unreachable.
  * A non-matching INCLUDE rule hides the tile whose editor you then need to
    reopen, so the fixture locks itself out. Every non-matching rule is seeded
    alongside one that keeps the subject reachable.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ 9657ad7

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
composer ✅ 102/102
npm ✅ 548/548
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-11 15:19 UTC

Download the full PDF report from the workflow artifacts.

…ec (gate-16)

CI's Hydra Gates job reported `[gate-16] spec-coverage: FAIL — 2 changed
method(s) missing @spec`, and it is correct: gate-16 is diff-scoped, and the
previous commit changed `applySearchDimming()` and `activateSearchResult()`,
neither of which carried an anchor. Reproduced locally with the canonical
checker, invoked the way the runner invokes it:

  HYDRA_GATE_BASE_REF=origin/development check_spec_coverage.py .
    src/views/WorkspaceApp.vue::activateSearchResult — missing @SPEC
    src/views/WorkspaceApp.vue::applySearchDimming — missing @SPEC
    # count=2

Both get a real anchor, not an `@spec exclude`. They are the two halves of
REQ-QSEARCH-002 (dim non-matching tiles) and REQ-QSEARCH-003 (Enter opens the
selected tile) — the DOM bridge the sibling methods in this file already point
at the same canonical spec for. The target is `openspec/specs/…`, never a
change directory, and the file was confirmed to exist: a dangling anchor
resolves to nothing and gate-19 does not report that as an error.

The JSDoc `@param` types are corrected in the same edit, because they were part
of how launchpad#95 hid: both said the ids were strings. They are integers.

After: gate-16 count=0, gate-19 unchanged at 128, vitest 26/26.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ 181ebff

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
composer ✅ 102/102
npm ✅ 548/548
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-11 16:41 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit e24334b into development Aug 11, 2026
27 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/gate-19-round-3 branch August 11, 2026 17:10
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