terriajs
Version:
Geospatial data visualization platform.
120 lines (77 loc) • 8.81 kB
Markdown
# 13. Replace react-test-renderer and react-shallow-testutils with @testing-library/react
Date: 2026-02-10
## Status
Accepted
## Context
TerriaJS component tests currently rely on two React testing libraries that are deprecated and incompatible with the React ecosystem's direction:
**react-test-renderer** provides a renderer that outputs React component trees as JavaScript objects. Tests use `create()` to render, then traverse the tree with methods like `findByType()`, `findByProps()`, `findAllByType()`, and `root.children`. Several files also use the `ReactTestRenderer` type for variable annotations and `act()` from this package.
**react-shallow-testutils** extends React's deprecated shallow renderer with traversal utilities (`findAllWithType`, `findAll`, `isComponentOfType`). TerriaJS also wraps these in custom utilities in `test/ReactViews/MoreShallowTools.ts` — `getShallowRenderedOutput()`, `getMountedInstance()`, `findAllEqualTo()`, and `findAllWithPropsChildEqualTo()`.
### Problems
1. **react-test-renderer is deprecated in React 19.** The React team has announced it will not be maintained going forward. The package was deprecated in React 18.x with a console warning directing users to `@testing-library/react`. Staying on these libraries blocks future React upgrades.
2. **Shallow rendering is removed in React 19.** `react-test-renderer/shallow` (the `createRenderer` API) is gone entirely. Tests using `react-shallow-testutils` and the `MoreShallowTools.ts` utility module will not work at all.
3. **Tests assert on implementation, not behavior.** The dominant patterns — `findByType(ComponentName)`, `findByProps({ someProp: value })`, `findAllWithType(InternalComponent)` — couple tests to internal component structure. These tests break when components are refactored even if user-visible behavior is unchanged. For example:
- `rendered.root.findByType(SearchForm)` — asserts that a specific child component type exists in the tree
- `findAllWithType(result, DataCatalogMember)` — counts internal component instances
- `findByProps({ active: true })` — inspects internal prop values
4. **Two testing paradigms create confusion.** The codebase has test files using both `@testing-library/react` and `react-test-renderer`. New contributors must learn both APIs, and there's no consistent pattern to follow.
5. **Custom utilities add maintenance burden.** `MoreShallowTools.ts` is a bespoke wrapper around deprecated APIs that must be understood and maintained independently.
## Decision
We will use `@testing-library/react` for all component tests. The deprecated `react-test-renderer` and `react-shallow-testutils` libraries will be removed from the codebase.
Tests will query and interact with components through their rendered DOM output using the accessibility tree (`getByRole`), visible text (`getByText`), and form labels (`getByLabelText`). Direct DOM queries (`container.querySelector`) remain acceptable for non-semantic elements and sanitization checks.
### Migration approach
The following patterns guide the conversion of existing tests:
**For tests using react-test-renderer (`create`, `findByType`, `findByProps`):**
| Before (react-test-renderer) | After (@testing-library/react) |
| --------------------------------- | ------------------------------------------- |
| `create(<Component />)` | `render(<Component />)` |
| `root.findByType(Component)` | `screen.getByRole()` / `screen.getByText()` |
| `root.findByProps({ name: "x" })` | `screen.getByRole("button", { name: "x" })` |
| `root.findAllByType("li")` | `screen.getAllByRole("listitem")` |
| `act()` from react-test-renderer | `act()` from `react` (React 18+) |
| `renderer.update(<Component />)` | `rerender(<Component />)` |
| `.click()` / `fireEvent.click()` | `userEvent.click()` |
**For tests using react-shallow-testutils (`findAllWithType`, `findAll`, `isComponentOfType`):**
| Before (react-shallow-testutils) | After (@testing-library/react) |
| -------------------------------------------- | ------------------------------------------------- |
| `getShallowRenderedOutput(<C />)` | `render(<C />)` |
| `findAllWithType(output, Tag)` | `screen.getAllByRole()` / `screen.getAllByText()` |
| `isComponentOfType(el, Component)` | Assert on rendered output via `screen` queries |
| `findAllEqualTo(output, text)` | `screen.getByText(text)` |
| `findAllWithPropsChildEqualTo(output, text)` | `screen.getByText(text)` |
**Query priority** (per Testing Library guiding principles):
1. `getByRole` — preferred; queries the accessibility tree
2. `getByText` — for visible text content
3. `getByLabelText` — for form elements
4. `container.querySelector` — last resort for elements with no accessible role (e.g., `<script>`, `<br>`, CSS class assertions)
### What stays
`container.querySelector` / `container.querySelectorAll` remains appropriate for:
- Script tag sanitization checks (`querySelectorAll("script").length`)
- Non-semantic element counting (`<br>`, `<b>`)
- CSS class presence checks (`.jj`, `.jk`)
- Complex dynamically-computed text assertions (locale-dependent number formatting, timezone-dependent date formatting)
### Cleanup after full migration
- Remove `react-test-renderer` and `@types/react-test-renderer` from `package.json`
- Remove `react-shallow-testutils` from `package.json`
- Delete `test/ReactViews/MoreShallowTools.ts`
## Alternatives considered
### Migrate directly to Vitest browser mode
Vitest is a completely new test runner that would replace Karma/Jasmine. Its browser mode runs tests in a real browser via Playwright/WebdriverIO and provides its own rendering and component testing library (`vitest-browser-react`) rather than `@testing-library/react`. This migration would involve three simultaneous changes: a new test runner, a new assertion API, and a new component testing API. Combining all of these at once would make the migration too large and impossible to review.
By decoupling the two concerns, the `@testing-library/react` conversion can be done as a standalone step that works with the existing Karma test runner. Each converted file can be reviewed and merged independently. A subsequent test runner migration can then focus purely on the runner change without also rethinking how every component test queries the DOM.
## Consequences
### Positive
- **Partially unblocks React 19 upgrade.** No dependency on deprecated rendering APIs.
- **Tests verify user-visible behavior.** Queries like `getByRole("button", { name: "Submit" })` assert what the user sees, not how the component tree is structured. Refactoring internals no longer breaks tests.
- **Single testing paradigm.** All component tests use the same API, reducing onboarding friction.
- **Accessibility coverage as a side effect.** `getByRole` queries fail when elements lack proper ARIA roles, surfacing accessibility issues during development.
- **Eliminates custom test utilities.** `MoreShallowTools.ts` and its bespoke traversal functions are no longer needed.
- **Aligns with React ecosystem.** `@testing-library/react` is the React team's recommended testing library and has broad community adoption.
### Negative
- **Migration effort.** Each file requires understanding what behavior the test was actually verifying, then rewriting queries to target that behavior through the DOM rather than the component tree.
- **Some tests need rethinking.** Tests that assert on internal component types (e.g., "renders a `SearchForm` component") must be rewritten to assert on the visible result of rendering that component. This is the correct trade-off but requires more thought per test.
- **`container` is still necessary in some cases.** Not all DOM assertions map cleanly to `screen` queries — sanitization checks, non-semantic element counts, and CSS class assertions still need direct DOM access.
### Risks
- **Behavioral equivalence is not guaranteed.** Shallow-rendered tests that asserted on component types were testing different things than full DOM renders with `screen` queries. Some tests may need to verify behavior differently rather than being a 1:1 translation.
## References
- [React 19 migration guide — removal of react-test-renderer](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-react-test-renderer)
- [Testing Library guiding principles](https://testing-library.com/docs/guiding-principles)
- [Testing Library query priority](https://testing-library.com/docs/queries/about#priority)