Skip to content

Rewrite the HARD-tier and remaining merge tests with React Testing Library#316

Closed
jonathangreen wants to merge 2 commits into
mainfrom
chore/rewrite-hard-tests-rtl
Closed

Rewrite the HARD-tier and remaining merge tests with React Testing Library#316
jonathangreen wants to merge 2 commits into
mainfrom
chore/rewrite-hard-tests-rtl

Conversation

@jonathangreen

@jonathangreen jonathangreen commented Jul 10, 2026

Copy link
Copy Markdown
Member

Description

The HARD-tier suites and the "remaining merge" cases, across two commits.

Rewrite more HARD-tier tests — 12 suites: the announcement/genre/contributor/paired-menu forms, ChangePasswordForm, NeighborhoodAnalyticsForm, TextWithEditMode, ToolTip, and three that merge into existing jest files (Lane, EditableInput, IndividualAdmins). Prototype stubs (EditableInput.prototype.getValue, TextWithEditMode) become real userEvent.type reading back through the component's ref API; state()/instance() reads become DOM assertions.

Rewrite the HARD-heavy and remaining merge tests — 20 more, the hardest of the migration: ServiceEditForm (1087 LOC), CustomListEntriesEditor (1461 LOC), Header, CatalogPage, EditorField, InputList, Lanes, LaneCustomListsEditor, LanesSidebar, LibraryRegistration, SelfTests, BookCoverEditor, ClassificationsForm, plus seven merges. Highlights:

  • Live-instance-method spies become observable behavior (asserting submitted FormData, removed/retained rows), with tiny test subclasses to drive branches the base method short-circuits.
  • react-beautiful-dnd (can't drag in jsdom) is handled by mocking DragDropContext to capture onDragStart/onDragEnd, then invoking them with synthetic drag results — CustomListEntriesEditor/LaneCustomListsEditor reach 100% this way.
  • Connected panels render their connected default export so mapStateToProps/mapDispatchToProps stay covered.

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

Motivation and Context

Part of the mocha/chai/enzyme → jest/RTL migration — the hardest suites, which enzyme mounted and which poked class internals. After this, the only legacy enzyme suites left are the deferred tab containers, whose panels are now all migrated with connected coverage.

How Has This Been Tested?

npx jest --selectProjects=unit — full suite green. Coverage rises to 95.21% (+75 lines over baseline) across the two commits; no previously-covered line lost. eslint + prettier clean.

Checklist:

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

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude finished @jonathangreen's task in 25m 18s —— View job


Code Review

  • Read the PR diff and understand the scope
  • Review the high-risk suites (BookCoverEditor, CustomListEntriesEditor, ServiceEditForm, drag mocks)
  • Check merged/modified suites for lost coverage or weakened assertions
  • Post review

Summary

This is a large but clean test-only migration. I reviewed the two big suites (ServiceEditForm, CustomListEntriesEditor), the ten merged/modified files, and every newly-added suite — tracing load-bearing assertions against the real components rather than the mocks. The react-beautiful-dnd and CatalogLink mocks match the shapes the components actually read; the drag-handler tests drive real branch logic through captured callbacks; connected-export tests genuinely exercise mapStateToProps/mapDispatchToProps (e.g. SelfTests waits on "2.50s", which only appears if the swap branch runs); the test subclasses in ServiceEditForm assert DOM outcomes driven by real gating rather than re-asserting the override; and userEvent/findBy/waitFor are consistently awaited with no zero-iteration forEach no-ops. On the draggableStyle question raised in-thread: react-beautiful-dnd@^2 does expose it top-level on provided, so the mock is correct. I have one minor, non-blocking suggestion below.

Details

Minor: tests/jest/components/BookCoverEditor.test.tsx:379-390

The "saves the cover" test asserts cover_url, title_position, rights_status, and rights_explanation in the posted payload but never asserts anything about cover_file. save() unconditionally does data.append("cover_file", imageFormData.get("cover_file")) (BookCoverEditor.tsx:298), and while the shim can't reproduce a File value, it does record the key — so deleting that append line would slip through the current test undetected. Adding a presence check closes that gap without depending on file semantics:

expect(editCall[1].body.has("cover_file")).toBe(true);

const editCall = fetchSpy.mock.calls.find(([u]) =>
String(u).includes("change_cover")
);
expect(editCall).toBeTruthy();
expect(editCall[1].method).toBe("POST");
expect(editCall[1].body.get("cover_url")).toBe("http://example.com");
expect(editCall[1].body.get("title_position")).toBe("center");
expect(editCall[1].body.get("rights_status")).toBe(
"http://creativecommons.org/licenses/by/4.0/"
);
expect(editCall[1].body.get("rights_explanation")).toBe("explanation");

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR rewrites the HARD-tier and remaining merge tests with React Testing Library. The main changes are:

  • Replaces Enzyme component tests under src/components/__tests__ with Jest tests under tests/jest/components.
  • Moves class-instance and state assertions toward DOM and user-event assertions.
  • Adds connected-component coverage for several Redux-backed panels.
  • Uses mocked drag callbacks for react-beautiful-dnd paths that cannot run in jsdom.

Confidence Score: 4/5

This is close, but the uploaded-cover test gap should be fixed before merging.

  • The BookCoverEditor test still does not assert the cover_file payload.
  • The current shim can make file-upload saves pass with data that does not match the browser form submission.
  • The drag-and-drop mock shape matches the installed library contract for the reviewed path.

tests/jest/components/BookCoverEditor.test.tsx

Important Files Changed

Filename Overview
tests/jest/components/BookCoverEditor.test.tsx Rewrites BookCoverEditor tests with connected Redux coverage, but the uploaded-cover save payload is still not exercised.
tests/jest/components/LaneCustomListsEditor.test.tsx Rewrites LaneCustomListsEditor tests with mocked drag callbacks and RTL assertions for list add, remove, and ref behavior.

Reviews (7): Last reviewed commit: "Rewrite the HARD-heavy and remaining mer..." | Re-trigger Greptile

Comment thread tests/jest/components/BookCoverEditor.test.tsx Outdated
Comment thread tests/jest/components/LaneCustomListsEditor.test.tsx
@codecov-commenter

codecov-commenter commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.90%. Comparing base (7014b93) to head (4e4fdfa).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #316      +/-   ##
==========================================
+ Coverage   90.19%   90.90%   +0.70%     
==========================================
  Files         205      205              
  Lines        7610     7610              
  Branches     1664     1664              
==========================================
+ Hits         6864     6918      +54     
+ Misses        437      394      -43     
+ Partials      309      298      -11     
Flag Coverage Δ
jest 90.90% <ø> (+0.70%) ⬆️

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-hard-tests-rtl branch from 7826d2d to 637e028 Compare July 10, 2026 14:13
@jonathangreen jonathangreen changed the base branch from chore/jest-remove-mocha to chore/formdata-shim-test-util July 10, 2026 14:13
@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-hard-tests-rtl branch from 637e028 to 5655faf Compare July 10, 2026 14:54
Comment thread tests/jest/components/BookCoverEditor.test.tsx
Comment thread tests/jest/components/LaneCustomListsEditor.test.tsx Outdated
@jonathangreen jonathangreen force-pushed the chore/rewrite-hard-tests-rtl branch from 5655faf to 85f9e49 Compare July 10, 2026 15:15
Comment thread tests/jest/components/LaneCustomListsEditor.test.tsx
@jonathangreen jonathangreen marked this pull request as draft July 10, 2026 15:19
@jonathangreen jonathangreen force-pushed the chore/rewrite-hard-tests-rtl branch 2 times, most recently from 48ea4b9 to 5643d14 Compare July 10, 2026 15:49
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
Twelve more enzyme suites migrated: the announcement/genre/contributor/paired-menu
forms, ChangePasswordForm, NeighborhoodAnalyticsForm, TextWithEditMode, ToolTip,
and three that merge into their existing jest files (Lane, EditableInput,
IndividualAdmins — which already had a jest test for a newer feature, so the
legacy behaviors were appended as a new describe block rather than duplicated).

The recurring rewrites: prototype stubs (EditableInput.prototype.getValue,
TextWithEditMode) become real userEvent.type into the field, reading the value
back through the component's real ref API; state()/instance() reads become DOM
assertions; window.confirm via jest.spyOn. IndividualAdmins' only gap was its
connect wiring (the RBAC gating is covered by roleBasedAccess.test.ts), so a
single connected-default-export render closes it.

Coverage 94.73% (+41 lines over baseline), no previously-covered line lost.

24 legacy suites remain.
…ibrary

Twenty more enzyme suites, the hardest of the migration: ServiceEditForm (1087
LOC), CustomListEntriesEditor (1461 LOC), Header, CatalogPage, EditorField,
InputList, Lanes, LaneCustomListsEditor, LanesSidebar, LibraryRegistration,
SelfTests, BookCoverEditor, ClassificationsForm — plus seven that merge into an
existing tests/jest file (Collections, CustomLists, DiscoveryServices,
EditableConfigList, IndividualAdminEditForm, LaneEditor, ProtocolFormField).

The techniques that made the hard cases tractable:

- Live-instance-method spies (ServiceEditForm's handleData / removeLibrary /
  isLibraryRemovalPermitted) become observable behavior — asserting the submitted
  FormData and the removed/retained library row — with tiny test subclasses to
  drive the removal-permitted branches, since the base method always returns true.
- react-beautiful-dnd, which can't drag in jsdom, is handled by mocking
  DragDropContext to capture the component's onDragStart/onDragEnd (and rendering
  the render-prop children so the DOM is intact), then calling those callbacks
  with synthetic drag results. CustomListEntriesEditor and LaneCustomListsEditor
  reach 100% this way; Lanes drives its `drag` callback through a ref (mirroring
  what LanesSidebar does in production).
- CatalogPage's OPDSCatalog and the web-opds-client require() are mocked to a
  marker that echoes the computed URLs and reads the tab/library child context;
  the legacy global.jsdom.reconfigure origin trick is replaced with the real
  jsdom origin (the expand helpers only strip the origin).
- Connected panels (SelfTests, BookCoverEditor, and the merges) render their
  connected default export so mapStateToProps/mapDispatchToProps stay covered —
  SelfTests' wiring is what the SelfTestsTabContainer full-mount used to carry.
- ClassificationsForm's setTimeout(500) focus path uses fake timers and a focus
  spy; forms install the FormData shim the unit env needs.

Coverage rises to 95.21% (+75 lines over the 94.22% baseline), no previously-
covered line lost. The gate caught every drag/edge line the first pass dropped
(Lanes, LaneCustomListsEditor, LibraryRegistration, LanesSidebar), each then
covered with the capture-callback or a targeted branch test.

Only 4 legacy enzyme suites remain: the deferred tab containers
(ConfigTabContainer, BookDetailsTabContainer, SelfTestsTabContainer,
AdvancedSearchBuilder), whose panels are now all migrated with connected
coverage, so they can finally be rewritten mocking their panels.
@jonathangreen jonathangreen force-pushed the chore/rewrite-hard-tests-rtl branch from 5643d14 to 4e4fdfa Compare July 11, 2026 11:42
Comment on lines +346 to +392
it("saves the cover with the entered values", async () => {
const user = userEvent.setup();
const { store, refreshCatalog } = renderEditor();
await awaitMounted();

// A preview must exist for the save button to be enabled.
act(() => {
store.dispatch({ type: "PREVIEW_BOOK_COVER_LOAD", data: "image data" });
});

await user.type(
document.querySelector('input[name="cover_url"]') as HTMLElement,
"http://example.com"
);
await user.selectOptions(
document.querySelector('select[name="title_position"]') as HTMLElement,
"center"
);
await user.selectOptions(
document.querySelector('select[name="rights_status"]') as HTMLElement,
"http://creativecommons.org/licenses/by/4.0/"
);
await user.type(
document.querySelector(
'textarea[name="rights_explanation"]'
) as HTMLElement,
"explanation"
);

await user.click(screen.getByRole("button", { name: "Save this cover" }));

// The edited cover is posted to the change-cover link with the form values,
// and on success the catalog is refreshed.
const editCall = fetchSpy.mock.calls.find(([u]) =>
String(u).includes("change_cover")
);
expect(editCall).toBeTruthy();
expect(editCall[1].method).toBe("POST");
expect(editCall[1].body.get("cover_url")).toBe("http://example.com");
expect(editCall[1].body.get("title_position")).toBe("center");
expect(editCall[1].body.get("rights_status")).toBe(
"http://creativecommons.org/licenses/by/4.0/"
);
expect(editCall[1].body.get("rights_explanation")).toBe("explanation");

await waitFor(() => expect(refreshCatalog).toHaveBeenCalledTimes(1));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Cover File Still Missing

This save test only exercises cover_url and never selects or asserts the cover_file field. The suite installs a FormData shim that omits file inputs, while a browser new FormData(imageForm) includes the uploaded cover as a File. A change that drops the uploaded cover from BookCoverEditor.save() would still pass this test because the posted body is only checked for URL, title position, and rights fields. Please add a file-upload save case or preserve File entries in the test shim so uploaded-cover submissions are asserted.

@jonathangreen

Copy link
Copy Markdown
Member Author

I'm going to close this monster, and try to split it into a few smaller more reviewable PRs.

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