fix(e2e): four defects behind eight failing specs, and two tests asserting surfaces that do not exist - #457
fix(e2e): four defects behind eight failing specs, and two tests asserting surfaces that do not exist#457rubenvdlinde wants to merge 2 commits into
Conversation
…rting surfaces that do not exist
NcSelect: THE PROP AND THE EVENT WERE BOTH GONE (4 components)
decidesk locks `@nextcloud/vue ^9.9.0`. In 9.9.0 NcSelect declares:
modelValue: { type: [String, Number, Object, Array], default: null },
emits: [" ", "update:modelValue"]
There is NO `value` prop and `input` is not in the emits list. Four components
still bound the v8 pair `:value` + `@input`, so the selection never reached the
component and the component's choice never reached the app. In
MeetingTranscriptionTab that is the failing test exactly: `selectedSource` stayed
null, so `:disabled="!selectedSource"` kept "Attach recording" disabled forever.
ActionItemDeckBoard (lane moves), GovernanceBodyRetentionTab (policy picker) and
one `@update:value` in the evaluations tab were silently broken the same way with
no test watching. All four move to `:model-value` / `@update:model-value`.
⚠️ This was checked against the WRONG VERSION first and "disproved". The shared
checkout's node_modules carries @nextcloud/vue 8.39.0, whose NcSelect does still
emit `input` — but package-lock.json pins 9.9.0, which is what CI installs. An
API check is only as good as the version it reads: verify against the LOCK, not
against whatever happens to be installed.
BOARD EVALUATION: THE RIGHT MEMBER, THE WRONG BODY (3 specs)
The card stayed at "0 of 3 responded" after a member submitted. Identity is
per-body, not per-person: a member serving on two boards has two Participant
objects, and the UID -> Participant lookup was unscoped, returning whichever the
store listed first. submitResponse() then compared an identity from one body
against the invited roster of another and rejected a genuinely invited member —
and the rejection reads as a legitimate "not invited to this cycle", so nothing
surfaced. The lookup is now scoped to the evaluation's own governance body; the
roster check remains the gate.
PUBLICATION: A SELECTOR THAT COULD NEVER RESOLVE
`getByTestId('publication-withdraw-reason').locator('textarea')` descends into an
NcTextArea that merges $attrs onto the inner <textarea> — the testid IS the
textarea, so there is nothing to descend into. `resolution-minutes.spec.ts:183`
already drives the identical NcTextArea construct with a direct `.fill()`, and
that spec passes. Matched to it.
TWO TESTS ASSERTED A SURFACE THAT DOES NOT EXIST — AND MUST NOT
`decision-management` clicked `getByRole('tab', { name: 'Lifecycle' })` and
`{ name: 'Voting results' }`. Neither is a tab. DecisionDetail's manifest lays
`decision-lifecycle` and `decision-voting` out as BODY WIDGETS, and its sidebar
declares exactly one tab: `audit`, labelled "History". So the click could never
have matched.
The tempting "fix" is to add `role="tab"` to those widget headers. That would be
a WCAG 4.1.2 regression — a tab role without a tablist and tabpanel — i.e. making
the gate green by breaking the thing it protects. Instead the tests assert the
widget the user actually lands on. Every substantive assertion below is
UNCHANGED: seven lifecycle states in machine order, exactly one current-state
marker, transition actions or the explicit empty notice, and the voting tab's
rounds/table/no-motion notice.
CRUD DELETE: ASSERT THE PHASE, DO NOT WAIT OUT A TIMER
CnDeleteDialog dwells on success — it shows "Item successfully deleted." and only
then arms a 2 s auto-close. The test sat out that timer inside a 20 s budget.
It now asserts the success phase explicitly
(`[data-testid-phase="result"]` + the success text — STRONGER than "something
vanished": a dialog stuck in `phase=confirm` or showing an error now fails here,
naming the phase) and dismisses via the Close button, as a user would. The
duplicate `gotoList` before the decision delete is dropped: the assertion above
it already leaves the browser on that list, and the reload was a second full page
load (~3.7 s measured) in a 20 s budget — which is where the test actually ran
out of clock.
No test is deleted, skipped, or given a longer timeout. No assertion is loosened.
FALSIFIABLE CLAIM FOR CI: transcription:220, publication:109,
decision-management:121 and :159, board-evaluation x3 and crud-persistence:202
flip. That is 8 of the 11, leaving integration-registry (upstream
openregister#2415), motion-amendment:34 and crud-persistence:318 — so 11 failed
-> 3 failed, with skips held at 59.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-11 10:28 UTC
Download the full PDF report from the workflow artifacts.
CI's PHPUnit job failed on this branch with no failing test:
Tests: 913, Assertions: 3215, Skipped: 37
Coverage current: 60.29% (9173/15214 statements)
Coverage merge base: 60.44% (9170/15172 statements)
FAIL: coverage dropped by 0.15% against the merge base.
This change adds 42 statements. Adding code without tests drops coverage.
A green test run under a red job is the coverage ratchet's signature, and it was
correct: the fix introduced ParticipantUuidLookup and
BoardEvaluationResponseService::resolveResponder and tested neither. The
behaviour was proven end to end by Playwright and not at all by a unit test,
which is exactly the gap the ratchet exists to catch.
Twelve tests, no threshold touched, no baseline edited:
ParticipantUuidLookupTest (7) — unscoped resolution, the null and `id`-fallback
shapes, and the defect itself: ONE user, TWO boards, TWO participant rows, and
the answer must depend on the body asked about. Also that belonging to no such
body resolves to null rather than to somebody else's identity, and that empty
inputs short-circuit before the store is touched.
BoardEvaluationResponderScopeTest (5) — the evaluation's `governanceBody` scopes
the lookup; the `@self.relations` form is read too; no body on the evaluation
degrades to the unscoped lookup; a store failure degrades rather than throwing
(the roster check in submitResponse() is still the gate); empty inputs never
query.
BOTH DIRECTIONS PROVEN. Disabling the body filter in
`forNextcloudUserInBody` — the exact shape of the original bug — turns three of
these red, the key one returning `participant-on-board-1` where the identity for
board 2 was asked for:
1) testForNextcloudUserInBodyPicksTheIdentityForThatBody
Failed asserting that two strings are identical.
3) testForNextcloudUserInBodyReturnsNullWhenNotOnThatBody
Failed asserting that 'participant-on-board-1' is null.
Reverted after measuring.
Suite: 913 -> 925 tests, 3215 -> 3230 assertions, 0 failures, skips held at 37.
phpcs on lib/ (CI's scope) unchanged at 0 errors / 105 warnings.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-11 11:05 UTC
Download the full PDF report from the workflow artifacts.
|
Superseded by #458, which is merged. Every commit from this branch is included there unchanged — the diagnosis was independently reproduced and these fixes were better than the ones I had written, so they are kept as authored. What #458 added on top: the gate-16 finding that kept this PR red ( One correction worth recording here rather than in the merge. The board-evaluation product fix on this branch — the body-scoped |
NcSelect: THE PROP AND THE EVENT WERE BOTH GONE (4 components)
decidesk locks
@nextcloud/vue ^9.9.0. In 9.9.0 NcSelect declares:There is NO
valueprop andinputis not in the emits list. Four componentsstill bound the v8 pair
:value+@input, so the selection never reached thecomponent and the component's choice never reached the app. In
MeetingTranscriptionTab that is the failing test exactly:
selectedSourcestayednull, so
:disabled="!selectedSource"kept "Attach recording" disabled forever.ActionItemDeckBoard (lane moves), GovernanceBodyRetentionTab (policy picker) and
one
@update:valuein the evaluations tab were silently broken the same way withno test watching. All four move to
:model-value/@update:model-value.checkout's node_modules carries @nextcloud/vue 8.39.0, whose NcSelect does still
emit
input— but package-lock.json pins 9.9.0, which is what CI installs. AnAPI check is only as good as the version it reads: verify against the LOCK, not
against whatever happens to be installed.
BOARD EVALUATION: THE RIGHT MEMBER, THE WRONG BODY (3 specs)
The card stayed at "0 of 3 responded" after a member submitted. Identity is
per-body, not per-person: a member serving on two boards has two Participant
objects, and the UID -> Participant lookup was unscoped, returning whichever the
store listed first. submitResponse() then compared an identity from one body
against the invited roster of another and rejected a genuinely invited member —
and the rejection reads as a legitimate "not invited to this cycle", so nothing
surfaced. The lookup is now scoped to the evaluation's own governance body; the
roster check remains the gate.
PUBLICATION: A SELECTOR THAT COULD NEVER RESOLVE
getByTestId('publication-withdraw-reason').locator('textarea')descends into anNcTextArea that merges $attrs onto the inner <textarea> — the testid IS the
textarea, so there is nothing to descend into.
resolution-minutes.spec.ts:183already drives the identical NcTextArea construct with a direct
.fill(), andthat spec passes. Matched to it.
TWO TESTS ASSERTED A SURFACE THAT DOES NOT EXIST — AND MUST NOT
decision-managementclickedgetByRole('tab', { name: 'Lifecycle' })and{ name: 'Voting results' }. Neither is a tab. DecisionDetail's manifest laysdecision-lifecycleanddecision-votingout as BODY WIDGETS, and its sidebardeclares exactly one tab:
audit, labelled "History". So the click could neverhave matched.
The tempting "fix" is to add
role="tab"to those widget headers. That would bea WCAG 4.1.2 regression — a tab role without a tablist and tabpanel — i.e. making
the gate green by breaking the thing it protects. Instead the tests assert the
widget the user actually lands on. Every substantive assertion below is
UNCHANGED: seven lifecycle states in machine order, exactly one current-state
marker, transition actions or the explicit empty notice, and the voting tab's
rounds/table/no-motion notice.
CRUD DELETE: ASSERT THE PHASE, DO NOT WAIT OUT A TIMER
CnDeleteDialog dwells on success — it shows "Item successfully deleted." and only
then arms a 2 s auto-close. The test sat out that timer inside a 20 s budget.
It now asserts the success phase explicitly
(
[data-testid-phase="result"]+ the success text — STRONGER than "somethingvanished": a dialog stuck in
phase=confirmor showing an error now fails here,naming the phase) and dismisses via the Close button, as a user would. The
duplicate
gotoListbefore the decision delete is dropped: the assertion aboveit already leaves the browser on that list, and the reload was a second full page
load (~3.7 s measured) in a 20 s budget — which is where the test actually ran
out of clock.
No test is deleted, skipped, or given a longer timeout. No assertion is loosened.
FALSIFIABLE CLAIM FOR CI: transcription:220, publication:109,
decision-management:121 and :159, board-evaluation x3 and crud-persistence:202
flip. That is 8 of the 11, leaving integration-registry (upstream
openregister#2415), motion-amendment:34 and crud-persistence:318 — so 11 failed
-> 3 failed, with skips held at 59.
PHP suite
The controller drops its
VotingServicedependency, soBoardEvaluationControllerTest(added in #456) had to move its resolution stubs ontoBoardEvaluationResponseService::resolveResponder. Without that co-change the suite errored six times — which is how I found it:Skips held at 37 and assertions went UP by 12 — the six tests that were erroring now actually run and assert. No test was deleted, skipped or loosened.
phpcsonlib/(CI's scope) is byte-identical before and after: 0 errors, 105 warnings either way.🤖 Generated with Claude Code