Migrate Enzyme tests to React Testing Library (RTL). Use when converting shallow/mount enzyme tests to RTL render, replacing enzyme selectors with RTL queries, updating snapshot tests, or when the user mentions enzyme migration, RTL migration, or react-testing-library.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Migrate Enzyme tests to React Testing Library (RTL). Use when converting shallow/mount enzyme tests to RTL render, replacing enzyme selectors with RTL queries, updating snapshot tests, or when the user mentions enzyme migration, RTL migration, or react-testing-library.
disable-model-invocation
true
Enzyme to React Testing Library Migration
Goal
Migrate enzyme tests to @testing-library/react as a 1:1 port — preserve existing test intent without refactoring toward integration-style testing or removing mocks.
Core principles
Preserve test intent. Do not rewrite test logic or remove mocks. Add mocks where enzyme's shallow rendering previously hid missing providers/contexts.
Cut dead tests. Enzyme tests component trees, not DOM. Tests that assert on elements never actually rendered in the DOM should be removed with a comment explaining why.
No new data-test-subj for snapshots. Use container.children[0] for root-element snapshots instead of adding a test locator just for snapshotting.
Migration workflow
Replace enzyme imports with RTL imports.
Replace shallow() / mount() with render().
Migrate selectors and assertions.
Update or delete snapshots (--updateSnapshot).
Run the test and fix any missing mocks/providers that enzyme's shallow rendering was hiding.
import { render, screen, within } from'@testing-library/react';
import userEvent from'@testing-library/user-event';
Keep @kbn/test-jest-helpers only for non-enzyme utilities (e.g. nextTick, StubBrowserStorage) and RTL render helpers (renderWithKibanaRenderContext, renderWithI18n, renderWithEuiTheme). Remove findTestSubject — use screen.getByTestId instead.
When the component needs i18n or EUI theme context, prefer the RTL helpers from @kbn/test-jest-helpers instead of manually wrapping in providers:
Helper
Wraps with
renderWithKibanaRenderContext(<Comp />)
EuiThemeProvider + I18nProvider — preferred default for most migrations
renderWithI18n(<Comp />)
I18nProvider only
renderWithEuiTheme(<Comp />)
EuiThemeProvider only
These are drop-in replacements for RTL's render() and accept the same arguments (including renderOptions). When the component needs additional providers (Redux, Router, custom contexts), add them as a wrapper option or inline in JSX.
Rendering
Enzyme
RTL
shallow(<Comp />)
render(<Comp />) or renderWithKibanaRenderContext(<Comp />)
mount(<Comp />)
render(<Comp />) or renderWithKibanaRenderContext(<Comp />)
shallowWithIntl(<Comp />)
renderWithI18n(<Comp />) or renderWithKibanaRenderContext(<Comp />)
mountWithIntl(<Comp />)
renderWithI18n(<Comp />) or renderWithKibanaRenderContext(<Comp />)
Use screen for queries (queries document.body, so portals are reachable too):
Make sure findTestSubject matcher behavior is preserved with a getByTestIdRegExp matcher when data-test-subj contains multiple tokens.
After interactions that trigger async updates, prefer findByTestId over getByTestId to avoid act() warnings from unresolved updates.
Kibana-specific fallback: subj() from @kbn/test-subj-selector converts test-subject selector syntax to a CSS selector (supports ~/*/>). Prefer RTL queries first; use this when you truly need CSS selection:
Note: Some EUI components reuse the same data-test-subj on both a wrapper and the actual control. If getByTestId throws “Found multiple elements”, use getAllByTestId/queryAllByTestId and narrow (or scope with within(...)) instead of switching to brittle CSS selectors.
CSS selectors
Enzyme
RTL
wrapper.find('.my-class')
container.querySelector('.my-class')
wrapper.find('button')
container.querySelector('button') or screen.getByRole('button')
wrapper.findAll('.item')
container.querySelectorAll('.item')
Complex traversals
Enzyme chains like wrapper.find('tbody tr td a').at(3).find('div span').at(2).text() become:
For elements rendered outside the component's container (portals), prefer screen / within(document.body):
// Portal content is in document.body, so screen queries can find itexpect(screen.getByTestId('modal-confirm')).toBeInTheDocument();
// Or scope explicitlywithin(document.body).getByTestId('modal-confirm');
If X is a test subject: screen.queryByTestId('x') !== null. If X is a CSS selector string: container.querySelector(X) !== null. If X is a React component (e.g. wrapper.find(EuiCallOut)), assert on DOM output (role/text/test subject) instead of component selectors.
wrapper.find(X).length
If X is a CSS selector string: container.querySelectorAll(X).length. If X is a React component (e.g. wrapper.find(EuiCallOut)), assert on DOM output (role/text/test subject) instead of component selectors.
fireEvent.change(input, { target: { value: 'x' } }) and fireEvent.blur(input) when validation is blur-driven. Use userEvent.type only when per-keystroke behavior matters.
wrapper.update()
Not needed — RTL re-queries the DOM automatically. Wrap state updates in act() if needed.
wrapper.setProps({ foo: 'bar' })
Re-render: rerender(<Comp foo="bar" />)
Testing component props (mock-based pattern)
When tests assert on props passed to child components, mock the child and inspect mock calls:
Use this pattern instead of enzyme's .find(Component).prop('propName'). Clear mocks between tests with jest.clearAllMocks() in beforeEach.
Async & state updates
Wait for a UI boundary with findBy* (preferred) or waitFor() when you need a custom assertion. Use act() for explicit timer advancement/flush (e.g. jest.runOnlyPendingTimersAsync() in fake-timer suites) or imperative callbacks that trigger React updates.
Replace wrapper.update() + nextTick() patterns with await waitFor(...).
For promises that resolve in tests, prefer findByTestId (auto-waits) over getByTestId + waitFor. Prefer reusing the element returned from findBy* instead of re-querying with getBy* immediately after (re-query only when you expect the DOM to change/replace the element).
For elements that should disappear, prefer waitForElementToBeRemoved(...) (example: await waitForElementToBeRemoved(screen.getByTestId('loading'))).
Don't wrap fireEvent/userEvent in act(); instead, perform the interaction and then wait on the relevant UI boundary (see bullets above).
Avoid “fixing” failures by increasing waitFor timeouts; tighten the UI boundary you wait for instead.
Portal-based components (popovers, modals, tooltips, toasts) must use targeted assertions, not snapshots. Their panel content renders outside container via portals, so container.children[0] only captures the trigger/anchor — not the actual content. Use screen queries (which search the full document body) or document.querySelector for portals:
// Popover: render with isOpen, assert on panel content via screenrender(
<MyPopoverisOpenbutton={<button>Toggle</button>}>
<SelectableListoptions={options} /></MyPopover>
);
expect(screen.getByText('Toggle')).toBeInTheDocument();
expect(screen.getByText('Panel Title')).toBeInTheDocument();
expect(screen.getByText('Option A')).toBeInTheDocument();
expect(document.querySelector('[id^="searchInput"]')).toBeInTheDocument();
// Modal: content renders in a portal, use document.querySelectorexpect(document.querySelector('[data-test-subj="confirmModal"]')).toBeInTheDocument();
// Tooltip: content only appears on hoverawait userEvent.hover(screen.getByText('Hover me'));
awaitwaitFor(() => {
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip text');
});
Common pitfalls
Missing providers after removing shallow. Enzyme shallow doesn't render children deeply, hiding missing context providers. After switching to render(), add required providers (I18n, Redux, Router, Theme, etc.) or mock them.
EUI component explosions. Some EUI components render complex DOM. Mock them if the test doesn't care about their internals. Prefer shared mocks when available (e.g. import "@kbn/code-editor-mock/jest_helper"), then plugin-local __mocks__/ files (e.g. <pluginRoot>/__mocks__/@elastic/charts/index.tsx), then an inline mock factory as a fallback. When stubbing components to <div>s, add a data-test-subj only when the test needs a stable query for "mock rendered".
Portal-based elements. Modals, toasts, and popovers render outside container. Use document.querySelector or screen (which queries the whole document body). Never snapshot these — use targeted assertions instead (see Snapshot strategy).
act() warnings. Usually caused by missing await / missing async UI boundary after an interaction. Prefer await screen.findBy... / await waitFor(...) over wrapping events in act() (events are already wrapped). Use act() for explicit timer advancement/flush (e.g. jest.runOnlyPendingTimersAsync() in fake-timer suites) or imperative callbacks that trigger React updates. Never use empty act() blocks (e.g. await act(async () => {})).
userEvent performance.userEvent simulates full event sequences and scales poorly in CI (geometrically with interaction count). Prefer fireEvent for simple clicks and value changes. Replace userEvent.type(input, 'text') with fireEvent.change(input, { target: { value: 'text' } }) + fireEvent.blur(input) unless the test is specifically exercising per-keystroke behavior (e.g. keydown handlers, typeahead suggestions, input masking/formatting, debounce-on-each-char). When fireEvent.change causes act warnings inside portals/overlays, prefer userEvent.paste over userEvent.type — it sets the full value in one step without per-character overhead.
Timer-based tests. Replace jest.advanceTimersByTime patterns carefully — RTL's userEvent uses real timers by default. Use userEvent.setup({ advanceTimers: jest.advanceTimersByTime }) when fake timers are needed.