oficina / docs Public

Fix what a real browser found: one WCAG failure, and four checks I blinded

The merge put main at 39/44 on the browser drive. The triage session has
a Chromium that starts and drove it; I reproduced all five myself before
changing anything, then diagnosed and fixed them. Back to 44/44.

ONE IS A REAL DEFECT AND IT IS MINE. `span[docs-menu-shortcut]` ended at
x=334 in a 320px viewport: the menu panel spans the bar correctly, but a
long item label plus its shortcut on one row is wider than the bar and
neither half wraps. That is SC 1.4.10 on the document screen, twelve
hours after the same failure was fixed on the list screen, in the check
I had specifically agreed to keep green. The shortcut labels are hidden
below 36rem now — they are already aria-hidden decoration for the eye,
and a viewport that narrow is a phone with no Ctrl key to press. The
item's label, which is its accessible name, is untouched.

THE OTHER FOUR ARE NOT PRODUCT BUGS, AND THAT IS NOT AN EXCUSE. The menu
bar gave a second home to every data-docs-command name, and menu items
come before the toolbar in document order — so
`[data-docs-command="strong"]` stopped identifying one control. The
drive's `querySelector` returned the menu item, which lives inside a
closed <details>, so its rect is all zeros and every click went to the
top-left corner of the page. Bold, the font mark, undo and the caret
check all failed thirty seconds later saying "timed out", which is true
and points nowhere near the cause. Both controls work when a person
clicks them; what I broke was the drive's ability to tell.

Fixed twice over. The three toolbar selectors are scoped to
[docs-toolbar], and clickSelector now refuses two things it used to do
silently: clicking an element with no size, and clicking the first of
several matches. Mutation-checked — restore the ambiguous selector and
the run stops with "2 elements match [data-docs-command="strong"] — the
selector no longer identifies one control" instead of a timeout.

That guard is the part worth keeping. The ambiguity was created by a
design decision I made on purpose and defended in the commit message —
one command table, two surfaces — and the cost landed somewhere I never
looked. A harness that cannot tell "hidden" from "absent" answers
confidently and wrongly, which is the failure this repo has a section
about.

Also carries the note on mark_names: it returns names and drops every
value, so an unknown mark is safe today, but it is where the first
valued mark's allowlist has to go and it must not assume only link
carries a value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 68273a47ab9bd3fdb6d7ffc51299be9c6ab9ab8c parent ca9641b
2 files changed, +54 −6
  • fixtures/browser-drive.mjs +36 −6
  • internal/docs/views/show.html +18 −0

CI passed — run details

diff --git a/fixtures/browser-drive.mjs b/fixtures/browser-drive.mjs
index 4c73414..65e8815 100644
--- a/fixtures/browser-drive.mjs
+++ b/fixtures/browser-drive.mjs
@@ -84,12 +84,42 @@ async function click(x, y, clickCount = 1) {
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount, buttons: 0 })
}
+// clickSelector clicks the centre of the first element matching a
+// selector — and refuses two things that used to fail much later and
+// much less clearly.
+//
+// A ZERO-SIZE TARGET IS AN ERROR, not a click at (0,0). This cost five
+// checks and an hour. The menu bar gave a second home to every
+// data-docs-command name, and menu items come before the toolbar in
+// document order, so `[data-docs-command="strong"]` started matching a
+// menu item inside a closed <details> — display: none, rect all zeros.
+// Every click went to the top-left corner of the page, and the checks
+// failed thirty seconds later saying "timed out waiting for bold to
+// apply", which is true and points nowhere near the cause. Refusing the
+// click names it instead.
+//
+// MORE THAN ONE MATCH IS ALSO AN ERROR, because that is the same bug
+// one step earlier: the selector stopped identifying one control and
+// nothing said so. A caller that genuinely wants the first of several
+// scopes its selector, which is what the toolbar's three now do.
async function clickSelector(selector) {
const at = await evaluate(
- `(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)return null;` +
- `const r=e.getBoundingClientRect();return {x:r.left+r.width/2,y:r.top+r.height/2}})()`,
+ `(()=>{const all=document.querySelectorAll(${JSON.stringify(selector)});` +
+ `if(!all.length)return {none:true};` +
+ `const e=all[0];const r=e.getBoundingClientRect();` +
+ `return {count:all.length,w:r.width,h:r.height,x:r.left+r.width/2,y:r.top+r.height/2}})()`,
)
- if (!at) throw new Error(`no element matching ${selector}`)
+ if (at.none) throw new Error(`no element matching ${selector}`)
+ if (at.count > 1) {
+ throw new Error(
+ `${at.count} elements match ${selector} — the selector no longer identifies one control, so this would click whichever comes first in the document`,
+ )
+ }
+ if (at.w === 0 || at.h === 0) {
+ throw new Error(
+ `${selector} matched an element with no size (${at.w}x${at.h}) — it is hidden, so clicking its centre would click the page corner`,
+ )
+ }
await click(at.x, at.y)
}
@@ -341,7 +371,7 @@ async function run() {
await click(wordAt.x, wordAt.y, 1)
await click(wordAt.x, wordAt.y, 2)
await sleep(300)
- await clickSelector('[data-docs-command="strong"]')
+ await clickSelector('[docs-toolbar] [data-docs-command="strong"]')
try {
await settle('bold to apply', (html) => html.includes('<strong>'))
check('the bold button bolds the selection', true)
@@ -393,9 +423,9 @@ async function run() {
// Undo has to be available now, and has to walk the heading back.
check('undo becomes available after an edit', !(await evaluate(
- `document.querySelector('[data-docs-command="undo"]').disabled`,
+ `document.querySelector('[docs-toolbar] [data-docs-command="undo"]').disabled`,
)))
- await clickSelector('[data-docs-command="undo"]')
+ await clickSelector('[docs-toolbar] [data-docs-command="undo"]')
try {
await settleOn(`the heading to be undone`, () => blockTag(paragraphIndex), (tag) => tag === 'P')
check('undo reverses the last change', true)
diff --git a/internal/docs/views/show.html b/internal/docs/views/show.html
index 10ef409..3f4b6c0 100644
--- a/internal/docs/views/show.html
+++ b/internal/docs/views/show.html
@@ -1010,6 +1010,24 @@
min-inline-size: 0;
max-inline-size: none;
}
+ /* The shortcut labels go at this width, and this is a real WCAG
+ failure that shipped rather than a tidy-up.
+ A browser drive on main measured `span[docs-menu-shortcut]`
+ ending at x=334 in a 320px viewport: the panel spans the bar
+ correctly, but "Find and replace in this document" plus its
+ shortcut on one row is wider than the bar, and neither half
+ wraps. SC 1.4.10, on the document screen, reintroduced by this
+ menu bar twelve hours after the same failure was fixed on the
+ list screen.
+ Hiding them is the right fix rather than the cheap one: they are
+ already aria-hidden decoration for the eye, and a viewport this
+ narrow is a phone, which has no Ctrl key to press. The item's own
+ label — which is its accessible name and the thing a person acts
+ on — is untouched. The bindings still work for anyone with a
+ keyboard attached, and the shortcuts sheet still lists them. */
+ [docs-menu-shortcut] {
+ display: none;
+ }
[docs-menu] {
position: static;
}