Skip to content

Rewrite the mount-based presentational tests with React Testing Library#314

Merged
jonathangreen merged 2 commits into
mainfrom
chore/rewrite-mount-based-tests-rtl
Jul 13, 2026
Merged

Rewrite the mount-based presentational tests with React Testing Library#314
jonathangreen merged 2 commits into
mainfrom
chore/rewrite-mount-based-tests-rtl

Conversation

@jonathangreen

@jonathangreen jonathangreen commented Jul 9, 2026

Copy link
Copy Markdown
Member

Description

Rewrites 24 of the 28 mount-based enzyme suites to React Testing Library: the AdvancedSearch filter widgets (rendered inside a real DndProvider — HTML5Backend works under jsdom), the small leaf components and buttons, the self-test and manage-patrons panels, the search/sidebar/services views, three of the tab containers, Footer, and the app bootstrap (index.tsx).

  • The tab containers navigate via router.push rather than internal state, so "active tab" is asserted through the visible panel; where a tab panel fetches on mount and is incidental to a navigation test, it's mocked to a marker.
  • LibraryStats is deleted outright rather than ported — tests/jest/components/Stats.test.tsx already covers LibraryStats.tsx at 100% of lines and tests more (the query hook, caching, sysadmin gating).
  • The bootstrap test keeps its structural shape: it spies on ReactDOM.render, constructs CirculationAdmin in each mode, and asserts the captured element tree contains SetupPage / a Router — the analog of the original enzyme find(), since what those roots render is covered by their own tests.

userEvent usage follows the const user = userEvent.setup() convention used across tests/jest/.

Deferred, with their enzyme originals retained: ConfigTabContainer and BookDetailsTabContainer (their full mounts are the sole cover for eleven connected panels' mapStateToProps/mapDispatchToProps wiring, restored properly when those panels migrate to render their connected exports) and AdvancedSearchBuilder (its one uncovered line is a react-dnd move callback reachable only through a real drop). Mocking those panels now would have dropped 77 lines; the coverage gate caught it.

Motivation and Context

Part of the mocha/chai/enzyme → jest/RTL migration — the components enzyme actually mounted, the largest tranche. Retiring the last enzyme suites is what unblocks React 17+.

How Has This Been Tested?

npm test — full suite green. Coverage rises to 94.48% (+24 lines over the baseline) with no previously-covered line lost; rendering real leaf children reaches branches the shallow and mount-with-stubbed-children tests did not.

Checklist:

  • I have updated the documentation accordingly.
  • All new and existing tests passed.

@claude

claude Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude finished @jonathangreen's task in 5m 2s —— View job


Claude Code Review

  • Read the new RTL test files
  • Analyze against migration goals & CLAUDE.md conventions
  • Check for behavior/coverage gaps vs. the deleted enzyme suites
  • Post review

Summary

This is a clean, high-quality tranche of the enzyme→RTL migration. All 24 new suites follow the established tests/jest/ conventions consistently (userEvent.setup(), screen/within role queries, jest-dom matchers), and the harder cases are handled thoughtfully: the real DndProvider/HTML5Backend for the AdvancedSearch tree, the inline LegacyContextProvider supplying router/pathFor/admin for ManagePatronsTabContainer, the marker mocks for fetch-on-mount panels, and the rounded-out FormDataShim that mirrors "successful controls" semantics. Tab-active state is asserted through the visible panel rather than internal state, which is the right RTL posture, and the deferrals (ConfigTabContainer, BookDetailsTabContainer, AdvancedSearchBuilder) are well justified in the description. I confirmed the latest commit's Test workflow is green, and every point from the earlier reviews is addressed in the code: the fireEvent.keyDown spacebar tests are now user.keyboard(" ") with a focused element, the FormDataShim now implements the full read/iteration surface (has/getAll/delete/entries/keys/values/Symbol.iterator), the modal button assertions use getByRole("button", …), and the mocked CustomListsSidebar Link is an <a href>.

I found no bug-level or otherwise actionable issues in the new suites — nothing to change. Nice work.
· chore/rewrite-mount-based-tests-rtl

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR rewrites 24 of 28 Enzyme mount-based test suites to React Testing Library, moving them from src/components/__tests__/ to tests/jest/components/. The three deferred suites (ConfigTabContainer, BookDetailsTabContainer, AdvancedSearchBuilder) retain their Enzyme originals, and LibraryStats-test.tsx is deleted outright since Stats.test.tsx already provides 100% line coverage.

  • All new tests use the const user = userEvent.setup() pattern for user interactions; AdvancedSearch filter tests wrap components in a real DndProvider/HTML5Backend to avoid the react-dnd context requirement. Previous review thread noted inconsistency with fireEvent.keyDown for spacebar tests — the new files use user.keyboard(\" \") throughout.
  • Tab container tests (DiagnosticsTabContainer, ManagePatronsTabContainer) mock the connected/fetching child panels to markers and assert navigation via router.push, matching the original enzyme intent without triggering on-mount fetches.
  • index.test.tsx replaces the Enzyme bootstrap test with a ReactDOM.render spy and a custom containsType tree-walker to assert SetupPage / Router are present in the captured element tree.

Confidence Score: 5/5

Pure test-code change with no production source modifications — safe to merge.

Every changed file is a test file. The three deferred suites retain their Enzyme originals, the deleted LibraryStats suite is demonstrably redundant, and the deleted AdvancedSearchBuilder RTL suite has its only meaningful test subsumed by the new AdvancedSearchFilterInput suite. No production behaviour is at risk.

No files require special attention.

Important Files Changed

Filename Overview
tests/jest/index.test.tsx New bootstrap test: spies on ReactDOM.render, captures the element tree, and uses a custom containsType walker to assert SetupPage/Router presence — equivalent to the Enzyme find() approach.
tests/jest/components/AdvancedSearchBooleanFilter.test.tsx RTL rewrite with DndProvider/HTML5Backend; spacebar tests correctly use user.keyboard(' ') rather than fireEvent, resolving the previous review thread concern.
tests/jest/components/AdvancedSearchValueFilter.test.tsx RTL rewrite with DndProvider; spacebar-select test uses user.keyboard(' ') for consistency with the rest of the file.
tests/jest/components/DiagnosticsTabContainer.test.tsx Tab container test mocks DiagnosticsServiceType to a data-attribute marker; asserts all four tabs, active class, prop forwarding, loading/error states, and goToTab navigation.
tests/jest/components/ManagePatronsTabContainer.test.tsx Provides a LegacyContextProvider to supply router/pathFor/admin legacy context; mocks both tab panels; asserts role-based tab visibility and router.push navigation.
tests/jest/components/SelfTestsCategory.test.tsx Mocks connected SelfTests component; tests color-coding, single-item open-by-default (correctly uses a second fresh render), and sortByCollection prop forwarding.
tests/jest/components/ManagePatronsForm.test.tsx Uses the shared FormData shim; covers rendering, form submission, error display, and success patron display. Shim limitation was already flagged in a previous review thread.
tests/jest/components/CustomListsSidebar.test.tsx Mocks react-router Link to a marker anchor; tests filter/sort selects, list rendering, edit/delete behavior, disabled/editing states, and manager-only delete visibility.
tests/jest/components/AdvancedSearchBuilder.test.tsx Deleted — its sole publication-date placeholder/help-text test is now covered more precisely in AdvancedSearchFilterInput.test.tsx; enzyme AdvancedSearchBuilder tests for dnd callbacks are retained.
src/components/tests/LibraryStats-test.tsx Deleted — coverage provided by the existing Stats.test.tsx which exercises the component at 100% line coverage with the query hook and sysadmin gating.
tests/jest/components/ServiceWithRegistrationsEditForm.test.tsx Uses a concrete DiscoveryServiceEditForm subclass as the legacy test did; asserts registration UI, library status badges, and button enabled states.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Enzyme mount test\nsrc/components/__tests__/] -->|deleted| B[RTL test\ntests/jest/components/]
    B --> C1[AdvancedSearch filters\nDndProvider + HTML5Backend]
    B --> C2[Leaf components\nWithEditButton · WithRemoveButton\nLoadButton · Footer · ErrorMessage]
    B --> C3[Self-test panels\nSelfTestResult · SelfTestsCategory]
    B --> C4[Search & sidebar\nCustomListSearch · CustomListsSidebar]
    B --> C5[Services views\nMetadataServices · CatalogServices\nManagePatronsForm]
    B --> C6[Tab containers\nTabContainer · DiagnosticsTabContainer\nManagePatronsTabContainer]
    B --> C7[Bootstrap\nindex.test.tsx]
    D[Deferred - enzyme retained] --> D1[ConfigTabContainer]
    D --> D2[BookDetailsTabContainer]
    D --> D3[AdvancedSearchBuilder]
    E[LibraryStats-test.tsx] -->|covered by Stats.test.tsx| F[deleted]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Enzyme mount test\nsrc/components/__tests__/] -->|deleted| B[RTL test\ntests/jest/components/]
    B --> C1[AdvancedSearch filters\nDndProvider + HTML5Backend]
    B --> C2[Leaf components\nWithEditButton · WithRemoveButton\nLoadButton · Footer · ErrorMessage]
    B --> C3[Self-test panels\nSelfTestResult · SelfTestsCategory]
    B --> C4[Search & sidebar\nCustomListSearch · CustomListsSidebar]
    B --> C5[Services views\nMetadataServices · CatalogServices\nManagePatronsForm]
    B --> C6[Tab containers\nTabContainer · DiagnosticsTabContainer\nManagePatronsTabContainer]
    B --> C7[Bootstrap\nindex.test.tsx]
    D[Deferred - enzyme retained] --> D1[ConfigTabContainer]
    D --> D2[BookDetailsTabContainer]
    D --> D3[AdvancedSearchBuilder]
    E[LibraryStats-test.tsx] -->|covered by Stats.test.tsx| F[deleted]
Loading

Reviews (9): Last reviewed commit: "Address PR review feedback on the RTL mi..." | Re-trigger Greptile

Comment thread tests/jest/components/ManagePatronsForm.test.tsx Outdated
Comment thread tests/jest/components/AdvancedSearchBooleanFilter.test.tsx
@codecov-commenter

codecov-commenter commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.14%. Comparing base (7014b93) to head (5af98cd).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #314      +/-   ##
==========================================
- Coverage   90.19%   90.14%   -0.06%     
==========================================
  Files         205      205              
  Lines        7610     7610              
  Branches     1664     1664              
==========================================
- Hits         6864     6860       -4     
  Misses        437      437              
- Partials      309      313       +4     
Flag Coverage Δ
jest 90.14% <ø> (-0.06%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jonathangreen jonathangreen force-pushed the chore/rewrite-mount-based-tests-rtl branch 2 times, most recently from a23adc7 to 1912007 Compare July 9, 2026 19:32
@jonathangreen jonathangreen force-pushed the chore/rewrite-mount-based-tests-rtl branch from 1912007 to b669d2c Compare July 9, 2026 19:54
@jonathangreen jonathangreen requested a review from a team July 9, 2026 22:20
@jonathangreen jonathangreen force-pushed the chore/rewrite-mount-based-tests-rtl branch from b669d2c to c567ffb Compare July 10, 2026 14:10
@jonathangreen jonathangreen changed the base branch from chore/jest-remove-mocha to chore/formdata-shim-test-util July 10, 2026 14:10
@jonathangreen jonathangreen force-pushed the chore/formdata-shim-test-util branch from d45ae2c to 3739c04 Compare July 10, 2026 14:43
@jonathangreen jonathangreen force-pushed the chore/rewrite-mount-based-tests-rtl branch 2 times, most recently from 2be4ee3 to 2085ccb Compare July 10, 2026 15:05
jonathangreen added a commit that referenced this pull request Jul 10, 2026
## Description

Adds one shared FormData test shim at
`tests/jest/testUtils/formDataShim.ts`, plus its spec test.

The unit jsdom environment installs undici's `FormData`, which throws on
`new FormData(formElement)`. The reusable-components `Form` builds
FormData that way on submit, so any test that asserts a submitted
payload needs a stand-in. Previously each migration branch carried its
own inline copy; this consolidates them into one.

The shim mirrors the browser's `new FormData(form)`
**successful-controls** rules:
- named, non-disabled controls only;
- checkboxes/radios contribute only when checked;
- submit/reset/button/image/file inputs excluded;
- every selected option of a `<select multiple>` included.

It also exposes the standard read/write/iterate surface
(`get`/`getAll`/`has`/`append`/`set`/`delete`/`forEach`/`keys`/`values`/`entries`/iterator).
`installFormDataShim()` swaps it in around a suite (`beforeAll`) and
restores the real `FormData` afterward (`afterAll`).

`formDataShim.test.ts` (8 cases) pins the successful-controls behavior —
disabled omitted, checkbox only when checked, radio group, submit
excluded, multi-select expansion, and the read/write API — so the
fidelity can't silently regress.

## Motivation and Context

Part of the mocha/chai/enzyme → jest/RTL migration. Extracting the shim
into its own small, self-tested base PR lets #314, #315, and #316 depend
on one implementation via `installFormDataShim()` instead of duplicating
(and independently drifting) a local shim in each — while keeping this
base stable so those stacked PRs don't churn.

## How Has This Been Tested?

`npx jest --selectProjects=unit --testPathPattern formDataShim` — 8/8
green.

## Checklist:

- [x] I have updated the documentation accordingly.
- [x] All new and existing tests passed.
Base automatically changed from chore/formdata-shim-test-util to main July 10, 2026 20:22
24 of the 28 mount-based enzyme suites: the AdvancedSearch filter widgets
(rendered inside a real DndProvider — HTML5Backend works under jsdom), the
small leaf components and buttons, the self-test and manage-patrons panels, the
search/sidebar/services views, three of the tab containers, Footer, and the app
bootstrap (index.tsx).

The tab containers navigate via router.push rather than internal state, so
"active tab" is asserted through the visible panel; where a tab panel fetches on
mount and is incidental to a navigation test it is mocked to a marker. LibraryStats
is deleted outright rather than ported — tests/jest/components/Stats.test.tsx
already covers LibraryStats.tsx at 100% of lines and tests more (the query hook,
caching, sysadmin gating). The bootstrap test keeps its structural shape: it
spies on ReactDOM.render, constructs CirculationAdmin in each mode, and asserts
the captured element tree contains SetupPage / a Router — the analog of the
original enzyme find(), since what those roots render is covered by their own
tests.

Coverage rises to 94.48% (+24 lines over the baseline) with no previously-
covered line lost; rendering real leaf children reaches branches the shallow
and mount-with-stubbed-children tests did not.

Deferred, with their enzyme originals retained: ConfigTabContainer and
BookDetailsTabContainer (their full mounts are the sole cover for eleven
connected panels' mapStateToProps/mapDispatchToProps wiring — that coverage is
restored properly when those panels are migrated to render their connected
exports in later waves) and AdvancedSearchBuilder (its one uncovered line is a
react-dnd move callback reachable only through a real drop, grouped with the
other drag-dependent coverage in the final wave). Mocking those panels now would
have dropped 77 lines; the coverage gate caught it.

57 legacy suites remain.
@jonathangreen jonathangreen force-pushed the chore/rewrite-mount-based-tests-rtl branch from 2085ccb to 13c6a7f Compare July 11, 2026 11:39

@tdilauro tdilauro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good!

A few minor things, but no showstoppers.

Comment on lines +65 to +71
expect((options[0] as HTMLOptionElement).selected).toBe(true);

expect(options[1]).toHaveValue("or");
expect(options[1]).toHaveTextContent(
"Any of these filters may be matched:"
);
expect((options[1] as HTMLOptionElement).selected).toBe(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor - A couple of these (the true/false) assertions are unneeded because of the expect(select).toHaveValue("and"); at the end of the test:

Suggested change
expect((options[0] as HTMLOptionElement).selected).toBe(true);
expect(options[1]).toHaveValue("or");
expect(options[1]).toHaveTextContent(
"Any of these filters may be matched:"
);
expect((options[1] as HTMLOptionElement).selected).toBe(false);
expect(options[1]).toHaveValue("or");
expect(options[1]).toHaveTextContent(
"Any of these filters may be matched:"
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done.

Comment on lines +34 to +36
expect(
screen.getByRole("button", { name: "Download" })
).toBeInTheDocument();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor - This is duplicating the check for one of the buttons above, but it is the way I would prefer (over the approach above) to check for the buttons with Jest/RTL.

expect(screen.getByRole("button", { name: "Download" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument() doesn't work here, because the headers x dismiss button is also called Close. So using that causes getByRole to throw because it matched two elements.

I made some changes here though to clean up the test a little bit and scope the getByRole down to the footer.

Comment on lines +11 to +13
<div data-testid="Link" data-to={props.to} className={props.className}>
{props.children}
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor - It would make more sense for this to be an <a> than a <div>.

Suggested change
<div data-testid="Link" data-to={props.to} className={props.className}>
{props.children}
</div>
<a data-testid="Link" data-to={props.to} className={props.className}>
{props.children}
</a>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I switched this to an <a>, but I also needed to add a href. Otherwise linting fails on jsx-a11y/anchor-is-valid.

- AdvancedSearchFilter: replace a masked not-selected assertion (readOnly
  forced it true regardless of id) with a dedicated non-readOnly mismatch
  test that actually exercises the query.id === selectedQueryId false branch
- AdvancedSearchBooleanFilter: drop the redundant option .selected
  assertions; select.toHaveValue("and") already covers it
- CirculationEventsDownloadForm: query the buttons by role, scoping Close to
  the modal footer to avoid the header dismiss button's colliding "Close" name
- CustomListsSidebar: render the mocked Link as an anchor (href for a11y)
@jonathangreen jonathangreen merged commit 9fdb519 into main Jul 13, 2026
4 checks passed
@jonathangreen jonathangreen deleted the chore/rewrite-mount-based-tests-rtl branch July 13, 2026 12:52
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.

3 participants