test(e2e): gate-19 round 3 — 143 → 128 with 14 real tests and zero exclusions, plus the quick-search fix (#95) - #99
Merged
Conversation
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.
Contributor
Quality Report — ConductionNL/launchpad @
|
| 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.
Contributor
Quality Report — ConductionNL/launchpad @
|
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
+14 is exactly the number of
@e2eanchors in that file, and all fourteen reappear inconditional-visibility-editor.Every number, and the scope it came from
All measured with the canonical
hydra-gates/scripts/lib/check_e2e_coverage.pyfrom a fresh clone ofConductionNL/.github, unscoped, from the app directory.8bb18844(pre-#92)66957c17=origin/development(post-#92)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 says176 → 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 → 143on removing and restoringtests/e2e/ci/dashboard-share-api.spec.ts.Exclusion accounting, per
.github#356launchpad's spec corpus carries 264
@e2e excludeannotations 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.replacedoes not exist, so every activation threw aTypeErrorinside a Vue event handler — where nothing surfaces it — and Enter silently did nothing.applySearchDimming()comparedgetAttribute('data-placement-id')against the raw ids withArray.includes, which uses SameValueZero.[7].includes('7')isfalse, so every tile dimmed on every query, including the matches.Why the unit suite was green throughout: all five existing fixtures in
WorkspaceApp.spec.jsseed 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 theTypeErrorverbatim. Those integer-id regressions are added.The
dimmed > 0assertion 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
isInclude: falseis aboolproperty boundPARAM_BOOLinto asmallintcolumn, soPOSTandPUTboth answer 400 on PostgreSQL — the shared workflow's default database. Identical body withtrue→ 201. Also: the controller reports it throughResponseHelper::error()without a logger, so nothing reachesnextcloud.log; the error path is undiagnosable from either end.Views.vuenever passes:available-groups, so both the rule operand and the preview-audience picker report "No results", and neither is taggable.checkRulesForPlacement()has exactly one production caller —RuleApiController::getRules(), the editor's own read — andisWidgetVisible(), 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 atisVisible: 1; and by a caller search taken with a positive control (the same grep findspreviewRules'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#345the gate reads an@e2e excludeas 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.add-a-group-inclusion-rule-through-the-ui,group-row-operands, and the threepreview-as-audiencescenariosinclude-rules-grouped-under-an-or-headingneeds a stored exclude ruleeditor-does-not-change-evaluation-semantics,preview-verdict-matches-render-time-verdict-for-identical-inputsendpoint-delegates-to-the-shared-evaluation-serviceis a claim about which PHP class handles the requestincludeexclude-toggleis 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
NcCheckboxRadioSwitchhas no<label>. Thedata-testlands on a visually hidden<input>, and the natural fallbacklabel[for=…]matches nothing — the association isaria-labelledbyonly. The clickable element is#${inputId}-label. Both wrong locators fail as a 10s timeout that reads like a missing or disabled control.force: trueis 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, nogrepInvert, no raised timeouts, no widenedtestIgnore, nomode: 'serial', nowaitForLoadState('networkidle'), no baselines. Nosed/awk/scripted edits to code. NoCo-Authored-Bytrailers. 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.