Skip to content

a11y(inbox): label-first accessible names on the capture composer after moving its test selectors to testids - #2700

Merged
Chris0Jeky merged 3 commits into
mainfrom
issue-1871/composer-names
Sep 5, 2026
Merged

a11y(inbox): label-first accessible names on the capture composer after moving its test selectors to testids#2700
Chris0Jeky merged 3 commits into
mainfrom
issue-1871/composer-names

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

The Paper capture composer's four accessible names now lead with the field's visible eyebrow and then say what the control does, the WCAG 2.5.3 shape the board controls took in PR #2675 and the triage board-pick took in PR #2696. Two of the four failed label-in-name outright before this: Add label did not contain the visible Labels, and Due date did not contain the visible Due (optional).

PR #2696 could not make that change. PaperInboxView.spec.ts and three Playwright specs selected these controls by their accessible name, which made the English text test API rather than copy. So this PR migrates those selectors to data-testid attributes first, proves the unchanged suites still pass, and only then rewrites the names in en, es and it.

The board select's new name says the capture is linked to the chosen board for triage. It deliberately does not say the capture lands there. Every capture lands in Inbox, the board choice links it so triage can propose against that board, and nothing reaches the board without approve and execute (ADR-0003). The composer's own footer and inbox.nib.destinationWithBoard already say exactly that.

The PR also closes the two LOWs the fresh-context review of PR #2696 recorded on the issue: the it and es composer docblocks claimed a placeholder rule that bodyPlaceholder breaks by design, and the Italian locale cases set the locale before mounting, so they covered first render only.

Refs #1871. This is the last item of the board-picker half. The review-i18n half (items 1 to 4 of the issue body) and the bare English outside the coordinator's residual list stay open.
Refs PR #2696 (the extraction this follows), PR #2675 (the label-first pattern).

Changes

Commit 1, 909c5f146 - selector migration, no behaviour and no copy change.

  • PaperCaptureComposer.vue: data-testid on the four fields, in the file's existing paper-composer-* naming: paper-composer-body (textarea), paper-composer-board (select), paper-composer-label-input (label input), paper-composer-due (due input, reaching the <input> through TdDateField's v-bind="$attrs").
  • PaperInboxView.spec.ts: 47 name selectors moved onto those testids.
  • capture-loop.spec.ts, first-run.spec.ts, review-proposals.spec.ts: getByRole('textbox', { name: 'Capture body' }) becomes getByTestId('paper-composer-body'), one line each. No E2E spec depended on the name for anything but selection.
  • PaperCaptureComposer.spec.ts: the selection selectors move too, including the fieldChrome helper. The assertions ON the names stay, unchanged in this commit.
  • routeAffordanceInventory.ts: the inbox.capture-submit source pin moves from PaperCaptureComposer.vue:369 to :373, the four added attribute lines having pushed the Capture button down. The E2E line pins (capture-loop.spec.ts:55, first-run.spec.ts:49, review-proposals.spec.ts:75, :88, :97) are unaffected: each spec had exactly one line replaced in place, verified by reading those lines after the edit.

Commit 2, 6a01c43b7 - the label-first names.

  • inbox.composer.bodyAria: Capture body becomes Body: write the text of this capture.
  • inbox.composer.labelsAria: Add label becomes Labels: type a label and press Enter to add it.
  • inbox.composer.dueAria: Due date becomes Due (optional): set a due date for this capture.
  • inbox.boardPicker.composerAria: Board picker becomes a label-first form, superseded in commit 3 below.
  • es and it rewritten to the same shape. The it and es composer docblocks now mirror the English placeholder rule (a hint about the shape of the value, never the field's name) and state that bodyPlaceholder is a capitalized sentence by design while labelsPlaceholder is a lowercase fragment. The previous wording said the placeholder stays lowercase, which is true of labelsPlaceholder only and invited a native reviewer under Seed an i18n translation layer (vue-i18n) with Italian and Spanish locales #1770 to lowercase the body one.
  • No other key changed. Placeholders, eyebrows, attachmentsUnavailable, triageAria and the boardPick copy are untouched.
  • PaperCaptureComposer.spec.ts: the English and Italian chrome cases assert the new names; a new case asserts the label-first RELATION (every accessible name must start with the eyebrow rendered above its control) so a future reword cannot silently break the rule; a new post-mount locale switch mounts in English, switches to Italian, awaits nextTick and asserts the eyebrows and bodyAria changed.
  • PaperTriageTable.spec.ts: the same post-mount switch case for the region name and the board-pick chrome. Nothing else in that file changed.

Commit 3, a563d4460 - review round 2: two MEDIUMs and one LOW.

Product truth of the board name. Board: choose where this new capture will land contradicted the surface it sits on, because nothing lands on a board from the composer. All three locales take the linking form, still label-first:

locale now
en Board: choose which board this capture is linked to for triage
es Tablero: elige a qué tablero se vincula esta captura para el triage
it Bacheca: scegli a quale bacheca collegare questa cattura per il triage

The es and it wording reuses each catalog's own triage vocabulary from the sibling nib.destinationWithBoard (vinculada a ... para el triage, collegata a ... per il triage), so the composer name and the Nib destination line now say the same thing in the same words. inbox.boardPicker.triageAria is NOT touched in any locale: the two names still differ only in their second half, and the docblocks now record why the composer's may not use the word "land".

Locale coverage of the label-first guard. The es and it docblocks claimed PaperCaptureComposer.spec.ts checks the eyebrow-to-name relation, but that test mounted on the default locale only, and Spanish composer chrome was asserted nowhere in the suite. The relation test is now an it.each over ['en', 'it', 'es'] that sets the locale, mounts, and reads both halves of each pair off the DOM, so the promise those docblocks make to a translator is true in every supported locale. The en docblock's "in every locale it covers" is replaced by the locales it actually runs in.

Element types back on seven selectors (LOW). In PaperInboxView.spec.ts, seven selectors had lost their element-type qualifier in the testid migration in places where the element type WAS the point: lines 128 and 143 read textarea[data-testid="paper-composer-body"] again (the composer variant present, and absent in archived history), and lines 780, 1443, 1540, 1645 and 1670 read select[data-testid="paper-composer-board"] again (including the two find<HTMLSelectElement> value reads).

Test plan

Verified, from frontend/taskdeck-web after npm ci:

  • npx vitest --run --maxWorkers=2 src/tests/views/paper/inbox/PaperCaptureComposer.spec.ts src/tests/views/paper/inbox/PaperTriageTable.spec.ts src/tests/views/paper/PaperInboxView.spec.ts src/tests/i18n/catalogs.spec.ts src/tests/guards/routeAffordanceCoverage.spec.ts at commit 1, names still unchanged: Test Files 5 passed, Tests 225 passed. That green is the migration's proof: the suites that were rewritten to testids still pass with the old copy in place, so the selector change is inert.
  • Same command at commit 2: Test Files 5 passed, Tests 228 passed (the three added cases).
  • Same command at commit 3, the current head: Test Files 5 passed, Tests 230 passed (the single relation test became three parameterised cases). Test Files equals the five spec paths named in every run, so no path was silently skipped.
  • Red-first for the names, with the new assertions in place and the catalogs still holding the old names: Tests 4 failed, 122 passed, all four in PaperCaptureComposer.spec.ts.
    1. renders the field chrome in English on the default locale (Capture body received, Body: write the text of this capture expected).
    2. starts every field accessible name with the visible eyebrow above it (all four pairs reported as violations: Body/Capture body, Board/Board picker, Labels/Add label, Due (optional)/Due date).
    3. re-renders the field chrome in Italian when the locale switches (Testo della cattura received).
    4. re-renders the field chrome when the locale switches after mount (same, on the already-mounted component).
      The new PaperTriageTable.spec.ts post-mount case passed at that point by construction: it covers names PR i18n(inbox): move the Paper triage table's and composer's remaining English to the inbox catalogs #2696 already made label-first, and it is a coverage addition, not a name change.
  • Red-first for the widened relation guard, with es.composer.dueLabel temporarily changed to Vencimiento (opcional): Tests 1 failed, 40 passed, only the es case, reporting the offending pair ["Vencimiento (opcional)", "Fecha límite (opcional): elige cuándo vence esta captura"]. The en and it cases stayed green, which is the point of widening it. The break was reverted before every verifying run above.
  • npm run typecheck (vue-tsc -b): clean, exit 0, at all three commits.
  • npx eslint on the changed files: 0 errors at every commit. One pre-existing warning survives, vuejs-accessibility/label-has-for on the composer's due-date label, which wraps TdDateField rather than a bare input. Confirmed pre-existing by linting git show HEAD:...PaperCaptureComposer.vue before the change, where it reports at line 352 instead of 355.
  • git diff --check: clean.
  • env -u Llm__Gemini__BaseUrl -u Llm__Gemini__ApiKey -u Llm__Gemini__Model -u Llm__Gemini__TimeoutSeconds npx playwright test tests/e2e/capture-loop.spec.ts tests/e2e/first-run.spec.ts tests/e2e/review-proposals.spec.ts --project=chromium --reporter=line: 11 passed (1.2m), the config's own webServer starting the stack. Run at commit 2 and not repeated for commit 3, which changed no E2E file: git diff --stat HEAD -- frontend/taskdeck-web/tests/e2e/ is empty for that commit.
  • Grepped the whole frontend for the four old names after the rewrite. The only remaining hits are inbox.triage.edit.* keys in a different namespace, which this PR does not touch.

NOT verified:

  • The es and it wording is this lane's, not native-reviewed. OUTSTANDING_TASKS.md Seed an i18n translation layer (vue-i18n) with Italian and Spanish locales #1770 tracks that review and it stays open.
  • No screen reader was run. The label-in-name claim rests on the asserted string relation between the eyebrow and the accessible name in en, it and es, not on an assistive-technology reading.
  • The frontend suite was not run whole and no backend test was run. Nothing outside the frontend changed.
  • The route-affordance source pin was updated and its shape is checked by routeAffordanceCoverage.spec.ts, but that guard only validates the src/<file>:<line> form. That the line still holds the Capture button was confirmed by reading it, not by an automated check.

Boundaries and risks

Owned and touched: PaperCaptureComposer.vue (testids only), src/locales/{en,es,it}/inbox.ts (the four keys and the docblocks), PaperCaptureComposer.spec.ts, PaperTriageTable.spec.ts (the added switch case only), PaperInboxView.spec.ts (selectors and one comment), the three E2E specs (one selector each), routeAffordanceInventory.ts (the one moved pin).

Not touched: PaperTriageTable.vue, PaperInboxView.vue, useInboxOrchestrator.ts, captureStore.ts, the review files, the #2698 draft-notice keys, the #2696 triage keys, and inbox.boardPicker.triageAria.

Risks. The composer's rendered behaviour is unchanged: the only production edit is four data-testid attributes and four catalog strings, no control, binding, guard or emitted payload moved. The one real exposure is that something outside the frontend selects these controls by their accessible name; the grep above found nothing, and the three E2E specs used the name purely as a locator. A future copy edit that breaks label-in-name, in any of the three locales, now fails a test rather than passing silently.

… by accessible name

The Composer's textarea, board select, label input and due input were reached
by their `aria-label` from `PaperInboxView.spec.ts` (47 selectors) and from
three Playwright specs. That made the four accessible names test API: they had
to keep their pre-extraction English byte for byte through the #1871 catalog
extraction, and two of them therefore still fail WCAG 2.5.3 label-in-name.

Each control gains a stable `data-testid` in the composer's existing
`paper-composer-*` naming, and every selector that read a name moves onto it.
No accessible name, no catalog value and no behaviour changes in this commit,
so the unchanged suites passing IS the proof that the migration is inert.

The route-affordance inventory's source pin for `inbox.capture-submit` moves
from PaperCaptureComposer.vue:369 to :373, the four added attribute lines
having pushed the Capture button down. The E2E line pins are unaffected: each
spec had exactly one line replaced in place.

Refs #1871
…ur accessible names

With the specs off the accessible names (previous commit), the four Composer
names are copy again and take the WCAG 2.5.3 shape the board controls and the
triage board-pick already use (PR #2675): the visible eyebrow first, then what
the control does.

  bodyAria    Capture body -> Body: write the text of this capture
  labelsAria  Add label    -> Labels: type a label and press Enter to add it
  dueAria     Due date     -> Due (optional): set a due date for this capture
  composerAria Board picker -> Board: choose where this new capture will land

Two of them failed label-in-name outright before this: "Add label" did not
contain the visible "Labels" and "Due date" did not contain "Due (optional)".
The two board names now differ only in their second half, which is the only
part a screen-reader user hears as a distinguisher, so the Composer's says
"this new capture" (an unfiled draft) against the triage row's "this capture".

es and it are rewritten to the same shape. Their composer docblocks also stop
claiming the placeholders stay lowercase: that is true of labelsPlaceholder
only, bodyPlaceholder is a capitalized sentence by design in all three
locales, and the wrong rule invited a native reviewer (#1770) to "fix" it. They
now mirror the English rule instead - a hint about the shape of the value,
never the field's name.

Tests. The composer spec asserts the label-first RELATION, not only the four
literals: every accessible name must start with the eyebrow rendered above its
control, so a future reword cannot silently break the rule. Each component also
gains one post-mount locale switch, because the existing Italian cases set the
locale before mounting and would pass against a non-reactive t() snapshot.

Red-first, before the catalogs changed: 4 failures in
PaperCaptureComposer.spec.ts - "renders the field chrome in English on the
default locale", "starts every field accessible name with the visible eyebrow
above it", "re-renders the field chrome in Italian when the locale switches"
and the new post-mount case.

The es/it wording is this lane's, not native-reviewed (OUTSTANDING_TASKS #1770).

Refs #1871
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Review record (alpha product-trust lane, review-and-ship round 1 at head 6a01c43b7).

Reviewer: one fresh-context independent reviewer subagent (read-only; Codex credits exhausted, SC-9), given the worktree at the head and the merge-base diff. Verdict: SHIP, no merge-blocking items; two MEDIUMs fixed in round 2 because both are copy and specs this PR introduced.

Lenses that found nothing: label-in-name holds in all three locales (each accessible name starts with the exact eyebrow rendered above it, compared key by key); no remaining selection by the old names in unit or E2E specs and no runtime selector keyed on the aria-label; the four testids are unique and reach the right elements (TdDateField forwards attrs to its input; the board testid sits on the select); the inventory pin lands on the Capture button and the E2E line pins are unaffected by the one-to-one replacements; the post-mount switch cases set, mount, switch and read the DOM with the catalogs preloaded and the locale reset per test; catalog form unchanged (no keys added, no placeholders or plural pipes); scope is the eleven files with four attributes as the only production change.

Findings, triaged once:

  • MEDIUM, fixed in round 2: the new composer board name said "choose where this new capture will land", but captures land in Inbox and the board choice links the capture for triage (the composer's own footer and the nib copy say so); the name becomes "Board: choose which board this capture is linked to for triage" in en, es and it. The triage picker's sibling name from i18n(inbox): move the Paper triage table's and composer's remaining English to the inbox catalogs #2696 ("choose where this capture goes") is recorded on [Frontend] Review-i18n + board-picker polish residuals (PR #1869 / #1870 reviews) #1871 for the same consistency check, not changed here.
  • MEDIUM, fixed in round 2: the es and it docblocks claimed the composer spec checks the eyebrow-to-name relation, but that test ran on the default locale only and Spanish chrome was asserted nowhere; the relation test becomes an it.each over en, it and es.
  • LOW, taken if trivial: two migrated selectors lost their element-type assertion (textarea and select forms restore it).

Round count: 2 after the fix push. Merge gate: CI green at the fix head plus the three-minute age; no second pass is owed (copy and specs only).

…the label-first rule in every locale

Round 2 of PR #2700, two MEDIUMs and one LOW from the fresh-context review.

MEDIUM 1 -- product truth. `inbox.boardPicker.composerAria` said "Board: choose
where this new capture will land". Nothing lands on a board from here. Every
capture lands in Inbox; the board choice LINKS the capture so triage can propose
against that board, and nothing reaches the board without approve and execute
(ADR-0003). The composer's own footer and `nib.destinationWithBoard` already say
so, so the name contradicted the surface it sits on. All three locales are
rewritten to the linking form, still label-first:

  en  Board: choose which board this capture is linked to for triage
  es  Tablero: elige a qué tablero se vincula esta captura para el triage
  it  Bacheca: scegli a quale bacheca collegare questa cattura per il triage

The es and it wording reuses each catalog's own triage vocabulary from the
sibling `nib.destinationWithBoard` ("vinculada a ... para el triage",
"collegata a ... per il triage"). `boardPicker.triageAria` is NOT touched: the
two names still differ only in their second half, and the docblocks now say why
the composer's may not use the word "land".

MEDIUM 2 -- the es and it docblocks claimed `PaperCaptureComposer.spec.ts`
checks the eyebrow-to-name relation, but that test mounted on the default locale
only, and Spanish composer chrome was asserted nowhere in the suite. The
relation test becomes an `it.each` over en, it and es, reading both halves of
each pair off the DOM, so the claim those docblocks make to a translator is now
true. The en docblock's "in every locale it covers" is replaced by the locales
it actually runs in.

LOW -- seven selectors in `PaperInboxView.spec.ts` had lost their element-type
qualifier in the testid migration, in places where the element type WAS the
point (the composer body existing or not existing per variant, and the board
select's value). They read `textarea[data-testid="paper-composer-body"]` and
`select[data-testid="paper-composer-board"]` again.

Red-first for the widened guard: with `es.composer.dueLabel` temporarily changed
to "Vencimiento (opcional)", only the new es case failed, reporting the pair
["Vencimiento (opcional)", "Fecha límite (opcional): elige cuándo vence esta
captura"]; en and it stayed green. The break was reverted before the verifying
run.

The es and it wording is this lane's, not native-reviewed (OUTSTANDING_TASKS
#1770).

Refs #1871
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Round-2 record (alpha product-trust lane; fix head a563d4460, one commit on top of 6a01c43b7, copy and specs only).

Fixed:

  • The composer board name says what the control does in the Inbox-first flow: en "Board: choose which board this capture is linked to for triage", es "Tablero: elige a qué tablero se vincula esta captura para el triage", it "Bacheca: scegli a quale bacheca collegare questa cattura per il triage", reusing each catalog's nib.destinationWithBoard vocabulary so the composer name and the Nib destination line say the same thing; the docblocks record why the composer's name may not say "land" (every capture lands in Inbox, the board choice links it for triage, nothing reaches the board without approve and execute). The triage picker's name is untouched and its consistency is recorded on [Frontend] Review-i18n + board-picker polish residuals (PR #1869 / #1870 reviews) #1871.
  • The eyebrow-to-name relation guard runs as an it.each over en, it and es, reading both halves off the DOM, so Spanish composer chrome is asserted for the first time and the es/it docblocks' claim is true. Red-first: with the es due eyebrow temporarily changed, only the es case failed (en and it green), then restored.
  • Seven migrated selectors in PaperInboxView.spec.ts regained their element-type qualifier (textarea[...], select[...]).

Verified at the fix head: the five named suites 5 files, 230 passed; npm run typecheck clean; eslint on the changed files clean; git diff --check clean; no E2E file changed in round 2, so the round-1 Playwright result stands (11 passed, chromium); origin/main unmoved at a789dabbe.

Not verified: es/it wording not native-reviewed (#1770); no screen-reader run. The PR body is being updated to the round-2 strings and counts.

Merge gate remaining: CI green at a563d4460 and the three-minute age. Round count: 2; no second pass is owed (copy and specs only).

@Chris0Jeky
Chris0Jeky merged commit 27b0e91 into main Sep 5, 2026
37 checks passed
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Sep 5, 2026
Chris0Jeky added a commit that referenced this pull request Sep 5, 2026
…pointers, #2701 record, #2630 correction clause)

Answers the docs review of PR #2704: the #2703 bullet no longer says only force guarantees a request (it gets past the throttle only; the in-flight share and demo mode still apply, the very overclaim round 2 removed from the code); the #2700 and #2703 bullets name every open residual the not-shipped line names; the #2701 pointer says the sixteenth block's subsection is its record; the #2700 bullet says its round 2 touched copy and specs only; the corrections line quotes the thirteenth block's clause that the pinned warning itself sticks.
@Chris0Jeky
Chris0Jeky deleted the issue-1871/composer-names branch September 6, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant