Skip to content

test: cover useFocusTrap scroll lock, tab cycling and focus restore - #268

Open
kubestellar-hive[bot] wants to merge 1 commit into
mainfrom
quality/test-use-focus-trap
Open

kubestellar-hive[bot] wants to merge 1 commit into
mainfrom
quality/test-use-focus-trap

Conversation

@kubestellar-hive

Copy link
Copy Markdown
Contributor

Test Improvement

src/components/hooks/useFocusTrap.js is the shared accessibility primitive behind every modal dialog on the site — used by src/components/MemberDirectory/index.js and src/components/CommunityPeople/index.js. Before this PR it had no coverage of any kind: src/ was absent from the node --test --experimental-test-coverage report entirely, because no test could load anything under src/.

Every behaviour it owns is a silent failure mode — a broken scroll restore leaves the page unscrollable after a dialog closes, a broken focus wrap lets keyboard users tab out of a modal into inert content, a broken focus restore strands screen-reader users. Nothing in the build, prettier, or the validator scripts would catch any of it.

Files claimed by this PR (all new, all test-only — no production code, no dependency, no package.json, no .github/workflows/**):

  • tests/tools/react-hook-driver.mjs — a renderHook() that swaps React's per-render dispatcher slot so useRef/useEffect resolve against a small local implementation, then restores the slot synchronously. It honours effect dependency arrays: rerender() re-runs an effect, previous cleanup first, only when deps actually change. Because the slot is restored immediately and no module specifier is aliased process-wide, this cannot disturb other tests that import React for real — notably [quality] test: cover src/components/ArchitectureFilters filterArchitectures and add a JSX import path (tests/tools/jsx-hooks.mjs + tests/helpers-jsx.mjs + tests/architecture-filters.test.mjs) #229's.
  • tests/tools/fake-dom.mjs — a stand-in for only the DOM surface this hook touches: body.style.overflow, activeElement, add/removeEventListener('keydown'), element.focus() and querySelectorAll('button, a[href]'). Deliberately tiny, so the hook's real contract with the DOM is explicit rather than buried in a DOM library.
  • tests/use-focus-trap.test.mjs — 17 tests: stable ref identity across renders; scroll lock and restore of the previous overflow value; focus moved to the close button on mount; unattached closeRef tolerated; Escape invoking onClose; other keys ignored; Shift-Tab wrapping first→last; Tab wrapping last→first; Tab mid-dialog left to the browser; a lone focusable element trapping onto itself both ways; Tab ignored with no focusable children and with an unattached dialogRef; non-focusable descendants (<a> without href, <span>) excluded from the cycle; listener removal on unmount; focus restored to the trigger; fallback to the previously focused element; and unmount not throwing with nothing to restore to.

Not a dependency on #229

#228 deferred this hook on the grounds that it "needs a DOM environment and a React renderer." It turns out neither is required, and this PR does not depend on #229 landing: useFocusTrap.js contains no JSX and imports no CSS module, so Node imports it as-is. This adds no package and touches neither package.json nor package-lock.json, so it cannot conflict with #229 or #225 there. File sets are disjoint from every open PR: #229 owns tests/tools/jsx-hooks.mjs, #225 owns tests/tools/coverage-report.mjs, #231 owns tests/helpers.mjs — none of which is touched here. MemberDirectory, under refactor in #118, is likewise untouched.

Verification

node --test: 72 tests pass, 0 fail (55 before). With --experimental-test-coverage:

src/components/hooks/useFocusTrap.js | 100.00 | 100.00 | 100.00

Repo totals move 73.51 → 82.93 line and 47.12 → 63.53 branch. npx prettier --check is clean on all three added files.

The suite was mutation-checked rather than assumed effective. Seven independent mutations of the hook each produced failures (1–3 tests apiece): overflow not restored, preventDefault dropped, keydown listener never removed, first/last focusable swapped, Escape no longer closing, close button not focused on mount, focus not restored on unmount. src/ was restored to a clean tree afterwards and is unmodified in this PR.

Coverage evidence

  • Unit: node --test --experimental-test-coverage (node v26.8.2), local clone of cncf/endusers at parent revision 00b44df after npm ci, 2026-09-17. Baseline: 55 tests, src/ absent from the report (0%, never loaded). After: 72 tests, figures above.
  • End-to-end: unavailable — this repository has no end-to-end or browser suite and publishes no coverage artifact from any suite (tracked in [quality] CI publishes no coverage evidence, so coverage findings cannot be verified #186). The two sources cannot be combined, and no claim is made that this path lacks end-to-end coverage.

Deliberately out of scope

The rendered components (AwardsTimeline, CNCFProjectCard, CommunityPeople, MetricsDashboard, ProjectsBorn, ReferenceArchitectures, MemberDirectory, theme/Footer) still have no coverage. Those genuinely do need a renderer and the JSX import path from #229, and belong in their own issue.

Related Issue

Closes #267


Filed by quality agent (hold-gated mode). Human review required.

— hive: agent=quality backend=copilot model=claude-opus-5

src/components/hooks/useFocusTrap.js is the shared accessibility
primitive behind the MemberDirectory and CommunityPeople modal dialogs,
and had no coverage of any kind: src/ was absent from the
node --test --experimental-test-coverage report entirely.

Add a dependency-free way to exercise it, plus the tests:

- tests/tools/react-hook-driver.mjs swaps React's per-render dispatcher
  slot so useRef/useEffect resolve locally, then restores it
  synchronously. Effect dependency arrays are honoured, so rerender()
  re-runs an effect (previous cleanup first) only when deps change.
- tests/tools/fake-dom.mjs stands in for the small DOM surface the hook
  touches: body.style.overflow, activeElement, keydown listeners,
  element.focus() and querySelectorAll('button, a[href]').
- tests/use-focus-trap.test.mjs adds 17 tests covering scroll lock and
  restore, mount focus, Escape, Tab/Shift-Tab wrapping at both ends,
  non-focusable descendants, listener cleanup and focus restoration.

The hook is plain JS, so this needs no JSX transform, no DOM library
and no React renderer, and adds no dependency.

72 tests pass (from 55); useFocusTrap.js reaches 100% line, branch and
function coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: kubestellar-hive[bot] <kubestellar-hive@hive.kubestellar.io>
@kubestellar-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "quality" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the outreach agent is always held because it publishes project-facing communication.

Hive will automatically remove the hold label once current policy no longer requires a level hold for "quality". If this is an outreach PR, a human must review it and remove the label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[quality] useFocusTrap has zero coverage: modal scroll lock, tab cycling and focus restore are unguarded

0 participants