
React18 Enzyme To Rtl
- 744 installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
react18-enzyme-to-rtl rewrites Enzyme tests to React Testing Library with behavior-first assertions for React 18.
About
React 18 Enzyme to RTL Migration states Enzyme has no React 18 adapter and requires full rewrites using React Testing Library instead of one-to-one API translation. The philosophy shift moves from implementation tests on wrapper.state, wrapper.instance, and wrapper.prop to user-visible outcomes via screen queries and userEvent interactions. An API map in references/enzyme-api-map.md covers shallow, mount, find, simulate, prop, state, instance, and configure mappings plus async patterns with waitFor and MockedProvider. The core rewrite template uses render, getByRole queries, userEvent clicks, and assertions on visible text. RTL query priority ranks getByRole first for accessibility-aligned tests and reserves getByTestId as last resort.
- Enzyme has no React 18 adapter; all tests must be rewritten in RTL.
- Philosophy shift: behavior and visible output, not internal state or instances.
- Not a 1:1 API map; rewrite tests to assert what users see and do.
- Core template: render, screen query, userEvent, visible assertion.
- Query priority favors getByRole; getByTestId is last resort.
React18 Enzyme To Rtl by the numbers
- 744 all-time installs (skills.sh)
- +17 installs in the week ending Jul 17, 2026 (Skillselion tracking)
- Ranked #567 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
react18-enzyme-to-rtl capabilities & compatibility
- Capabilities
- enzyme to rtl philosophy and non 1:1 rewrite gui · core render query interact assert template · rtl query priority ladder from getbyrole to getb · provider wrapping pattern for apollo and theme c · async patterns with waitfor, findby, and mockedp
- Use cases
- testing · refactoring
What react18-enzyme-to-rtl says it does
Enzyme has no React 18 adapter and no React 18 support path.
This is not a 1:1 translation.
npx skills add https://github.com/github/awesome-copilot --skill react18-enzyme-to-rtlAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 744 |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
How do I replace Enzyme shallow, mount, and wrapper APIs now that Enzyme lacks React 18 support?
Rewrite Enzyme shallow and mount tests to React Testing Library with behavior-first assertions during React 18 upgrades.
Who is it for?
Frontend teams with Enzyme test suites blocking React 18 upgrades.
Skip if: Skip for greenfield RTL projects, production component code migration, or dependency version matrices.
When should I use this skill?
Test files import enzyme, use shallow/mount, wrapper.find, simulate, state, prop, or instance APIs.
What you get
RTL rewrite template, query priority guide, provider wrapping patterns, and async reference mappings.
- RTL test files
- behavior-first test queries
By the numbers
- Covers 8 Enzyme API areas: shallow, mount, find, simulate, prop, state, instance, and Adapter config
Files
React 18 Enzyme → RTL Migration
Enzyme has no React 18 adapter and no React 18 support path. All Enzyme tests must be rewritten using React Testing Library.
The Philosophy Shift (Read This First)
Enzyme tests implementation. RTL tests behavior.
// Enzyme: tests that the component has the right internal state
expect(wrapper.state('count')).toBe(3);
expect(wrapper.instance().handleClick).toBeDefined();
expect(wrapper.find('Button').prop('disabled')).toBe(true);
// RTL: tests what the user actually sees and can do
expect(screen.getByText('Count: 3')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();This is not a 1:1 translation. Enzyme tests that verify internal state or instance methods don't have RTL equivalents - because RTL intentionally doesn't expose internals. Rewrite the test to assert the visible outcome instead.
API Map
For complete before/after code for each Enzyme API, read:
- `references/enzyme-api-map.md` - full mapping: shallow, mount, find, simulate, prop, state, instance, configure
- `references/async-patterns.md` - waitFor, findBy, act(), Apollo MockedProvider, loading states, error states
Core Rewrite Template
// Every Enzyme test rewrites to this shape:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('does the thing', async () => {
// 1. Render (replaces shallow/mount)
render(<MyComponent prop="value" />);
// 2. Query (replaces wrapper.find())
const button = screen.getByRole('button', { name: /submit/i });
// 3. Interact (replaces simulate())
await userEvent.setup().click(button);
// 4. Assert on visible output (replaces wrapper.state() / wrapper.prop())
expect(screen.getByText('Submitted!')).toBeInTheDocument();
});
});RTL Query Priority (use in this order)
1. getByRole - matches accessible roles (button, textbox, heading, checkbox, etc.) 2. getByLabelText - form fields linked to labels 3. getByPlaceholderText - input placeholders 4. getByText - visible text content 5. getByDisplayValue - current value of input/select/textarea 6. getByAltText - image alt text 7. getByTitle - title attribute 8. getByTestId - data-testid attribute (last resort)
Prefer getByRole over getByTestId. It tests accessibility too.
Wrapping with Providers
// Enzyme with context:
const wrapper = mount(
<ApolloProvider client={client}>
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
</ApolloProvider>
);
// RTL equivalent (use your project's customRender or wrap inline):
import { render } from '@testing-library/react';
render(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
</MockedProvider>
);
// Or use the project's customRender helper if it wraps providersAsync Test Patterns - Enzyme → RTL Migration
Reference for rewriting Enzyme async tests to React Testing Library with React 18 compatible patterns.
The Core Problem
Enzyme's async tests typically used one of these approaches:
wrapper.update()after state changessetTimeout/Promise.resolve()to flush microtaskssetImmediateto flush async queues- Direct instance method calls followed by
wrapper.update()
None of these work in RTL. RTL provides waitFor, findBy*, and act instead.
---
Pattern 1 - wrapper.update() After State Change
Enzyme required wrapper.update() to force a re-render after async state changes.
// Enzyme:
it('loads data', async () => {
const wrapper = mount(<UserList />);
await Promise.resolve(); // flush microtasks
wrapper.update(); // force Enzyme to sync with DOM
expect(wrapper.find('li')).toHaveLength(3);
});// RTL - waitFor handles re-renders automatically:
import { render, screen, waitFor } from '@testing-library/react';
it('loads data', async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getAllByRole('listitem')).toHaveLength(3);
});
});---
Pattern 2 - Async Action Triggered by User Interaction
// Enzyme:
it('fetches user on button click', async () => {
const wrapper = mount(<UserCard />);
wrapper.find('button').simulate('click');
await new Promise(resolve => setTimeout(resolve, 0));
wrapper.update();
expect(wrapper.find('.user-name').text()).toBe('John Doe');
});// RTL:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
it('fetches user on button click', async () => {
render(<UserCard />);
await userEvent.setup().click(screen.getByRole('button', { name: /load/i }));
// findBy* auto-waits up to 1000ms (configurable)
expect(await screen.findByText('John Doe')).toBeInTheDocument();
});---
Pattern 3 - Loading State Assertion
// Enzyme - asserted loading state synchronously then final state after flush:
it('shows loading then result', async () => {
const wrapper = mount(<SearchResults query="react" />);
expect(wrapper.find('.spinner').exists()).toBe(true);
await new Promise(resolve => setTimeout(resolve, 100));
wrapper.update();
expect(wrapper.find('.spinner').exists()).toBe(false);
expect(wrapper.find('.result')).toHaveLength(5);
});// RTL:
it('shows loading then result', async () => {
render(<SearchResults query="react" />);
// Loading state - check it appears
expect(screen.getByRole('progressbar')).toBeInTheDocument();
// Or if loading is text:
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for results to appear (loading disappears, results show)
await waitFor(() => {
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('listitem')).toHaveLength(5);
});---
Pattern 4 - Apollo MockedProvider Async Tests
// Enzyme with Apollo - used to flush with multiple ticks:
it('renders user from query', async () => {
const wrapper = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile id="1" />
</MockedProvider>
);
await new Promise(resolve => setTimeout(resolve, 0)); // flush Apollo queue
wrapper.update();
expect(wrapper.find('.username').text()).toBe('Alice');
});// RTL with Apollo:
import { render, screen, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
it('renders user from query', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile id="1" />
</MockedProvider>
);
// Wait for Apollo to resolve the query
expect(await screen.findByText('Alice')).toBeInTheDocument();
// OR:
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});Apollo loading state in RTL:
it('shows loading then data', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile id="1" />
</MockedProvider>
);
// Apollo loading state - check immediately after render
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Then wait for data
expect(await screen.findByText('Alice')).toBeInTheDocument();
});---
Pattern 5 - Error State from Async Operation
// Enzyme:
it('shows error on failed fetch', async () => {
server.use(rest.get('/api/user', (req, res, ctx) => res(ctx.status(500))));
const wrapper = mount(<UserCard />);
wrapper.find('button').simulate('click');
await new Promise(resolve => setTimeout(resolve, 0));
wrapper.update();
expect(wrapper.find('.error-message').text()).toContain('Something went wrong');
});// RTL:
it('shows error on failed fetch', async () => {
// (assuming MSW or jest.mock for fetch)
render(<UserCard />);
await userEvent.setup().click(screen.getByRole('button', { name: /load/i }));
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});---
Pattern 6 - act() for Manual Async Control
When you need explicit control over async timing (rare with RTL but occasionally needed for class component tests):
// RTL with act() for fine-grained async control:
import { act } from 'react';
it('handles sequential state updates', async () => {
render(<MultiStepForm />);
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /next/i }));
await Promise.resolve(); // flush microtask queue
});
expect(screen.getByText('Step 2')).toBeInTheDocument();
});---
RTL Async Query Guide
| Method | Behavior | Use when |
|---|---|---|
getBy* | Synchronous - throws if not found | Element is always present immediately |
queryBy* | Synchronous - returns null if not found | Checking element does NOT exist |
findBy* | Async - waits up to 1000ms, rejects if not found | Element appears asynchronously |
getAllBy* | Synchronous - throws if 0 found | Multiple elements always present |
queryAllBy* | Synchronous - returns [] if none found | Checking count or non-existence |
findAllBy* | Async - waits for elements to appear | Multiple elements appear asynchronously |
waitFor(fn) | Retries fn until no error or timeout | Custom assertion that needs polling |
waitForElementToBeRemoved(el) | Waits until element disappears | Loading states, removals |
Default timeout: 1000ms. Configure globally in jest.config.js:
// Increase timeout for slow CI environments
// jest.config.js
module.exports = {
testEnvironmentOptions: {
asyncUtilTimeout: 3000,
},
};---
Common Migration Mistakes
// WRONG - mixing async query with sync assertion:
const el = await screen.findByText('Result');
// el is already resolved here - findBy returns the element, not a promise
expect(await el).toBeInTheDocument(); // unnecessary second await
// CORRECT:
const el = await screen.findByText('Result');
expect(el).toBeInTheDocument();
// OR simply:
expect(await screen.findByText('Result')).toBeInTheDocument();// WRONG - using getBy* for elements that appear asynchronously:
fireEvent.click(button);
expect(screen.getByText('Loaded!')).toBeInTheDocument(); // throws before data loads
// CORRECT:
fireEvent.click(button);
expect(await screen.findByText('Loaded!')).toBeInTheDocument(); // waitsEnzyme API Map - Complete Before/After
Setup / Configure
// Enzyme:
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
// RTL: delete this entirely - no setup needed
// (jest.config.js setupFilesAfterFramework handles @testing-library/jest-dom matchers)---
Rendering
// Enzyme - shallow (no children rendered):
import { shallow } from 'enzyme';
const wrapper = shallow(<MyComponent prop="value" />);
// RTL - render (full render, children included):
import { render } from '@testing-library/react';
render(<MyComponent prop="value" />);
// No wrapper variable needed - query via screen// Enzyme - mount (full render with DOM):
import { mount } from 'enzyme';
const wrapper = mount(<MyComponent />);
// RTL - same render() call handles this
render(<MyComponent />);---
Querying
// Enzyme - find by component type:
const button = wrapper.find('button');
const comp = wrapper.find(ChildComponent);
const items = wrapper.find('.list-item');
// RTL - query by accessible attributes:
const button = screen.getByRole('button');
const button = screen.getByRole('button', { name: /submit/i });
const heading = screen.getByRole('heading', { name: /title/i });
const input = screen.getByLabelText('Email');
const items = screen.getAllByRole('listitem');// Enzyme - find by text:
wrapper.find('.message').text() === 'Hello'
// RTL:
screen.getByText('Hello')
screen.getByText(/hello/i) // case-insensitive regex---
User Interaction
// Enzyme:
wrapper.find('button').simulate('click');
wrapper.find('input').simulate('change', { target: { value: 'hello' } });
wrapper.find('form').simulate('submit');
// RTL - fireEvent (synchronous, low-level):
import { fireEvent } from '@testing-library/react';
fireEvent.click(screen.getByRole('button'));
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'hello' } });
fireEvent.submit(screen.getByRole('form'));
// RTL - userEvent (preferred, simulates real user behavior):
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(screen.getByRole('button'));
await user.type(screen.getByRole('textbox'), 'hello');
await user.selectOptions(screen.getByRole('combobox'), 'option1');Use `userEvent` for most interactions - it fires the full event sequence (pointerdown, mousedown, focus, click, etc.) like a real user. Use fireEvent only when testing specific event properties.
---
Assertions on Props and State
// Enzyme - prop assertion:
expect(wrapper.find('input').prop('disabled')).toBe(true);
expect(wrapper.prop('className')).toContain('active');
// RTL - assert on visible attributes:
expect(screen.getByRole('textbox')).toBeDisabled();
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
expect(screen.getByRole('listitem')).toHaveClass('active');// Enzyme - state assertion (NO RTL EQUIVALENT):
expect(wrapper.state('count')).toBe(3);
expect(wrapper.state('loading')).toBe(false);
// RTL - assert on what the state renders:
expect(screen.getByText('Count: 3')).toBeInTheDocument();
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();Key principle: Don't test state values - test what the state produces in the UI. If the component renders <span>Count: {this.state.count}</span>, test that span.
---
Instance Methods
// Enzyme - direct method call (NO RTL EQUIVALENT):
wrapper.instance().handleSubmit();
wrapper.instance().loadData();
// RTL - trigger through the UI:
await userEvent.setup().click(screen.getByRole('button', { name: /submit/i }));
// Or if no UI trigger exists, reconsider: should internal methods be tested directly?
// Usually the answer is no - test the rendered outcome instead.---
Existence Checks
// Enzyme:
expect(wrapper.find('.error')).toHaveLength(1);
expect(wrapper.find('.error')).toHaveLength(0);
expect(wrapper.exists('.error')).toBe(true);
// RTL:
expect(screen.getByText('Error message')).toBeInTheDocument();
expect(screen.queryByText('Error message')).not.toBeInTheDocument();
// queryBy returns null instead of throwing when not found
// getBy throws if not found - use in positive assertions
// findBy returns a promise - use for async elements---
Multiple Elements
// Enzyme:
expect(wrapper.find('li')).toHaveLength(5);
wrapper.find('li').forEach((item, i) => {
expect(item.text()).toBe(expectedItems[i]);
});
// RTL:
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(5);
items.forEach((item, i) => {
expect(item).toHaveTextContent(expectedItems[i]);
});---
Before/After: Complete Component Test
// Enzyme version:
import { shallow } from 'enzyme';
describe('LoginForm', () => {
it('submits with credentials', () => {
const mockSubmit = jest.fn();
const wrapper = shallow(<LoginForm onSubmit={mockSubmit} />);
wrapper.find('input[name="email"]').simulate('change', {
target: { value: 'user@example.com' }
});
wrapper.find('input[name="password"]').simulate('change', {
target: { value: 'password123' }
});
wrapper.find('button[type="submit"]').simulate('click');
expect(wrapper.state('loading')).toBe(true);
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123'
});
});
});// RTL version:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('submits with credentials', async () => {
const mockSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'user@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
// Assert on visible output - not on state
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled(); // loading state
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123'
});
});
});Related skills
How it compares
Pick react18-enzyme-to-rtl over generic RTL tutorials when migrating existing Enzyme suites that use wrapper internals on a React 18 upgrade.
FAQ
Can I translate Enzyme APIs 1:1 to RTL?
No. RTL intentionally hides internals; rewrite tests to assert visible user outcomes instead.
What replaces wrapper.simulate click?
Use userEvent.setup().click on a screen query result from getByRole or similar.
How test wrapper.state count?
Assert visible text like screen.getByText('Count: 3') instead of reading component state.
Is React18 Enzyme To Rtl safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.