Skip to content

Add React Testing Library migration infrastructure and first conversions - #385

Merged
cigamit merged 2 commits into
ctrliq:mainfrom
blaipr:feature/rtl-migration
Jun 12, 2026
Merged

Add React Testing Library migration infrastructure and first conversions#385
cigamit merged 2 commits into
ctrliq:mainfrom
blaipr:feature/rtl-migration

Conversation

@blaipr

@blaipr blaipr commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

Stage 3 groundwork of the React stack modernization: start retiring enzyme, which is dead upstream and has no adapter beyond React 17 — it is the single hard blocker for ever moving to React 18/19 (495 test files depend on it).

This PR makes incremental conversion possible and establishes the pattern:

  • testUtils/rtlContexts.jsrenderWithContexts(), the React Testing Library counterpart of mountWithContexts: same context defaults and override mechanism (i18n, session, config, router with optional custom history), plus a ready userEvent instance and the history object in the return value.
  • @testing-library/jest-dom matchers loaded globally in setupTests.js (suites no longer import them individually; existing per-file imports keep working).
  • Seven representative suites converted as the pattern: ContentEmpty, ContentLoading, AlertModal, FormActionGroup, ChipGroup, ToolbarSyncSourceButton (interaction) and DeleteButton (modal open → confirm flow → async error). Where the enzyme versions only asserted wrapper length or component props, the RTL versions assert rendered behavior — e.g. ChipGroup's old test asserted a prop on an effectively empty render; it now renders actual chips and asserts the visible overflow text.

Conversion recipe

enzyme RTL
mountWithContexts(<X/>) renderWithContexts(<X/>)
wrapper.find('button').simulate('click') await user.click(screen.getByRole('button', { name }))
waitForElement(wrapper, 'X') await screen.findBy...
asserting .props() / .length assert visible text/roles
act(...) wrapping usually unnecessary

Both helpers coexist; the remaining ~490 enzyme suites convert incrementally (screen directory at a time works well). When the last one converts, enzyme + the React 17 adapter come out of package.json — unblocking React 18.

Independent of all open PRs — test-only changes (no production code, no dependency changes). Note for the future: when the react-router compat migration (#392) merges, rtlContexts should gain the same controlled v6 Router layer its enzyme counterpart gets there — a 5-line follow-up.

ISSUE TYPE

  • New or Enhanced Feature

COMPONENT NAME

  • UI

ASCENDER VERSION

awx: 25.4.1.dev5+gcda0899.d20260610

ADDITIONAL INFORMATION

npm --prefix awx/ui run lint     # clean
npm --prefix awx/ui run test     # 544 suites passed (1 skipped), 2855 tests passed (19 skipped)

Production build unaffected (test files and test utilities only).

Groundwork for retiring enzyme (dead upstream, no adapter beyond React
17 - it is the hard blocker for React 18+):

- testUtils/rtlContexts.js: renderWithContexts(), the RTL counterpart of
  mountWithContexts. Same context defaults and override mechanism
  (i18n, session, config, router with optional custom history), plus a
  ready userEvent instance and the history object in the return value.
- @testing-library/jest-dom matchers are loaded globally in setupTests
  (individual suites no longer need to import them).
- Seven representative suites converted from enzyme to RTL as the
  pattern: ContentEmpty, ContentLoading, AlertModal, FormActionGroup,
  ChipGroup, ToolbarSyncSourceButton (interaction) and DeleteButton
  (modal open, confirm flow, async error). Where the enzyme versions
  only asserted wrapper length or props, the RTL versions assert
  rendered behavior; ChipGroup's old test asserted a prop on an empty
  render and now renders actual chips and asserts the visible overflow
  text.

Conversion recipe:
  mountWithContexts(<X/>)            -> renderWithContexts(<X/>)
  wrapper.find('button').simulate    -> await user.click(screen.getByRole('button', { name }))
  waitForElement(wrapper, 'X')       -> await screen.findBy...
  wrapper.find(...).props() asserts  -> assert visible text/roles instead
  act(...) wrapping                  -> usually unnecessary (RTL wraps)

545 suites remain on enzyme; they convert incrementally with both
helpers coexisting.

Copilot AI 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.

Pull request overview

This PR lays the groundwork for incrementally migrating Ascender’s UI test suite from Enzyme (React 17-bound) to React Testing Library by introducing a shared RTL render helper and converting an initial set of representative component tests.

Changes:

  • Added renderWithContexts() helper to mirror Enzyme’s mountWithContexts() provider wiring for RTL tests.
  • Loaded @testing-library/jest-dom matchers globally via setupTests.js.
  • Converted 7 component test suites from Enzyme to RTL patterns (behavior/DOM assertions + userEvent interactions).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
awx/ui/testUtils/rtlContexts.js Adds RTL context render helper used for incremental Enzyme → RTL conversions.
awx/ui/src/setupTests.js Globally registers @testing-library/jest-dom matchers for all UI tests.
awx/ui/src/components/PaginatedTable/ToolbarSyncSourceButton.test.js Converts suite to RTL and userEvent click interaction.
awx/ui/src/components/FormActionGroup/FormActionGroup.test.js Converts to RTL with accessible-button clicks and handler assertions.
awx/ui/src/components/DeleteButton/DeleteButton.test.js Converts modal flow tests to RTL patterns (findByRole, waitFor).
awx/ui/src/components/ContentLoading/ContentLoading.test.js Converts to RTL with role-based assertion.
awx/ui/src/components/ContentEmpty/ContentEmpty.test.js Converts to RTL with visible-text assertion.
awx/ui/src/components/ChipGroup/ChipGroup.test.js Converts to RTL and asserts visible collapsed count text.
awx/ui/src/components/AlertModal/AlertModal.test.js Converts to RTL and asserts dialog/title/body rendering.

Comment thread awx/ui/testUtils/rtlContexts.js
Comment thread awx/ui/testUtils/rtlContexts.js
Comment thread awx/ui/src/components/PaginatedTable/ToolbarSyncSourceButton.test.js Outdated
Comment thread awx/ui/src/components/DeleteButton/DeleteButton.test.js
- rtlContexts now loads Lingui plural rules like enzymeHelpers does,
  behind a guard that degrades cleanly when the Lingui 6 upgrade removes
  loadLocaleData and the make-plural dependency
- adds testUtils/rtlContexts.test.js covering context defaults and
  overrides, router/history wiring and plural rendering - and extends
  the jest roots/testMatch to <rootDir>/testUtils, which revealed that
  enzymeHelpers.test.js had never been executed (the old async-done
  test style is rejected by modern Jest; both tests fixed, 8 tests
  revived)
- ToolbarSyncSourceButton test queries the button by accessible name
- DeleteButton test attaches .response to a real Error so the network
  error path is actually exercised; fixes the URL typo
@blaipr

blaipr commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

All four review comments addressed: (1) rtlContexts now loads Lingui plural rules like enzymeHelpers, behind a guard that degrades cleanly when the Lingui 6 upgrade (#391) removes loadLocaleData/make-plural; (2) added testUtils/rtlContexts.test.js covering context defaults/overrides, router wiring and plural rendering — and in doing so discovered that testUtils was never in Jest's roots, so enzymeHelpers.test.js had never actually run; the roots/testMatch now include it, and the two rotted async-done tests were fixed (8 tests revived); (3) the ToolbarSyncSourceButton test queries by role + accessible name; (4) the DeleteButton mock attaches .response to a real Error (URL typo fixed too), so the network-error path is genuinely exercised. Full suite: 2,869 passed.

@blaipr

blaipr commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

All review feedback on this PR is addressed, and after the latest merges to main (#376, #380, #386, #389, #391, #394) this branch has been re-verified: it merges cleanly into current main (51188f0) and is conflict-free against every other open PR (git merge-tree, all pairs). Ready to merge — in any order relative to #385/#390/#392/#393/#395.

@cigamit cigamit left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tested and working. We do get this warning, but I can remove the file in another PR.

Snapshot Summary
› 1 snapshot file obsolete from 1 test suite. To remove it, re-run jest with -u.
↳ • testUtils/snapshots/enzymeHelpers.test.jsx.snap

@cigamit
cigamit merged commit 3be02ff into ctrliq:main Jun 12, 2026
blaipr added a commit to blaipr/ascender that referenced this pull request Jun 13, 2026
renderWithContexts mounted only the v5 Router, so components migrated to
the react-router-dom-v5-compat APIs threw 'useNavigate() may be used only
in the context of a <Router>' under the RTL suites that ctrliq#385 introduced
(rtlContexts.js did not exist when this branch was cut). Nest the same
controlled v6 Router used by enzymeHelpers — location from the v5
context, navigator = the shared history — and add a regression test
exercising useNavigate/useLocation from the compat package.

Also drops the dead Lingui v5 make-plural guard and fixes the stale
import/* eslint rule names, matching the identical cleanup already on
the rtl-batch-user branch so the hunks merge cleanly.
cigamit pushed a commit that referenced this pull request Jun 13, 2026
…tree files) (#392)

* Migrate react-router usage to v6 compat APIs (bridge + all non-route-tree files)

Combines the v5-compat bridge and the three migration batches into one
change covering ~180 of the 237 files that used the router APIs removed
in react-router 6/7 (Switch, useHistory, useRouteMatch, Redirect,
withRouter).

The bridge:
- react-router-dom-v5-compat added (router v6 running alongside v5,
  React 17 compatible) with CompatRouter mounted inside the app's
  HashRouter, so components migrate file by file
- testUtils/enzymeHelpers renders a controlled nested v6 Router
  (location from the v5 context, navigator = the shared history) inside
  the v5 Router: no second history subscription, so no act() warnings,
  and enzyme shallow rendering keeps working
- licenses/ui entry for the new dependency (test_licenses enforces it)

The migration (components, contexts, hooks and all screen directories):
- useHistory() -> useNavigate(); history.push/replace/goBack ->
  navigate(...) with { replace: true } where applicable;
  history.go() -> navigate(0)
- history.location reads -> useLocation()
- zero-argument useRouteMatch used only for .params -> useParams()
- conditional Redirect -> Navigate (only in files without a Switch)
- withRouter wrappers replaced with hooks, or dropped where the
  component never read the injected props (AppContainer, Templates,
  Organizations, Projects)
- contexts/Session's history.listen POP detection rewritten with
  useLocation/useNavigationType, skipping the initial render to keep
  v5's fire-on-change-only semantics
- UIEdit's custom hardReload flag moves to navigation state (v6 drops
  custom location fields); TopologyView's redirectToDetailsPage helper
  takes navigate instead of a history object

Two correctness findings baked in:
- navigate() from the compat package is not referentially stable (its
  identity changes with the location), so redirect-on-result effects
  that previously listed the stable v5 history in their deps drop
  navigate with an explanatory eslint-disable - including it refires
  the redirect after unrelated navigations (caught by an existing
  cancel-flow test)
- screens/Login migrated as the first slice surfaced a test.only that
  had silenced 14 Login tests since the initial import; it is removed,
  two assertions that rotted while dark are fixed, and the suite now
  runs 2869 tests (14 more than before)

Deliberately left on v5 for the final route-tree phase: Switch/Route
definitions, Redirect from= entries inside Switch, match.path/match.url
route building, and argument-form useRouteMatch prefix checks. After
that phase, react-router-dom flips to v6 and the compat package is
removed.

* Address review: fix resource-param check, consolidate useLocation

- JobTemplateAdd: location.search.includes('resource_id' && 'resource_name')
  only ever tested for resource_name ('a' && 'b' evaluates to 'b'); now
  requires both params before building resource defaults
- PaginatedTable: single useLocation() call instead of two

* Address review: fix eslint-disable placement for navigate dep comments

The explanation for omitting navigate from effect deps was wrapped onto
comment lines after the eslint-disable-next-line directive, which makes
the directive target the comment instead of the dependency array. Move
the explanation above the directive and keep the directive as a single
bare line directly before the code it suppresses.

Comment-only change, no runtime impact.

* Wire the v5-compat bridge into the RTL test harness

renderWithContexts mounted only the v5 Router, so components migrated to
the react-router-dom-v5-compat APIs threw 'useNavigate() may be used only
in the context of a <Router>' under the RTL suites that #385 introduced
(rtlContexts.js did not exist when this branch was cut). Nest the same
controlled v6 Router used by enzymeHelpers — location from the v5
context, navigator = the shared history — and add a regression test
exercising useNavigate/useLocation from the compat package.

Also drops the dead Lingui v5 make-plural guard and fixes the stale
import/* eslint rule names, matching the identical cleanup already on
the rtl-batch-user branch so the hunks merge cleanly.

* Remove the orphaned enzymeHelpers.test.jsx.snap

The suite lives in enzymeHelpers.test.js; the .jsx twin snapshot is
obsolete and fails the run under jest's ci mode.
cigamit pushed a commit that referenced this pull request Jun 14, 2026
…ary (#398)

* Convert the User screen test suites from Enzyme to React Testing Library

Converts all 22 test suites under screens/User/ to RTL, continuing the
migration started in #385. Tests drive the real UI through roles and
labels instead of reaching into component props.

Infrastructure fixes surfaced by the conversion:

- setupTests.js: mockument's createRange stub lacks cloneRange and most
  of the Range API, crashing @testing-library/user-event's pointer
  handling. Restore jsdom's real Range with the two rect-method stubs
  the editor components need.
- testUtils/rtlContexts.js gains settleTooltips(): a PF Modal close
  restores focus to a Tooltip-wrapped button, and a tooltip pending at
  unmount logs a state-update warning into the NEXT test, producing
  order-dependent failures. Applied in the three list suites that end
  right after closing an error modal.
- testUtils/rtlContexts.js gains assertDetail() shared by the detail
  suites.
- DisassociateButton: drop the invalid ouiaId prop on PF Tooltip that
  leaked to the DOM (enzyme never opened the tooltip; user-event's real
  hover exposed the warning).
- Delete the orphaned enzymeHelpers.test.jsx.snap behind the perpetual
  'snapshot file obsolete' notice.

* Build UserForm submit payload from a copy instead of mutating Formik values

The submit handler mutated Formik's values object (deleting password,
assigning is_superuser/is_system_auditor). Deleting password flips the
still-mounted password field from controlled to uncontrolled after a
successful submit, which React warns about — surfaced once the merged
react-router-dom-v5-compat bridge drives a re-render of the mounted form
on navigate(). Destructure into a fresh submitValues and mutate only the
copy; Formik's own state is left intact.
@cigamit cigamit self-assigned this Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants