Add deepFindAll to the shared browser helper, so counting probes stop being hand-rolled
Some checks are pending
build-gate / build (push) Waiting to run

deepFind returns the FIRST match and stops. Every count-based assertion needs
all of them -- how many rows, anchors, hints, canvases -- and because this
file offered no such helper, each ad-hoc probe hand-rolled its own shadow
walker. Across one session of verification work those produced four false
results:

  a naive walker double-visited through nested roots: 48 cards for 24;
  a single-level querySelector found 0 anchors where there were 155;
  an .rd-row count included a detail panel sharing that class, so a correct
    filter read as broken;
  a bare "<script" prefix match counted the word scriptUrl in a comment and
    flagged a valid file.

Two of those nearly became bug reports against working code. A missing helper
does not stop anyone writing the check; it moves the check somewhere nobody
reviews.

The seen-Set dedupe is the load-bearing part: a node reachable through two
roots must count once, which is the first failure above.

Backward compatible by construction -- the third parameter is APPENDED, so
callbacks taking (deepFind, arg) are untouched. Verified rather than assumed:
a (deepFind, arg) callback still resolves; deepFindAll returns 24 cards with
returned === distinct; share-roundtrip-smoke (a real consumer) still passes;
check:a11y-button-names and check:parse both pass.
This commit is contained in:
doxa-maps 2026-09-22 06:19:13 +00:00
parent 35dbb5d557
commit 2015378fcd

View file

@ -13,7 +13,12 @@
* DEEP_FIND_SRC page-side `deepFind(root, sel)` source: querySelector that
* recurses into every nested OPEN shadow root (composite MFEs
* nest custom elements, each with its own shadow root).
* deepEvaluate(page, fn, arg) page.evaluate with deepFind bound: fn receives (deepFind, arg).
* DEEP_FIND_ALL_SRC page-side `deepFindAll(root, sel)`: the COUNTING counterpart —
* returns EVERY match across nested open shadow roots, deduped
* via a seen-Set (a node reachable through two roots counts once).
* deepEvaluate(page, fn, arg) page.evaluate with both bound: fn receives
* (deepFind, arg, deepFindAll). The third parameter is APPENDED,
* so every existing caller taking (deepFind, arg) is unaffected.
* Composed in Node (no eval inside the page, so a host CSP
* cannot break it).
*/
@ -61,7 +66,38 @@ export const DEEP_FIND_SRC = `(root, sel) => {
return null
}`
// ── deepFindAll — the COUNTING counterpart to deepFind ───────────────────────────────────────
//
// WHY THIS EXISTS (added 2026-09-22). deepFind returns the FIRST match and stops. Every
// count-based assertion needs all of them — how many rows, anchors, hints, canvases — and because
// this file offered no such helper, each ad-hoc probe hand-rolled its own shadow walker. Measured
// across one session of verification work, those hand-rolled walkers produced FOUR false results:
//
// · a naive walker double-visited through nested roots and reported 48 cards for 24;
// · a single-level querySelector found 0 anchors in a composite that had 155;
// · a `.rd-row` count included a DETAIL PANEL sharing that class, so a correct filter read as broken;
// · a `<script` prefix match counted the word `scriptUrl` in a comment and flagged a valid file.
//
// Two of those nearly became bug reports against working code. A missing helper does not stop
// anyone writing the check — it just moves the check somewhere nobody reviews.
//
// SEEN-SET DEDUPE IS THE LOAD-BEARING PART. A node reachable through two roots must be counted
// ONCE; without the Set the count silently doubles, which is the first failure above.
export const DEEP_FIND_ALL_SRC = `(root, sel, _seen) => {
const seen = _seen || new Set()
const out = []
if (!root || !root.querySelectorAll) return out
for (const el of root.querySelectorAll(sel)) { if (!seen.has(el)) { seen.add(el); out.push(el) } }
for (const el of root.querySelectorAll('*')) {
if (el.shadowRoot) out.push(...deepFindAll(el.shadowRoot, sel, seen))
}
return out
}`
export function deepEvaluate(page, fn, arg) {
const composed = new Function('arg', `const deepFind = ${DEEP_FIND_SRC};\nreturn (${fn.toString()})(deepFind, arg)`)
const composed = new Function('arg',
`const deepFind = ${DEEP_FIND_SRC};\n` +
`const deepFindAll = ${DEEP_FIND_ALL_SRC};\n` +
`return (${fn.toString()})(deepFind, arg, deepFindAll)`)
return page.evaluate(composed, arg)
}