Generated banner for React18 Enzyme To Rtl
Generated logo for React18 Enzyme To Rtl

React18 Enzyme To Rtl

GitHubOfficialFirst committed by SaravananRajaramanDistributed by GitHub

  • Skill
  • Guidelines
  • Software development
  • MIT
  • No issues found

From the author

Provides exact Enzyme → React Testing Library migration patterns for React 18 upgrades. Use this skill whenever Enzyme tests need to be rewritten - shallow, mount, wrapper.find(), wrapper.simulate(), wrapper.prop(), wrapper.state(), wrapper.instance(), Enzyme configure/Adapter calls, or any test file that imports from enzyme. This skill covers the full API mapping and the philosophy shift from implementation testing to behavior testing. Always read this skill before rewriting Enzyme tests - do not translate Enzyme APIs 1:1, that produces brittle RTL tests.
— the description by SaravananRajaraman

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a comprehensive guide for migrating legacy Enzyme tests to React Testing Library (RTL) in the context of React 18 upgrades. It emphasizes a fundamental shift in testing philosophy: moving away from testing internal component implementation details (like state, props, and instance methods) toward testing user-facing behavior. The guide includes API mapping, async handling patterns, and strategies for replacing Enzyme-specific concepts with accessible, user-centric RTL queries.

What it does

  • Maps common Enzyme APIs like shallow, mount, find, and simulate to their RTL equivalents.
  • Provides patterns for handling asynchronous operations and state updates without wrapper.update().
  • Explains the shift from implementation-based testing to behavior-driven testing.
  • Offers templates for rendering components and wrapping them with necessary providers.
  • Defines a priority order for RTL queries to ensure accessible and robust tests.
  • Advises on how to replace direct instance method calls with user-event interactions.

When to use it

  • When upgrading a React project to version 18 and needing to replace Enzyme.
  • When rewriting brittle tests that rely on internal component state or instance methods.
  • When you need to understand the correct RTL approach for complex async data fetching.
  • When you are unsure how to map specific Enzyme wrappers or find methods to RTL queries.

Advantages

  • Promotes the creation of more resilient tests that do not break during refactoring.
  • Ensures tests reflect actual user interactions rather than internal code structure.
  • Aligns testing practices with modern React 18 standards and accessibility best practices.
  • Reduces reliance on implementation-specific testing patterns.

Usage examples

“How do I replace wrapper.state('count') in my tests?”
The skill advises against testing state directly. Instead, you should assert that the rendered output reflects the state, such as using screen.getByText('Count: 3') to verify the UI.
“How do I handle async data loading that used wrapper.update()?”
The skill recommends using RTL's findBy* queries or waitFor, which automatically handle re-renders and wait for the DOM to update, replacing the need for manual flushing.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/github/react18-enzyme-to-rtl/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip react18-enzyme-to-rtl.zip -d .agents/skills

Documentation by SaravananRajaraman

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 providers

Files

references/async-patterns.md

Async 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 changes

  • setTimeout / Promise.resolve() to flush microtasks

  • setImmediate to 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

MethodBehaviorUse when
getBy*Synchronous - throws if not foundElement is always present immediately
queryBy*Synchronous - returns null if not foundChecking element does NOT exist
findBy*Async - waits up to 1000ms, rejects if not foundElement appears asynchronously
getAllBy*Synchronous - throws if 0 foundMultiple elements always present
queryAllBy*Synchronous - returns [] if none foundChecking count or non-existence
findAllBy*Async - waits for elements to appearMultiple elements appear asynchronously
waitFor(fn)Retries fn until no error or timeoutCustom assertion that needs polling
waitForElementToBeRemoved(el)Waits until element disappearsLoading 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(); // waits

Requirements and compatibility

Written for: GitHub Copilot

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

First committed by
GitHub avatar of SaravananRajaramanSaravananRajaraman
Publisher
GitHub (Official)
Distributed by
GitHub
Source
github/awesome-copilot · skills/react18-enzyme-to-rtl · commit 797337f
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Contributors
SaravananRajaraman

AutoGineer is not affiliated with or endorsed by SaravananRajaraman or GitHub.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download react18-enzyme-to-rtl.zip