chore: publish from staged

This commit is contained in:
github-actions[bot]
2026-04-09 06:26:21 +00:00
parent 017f31f495
commit a68b190031
467 changed files with 97527 additions and 276 deletions

View File

@@ -0,0 +1,260 @@
# 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.
```jsx
// 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);
});
```
```jsx
// 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
```jsx
// 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');
});
```
```jsx
// 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
```jsx
// 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);
});
```
```jsx
// 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
```jsx
// 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');
});
```
```jsx
// 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:**
```jsx
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
```jsx
// 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');
});
```
```jsx
// 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):
```jsx
// 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`:
```js
// Increase timeout for slow CI environments
// jest.config.js
module.exports = {
testEnvironmentOptions: {
asyncUtilTimeout: 3000,
},
};
```
---
## Common Migration Mistakes
```jsx
// 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();
```
```jsx
// 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
```

View File

@@ -0,0 +1,224 @@
# Enzyme API Map - Complete Before/After
## Setup / Configure
```jsx
// 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
```jsx
// 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
```
```jsx
// Enzyme - mount (full render with DOM):
import { mount } from 'enzyme';
const wrapper = mount(<MyComponent />);
// RTL - same render() call handles this
render(<MyComponent />);
```
---
## Querying
```jsx
// 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');
```
```jsx
// Enzyme - find by text:
wrapper.find('.message').text() === 'Hello'
// RTL:
screen.getByText('Hello')
screen.getByText(/hello/i) // case-insensitive regex
```
---
## User Interaction
```jsx
// 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
```jsx
// 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');
```
```jsx
// 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
```jsx
// 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
```jsx
// 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
```jsx
// 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
```jsx
// 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'
});
});
});
```
```jsx
// 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'
});
});
});
```