Skip to content

[3/7] Add frontend test infrastructure (Vitest + Testing Library) - #274

Open
alex-clickhouse wants to merge 1 commit into
alex-clickhouse/task-board-modalfrom
alex-clickhouse/web-test-harness
Open

[3/7] Add frontend test infrastructure (Vitest + Testing Library)#274
alex-clickhouse wants to merge 1 commit into
alex-clickhouse/task-board-modalfrom
alex-clickhouse/web-test-harness

Conversation

@alex-clickhouse

Copy link
Copy Markdown
Collaborator

Stacked on #273#272main. The diff here is only the test harness.

Why

web/ has no test tooling and zero test files. Every frontend change is verified by tsc -b and vite build — which prove code compiles and bundles, and say nothing about whether it behaves.

That gap is widest for exactly the kind of code #273 just added. A focus trap, Escape precedence against document-level handlers, dialog stacking, mousedown-vs-click — none of it is reachable by a type checker.

It caught a bug immediately

Writing the Modal specs surfaced a real defect that tsc and vite build were both happy with:

The body scroll lock read modalStack.length to decide if it was the last dialog out. React runs effect cleanups in declaration order, and the scroll-lock effect is declared before the one that unregisters from the stack — so the closing dialog was still in the stack, the length was never 0, and overflow: hidden was never lifted. Closing any dialog left the page unscrollable.

Fixed in be57e5f on the #273 branch, so that PR doesn't ship the bug. The page looks completely normal until you try to scroll it, which is the sort of thing a manual pass misses and a spec doesn't.

What's here

  • vitest + jsdom + @testing-library/{react,user-event,jest-dom} as dev dependencies
  • test block in vite.config.ts (shares the existing resolve/plugin config — no second config file to drift)
  • src/test/setup.tsjest-dom matchers, cleanup between tests, and two jsdom shims
  • npm test / npm run test:watch
  • npm test added to the existing Frontend build (Vite) CI job
  • 19 specs for Modal covering portal target, ARIA semantics, Escape (including that it does not reach app-level handlers), backdrop dismissal, the drag-release-outside case, focus entry/wrap/restore, stacked-dialog Escape isolation, and scroll-lock refcounting

The two jsdom shims are load-bearing

Both live in setup.ts with comments, but they're the sort of thing worth flagging in review:

  1. requestAnimationFrame — jsdom doesn't implement it, and the Modal defers its initial focus through it.
  2. offsetParent — jsdom gives every element no layout, so offsetParent is null for everything. The focus trap filters candidates on exactly that. Without the shim the trap would find nothing focusable and every trap assertion would pass vacuously — worse than having no tests, because it looks like coverage.

Notes

  • Scoped include to src/**/*.test.{ts,tsx}; the default glob walks node_modules.
  • css: false — specs assert behaviour and ARIA, never Tailwind classes.
  • Installing added 137 packages. npm ci in CI is cached on web/package-lock.json, and the test step adds ~2s to the job.
  • I did not backfill specs for any pre-existing component. This PR is the harness plus one worked example; retrofitting coverage is a separate conversation.

Testing

  • npm test — 19 passed
  • npx tsc -b — clean
  • npm run build — clean
  • npx eslint on both new files — clean

🤖 Generated with Claude Code

@alex-clickhouse
alex-clickhouse requested a review from Copilot August 5, 2026 10:38
@alex-clickhouse alex-clickhouse changed the title Add frontend test infrastructure (Vitest + Testing Library) [3/6] Add frontend test infrastructure (Vitest + Testing Library) Aug 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a frontend testing harness for the web/ app using Vitest + jsdom + Testing Library, and introduces an initial worked example test suite for the shared Modal component. This closes the current gap where frontend changes are only validated via typechecking and bundling.

Changes:

  • Configure Vitest in vite.config.ts (jsdom env, setup file, scoped test glob, CSS disabled).
  • Add global test setup shims and cleanup in src/test/setup.ts.
  • Add a comprehensive Modal interaction test suite and wire npm test into CI.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/vite.config.ts Adds Vitest configuration alongside existing Vite config to share resolve/plugins.
web/src/test/setup.ts Establishes global test setup (jest-dom, cleanup, jsdom shims).
web/src/components/ui/Modal.test.tsx Adds behavioral specs for the Modal component (dismissal, focus, stacking, scroll lock).
web/package.json Adds test scripts and Vitest/Testing Library/jsdom devDependencies.
web/package-lock.json Locks new testing dependencies and transitive packages.
.github/workflows/ci.yml Runs npm test in the existing frontend CI job.
Files not reviewed (1)
  • web/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread web/package.json
Comment thread .github/workflows/ci.yml
@alex-clickhouse alex-clickhouse changed the title [3/6] Add frontend test infrastructure (Vitest + Testing Library) [3/7] Add frontend test infrastructure (Vitest + Testing Library) Aug 5, 2026
web/ had no test tooling and no test files. Everything in the frontend
was verified by `tsc -b` and `vite build`, which prove code compiles and
bundles — and say nothing about whether it behaves.

Sets up Vitest with jsdom and Testing Library, wires `npm test` into the
existing frontend CI job, and takes the Modal as its first subject. The
Modal is close to an ideal starting point: it is almost entirely
interaction (Escape precedence, focus trapping, stacking, the
mousedown-not-click backdrop), so essentially none of it was reachable by
the checks already in place.

That was not hypothetical. Writing these specs immediately surfaced a
bug the type checker could not: the body scroll lock read
modalStack.length to decide whether it was the last dialog out, but
React runs effect cleanups in declaration order, so the closing dialog
was still in the stack and overflow:hidden was never lifted. Closing any
dialog left the page unscrollable. Fixed in the preceding commit.

Two jsdom gaps are patched in the setup file rather than worked around
per test. requestAnimationFrame is missing, and the Modal defers its
initial focus through it. More subtly, jsdom reports offsetParent as
null for every element because nothing has layout — and the focus trap
filters candidates on exactly that, so without the shim the trap would
find nothing focusable and every trap assertion would pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alex-clickhouse
alex-clickhouse force-pushed the alex-clickhouse/web-test-harness branch from bb10782 to adc2904 Compare August 5, 2026 12:07
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review August 5, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants