Skip to content

test(e2e): cover useFocusTrap's previousFocus cleanup fallback when the trigger unmounts - #1178

Merged
mrbobbytables merged 1 commit into
mainfrom
quality/test-focus-trap-trigger-unmounted
Oct 8, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
quality/test-focus-trap-trigger-unmounted

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Test Improvement

Adds one new end-to-end spec, tests/e2e/focus-trap-trigger-unmounted.spec.js,
for the only close path that nulls a lightbox's trigger ref.

src/components/hooks/useFocusTrap.js:51 restores focus with
(triggerRef.current || previousFocus)?.focus?.(). Every existing lightbox spec
closes the dialog with Escape, the close button or a backdrop click, and in all
three the trigger is still mounted, so the previousFocus operand is never
evaluated in a browser.

Both lightboxes are rendered by the card that triggers them
(src/components/MemberDirectory/MemberCard.js:72-78), and the directory's
search field stays live behind the backdrop. A visitor who opens a profile and
keeps typing filters that card out: trigger and dialog unmount in the same
commit, React detaches refs before running passive-effect cleanups, and the
cleanup falls through to previousFocus.

The spec drives exactly that: open a profile, focus #member-search through the
DOM (a click would dismiss via the backdrop while the trigger is still mounted),
type a query no organization matches, then assert

  • the dialog unmounts with its card,
  • no pageerror fires — written as triggerRef.current.focus() the cleanup
    throws a TypeError into the commit while the visitor is typing,
  • document.body.style.overflow returns to the value captured before the
    dialog opened — a cleanup that throws early leaves the page unscrollable with
    no dialog on screen,
  • focus stays in the search field with its typed value intact,
  • and the directory is still live after the query is cleared, so the assertions
    describe a working page rather than one that stopped responding.

Verification at 03cfcfe

  • Full two-build coverage run (npm run build:e2e:coverage, then
    test:e2e:coverage): 341 passed, and the gated report
    (--check-source 100 --check-source-regions 91 --require-source-files) exits
    0 at src files | 100.00 | 91.72 | 2099/2099 lines | 443/483 regions, up
    from 91.63% / 438 of 478 on main.
  • npm run test:unit: 2025 passed, 0 failed. npm run check:format: clean.

This does not retire the region, and that is #1066 / #1079

Stated up front rather than claimed otherwise. A control run of this spec alone
against the real build reports useFocusTrap's uncovered regions as 29 34 35
— line 51 absent, i.e. demonstrably executed. The full two-build run puts it
back (35 51), because the variant build emits the cleanup under coordinates
the real build's key does not match. That is the region-union attribution
behaviour tracked by #1066 and #1079, and this spec is written as behaviour
rather than as a coverage claim for the same reason
tests/e2e/dialog-non-dismissing.spec.js already is.

Line 35 (if (!focusable?.length) return;) is deliberately out of scope: both
lightboxes always render a close button, so the taken arm has no browser path
at all.

Files

One new file, nothing edited:

  • tests/e2e/focus-trap-trigger-unmounted.spec.js

Disjoint from every open hold-gated PR — not scripts/audit-gate.mjs (#1161),
tests/tools/e2e-coverage-report.mjs (#1163), scripts/lib/svg-active-content.mjs
(#1168, #1176), CONTRIBUTING.md (#1166), tests/architecture-content-mirror.test.mjs
(#1165), tests/tools/e2e-data-fixtures.cjs or the metrics.json overlay
(#1171), or tests/e2e-coverage-run.test.mjs (#1174). No fixture overlay is
added, so the two-build ceiling in #1172 is untouched, and no workflow file is
involved.

Related Issue

Closes #1177


Human review required; this PR carries hold and must not be merged by automation.

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

No existing spec closes a lightbox while its trigger is unmounting, so the
previousFocus operand at src/components/hooks/useFocusTrap.js:51 is never
evaluated in a browser. Both lightboxes are rendered by the card that opens
them, so filtering that card out of the member directory unmounts the trigger
and the dialog in one commit and the cleanup sees triggerRef.current === null.

Asserts the observable consequences of that cleanup -- no pageerror, the body
scroll lock released to the value captured before the dialog opened, and focus
left in the search field the visitor is typing in -- rather than asserting that
a line ran.

Closes #1177

Signed-off-by: quality <quality@hive.kubestellar.io>
@hivecommons-hive hivecommons-hive Bot added the hold label Oct 8, 2026
@hivecommons-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 keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

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's previousFocus cleanup fallback has no end-to-end coverage: no spec closes a lightbox by unmounting its trigger

1 participant