test: cover useFocusTrap scroll lock, tab cycling and focus restore - #268
Open
kubestellar-hive[bot] wants to merge 1 commit into
Open
kubestellar-hive[bot] wants to merge 1 commit into
kubestellar-hive[bot] wants to merge 1 commit into
Conversation
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>
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 Hive will automatically remove the |
This was referenced Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Test Improvement
src/components/hooks/useFocusTrap.jsis the shared accessibility primitive behind every modal dialog on the site — used bysrc/components/MemberDirectory/index.jsandsrc/components/CommunityPeople/index.js. Before this PR it had no coverage of any kind:src/was absent from thenode --test --experimental-test-coveragereport entirely, because no test could load anything undersrc/.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— arenderHook()that swaps React's per-render dispatcher slot souseRef/useEffectresolve 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()andquerySelectorAll('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; unattachedcloseReftolerated; Escape invokingonClose; 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 unattacheddialogRef; non-focusable descendants (<a>withouthref,<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.jscontains no JSX and imports no CSS module, so Node imports it as-is. This adds no package and touches neitherpackage.jsonnorpackage-lock.json, so it cannot conflict with #229 or #225 there. File sets are disjoint from every open PR: #229 ownstests/tools/jsx-hooks.mjs, #225 ownstests/tools/coverage-report.mjs, #231 ownstests/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:Repo totals move 73.51 → 82.93 line and 47.12 → 63.53 branch.
npx prettier --checkis 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,
preventDefaultdropped, 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
node --test --experimental-test-coverage(node v26.8.2), local clone ofcncf/endusersat parent revision00b44dfafternpm ci, 2026-09-17. Baseline: 55 tests,src/absent from the report (0%, never loaded). After: 72 tests, figures above.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