{"kind":"Skill","metadata":{"namespace":"community","name":"react18-enzyme-to-rtl","version":"0.1.0"},"spec":{"description":"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.","files":{"SKILL.md":"---\nname: react18-enzyme-to-rtl\ndescription: '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.'\n---\n\n# React 18 Enzyme → RTL Migration\n\nEnzyme has no React 18 adapter and no React 18 support path. All Enzyme tests must be rewritten using React Testing Library.\n\n## The Philosophy Shift (Read This First)\n\nEnzyme tests implementation. RTL tests behavior.\n\n```jsx\n// Enzyme: tests that the component has the right internal state\nexpect(wrapper.state('count')).toBe(3);\nexpect(wrapper.instance().handleClick).toBeDefined();\nexpect(wrapper.find('Button').prop('disabled')).toBe(true);\n\n// RTL: tests what the user actually sees and can do\nexpect(screen.getByText('Count: 3')).toBeInTheDocument();\nexpect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();\n```\n\nThis 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.**\n\n## API Map\n\nFor complete before/after code for each Enzyme API, read:\n- **`references/enzyme-api-map.md`** - full mapping: shallow, mount, find, simulate, prop, state, instance, configure\n- **`references/async-patterns.md`** - waitFor, findBy, act(), Apollo MockedProvider, loading states, error states\n\n## Core Rewrite Template\n\n```jsx\n// Every Enzyme test rewrites to this shape:\nimport { render, screen, fireEvent, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport MyComponent from './MyComponent';\n\ndescribe('MyComponent', () =\u003e {\n  it('does the thing', async () =\u003e {\n    // 1. Render (replaces shallow/mount)\n    render(\u003cMyComponent prop=\"value\" /\u003e);\n\n    // 2. Query (replaces wrapper.find())\n    const button = screen.getByRole('button', { name: /submit/i });\n\n    // 3. Interact (replaces simulate())\n    await userEvent.setup().click(button);\n\n    // 4. Assert on visible output (replaces wrapper.state() / wrapper.prop())\n    expect(screen.getByText('Submitted!')).toBeInTheDocument();\n  });\n});\n```\n\n## RTL Query Priority (use in this order)\n\n1. `getByRole` - matches accessible roles (button, textbox, heading, checkbox, etc.)\n2. `getByLabelText` - form fields linked to labels\n3. `getByPlaceholderText` - input placeholders\n4. `getByText` - visible text content\n5. `getByDisplayValue` - current value of input/select/textarea\n6. `getByAltText` - image alt text\n7. `getByTitle` - title attribute\n8. `getByTestId` - `data-testid` attribute (last resort)\n\nPrefer `getByRole` over `getByTestId`. It tests accessibility too.\n\n## Wrapping with Providers\n\n```jsx\n// Enzyme with context:\nconst wrapper = mount(\n  \u003cApolloProvider client={client}\u003e\n    \u003cThemeProvider theme={theme}\u003e\n      \u003cMyComponent /\u003e\n    \u003c/ThemeProvider\u003e\n  \u003c/ApolloProvider\u003e\n);\n\n// RTL equivalent (use your project's customRender or wrap inline):\nimport { render } from '@testing-library/react';\nrender(\n  \u003cMockedProvider mocks={mocks} addTypename={false}\u003e\n    \u003cThemeProvider theme={theme}\u003e\n      \u003cMyComponent /\u003e\n    \u003c/ThemeProvider\u003e\n  \u003c/MockedProvider\u003e\n);\n// Or use the project's customRender helper if it wraps providers\n```\n","references/async-patterns.md":"# Async Test Patterns - Enzyme → RTL Migration\n\nReference for rewriting Enzyme async tests to React Testing Library with React 18 compatible patterns.\n\n## The Core Problem\n\nEnzyme's async tests typically used one of these approaches:\n\n- `wrapper.update()` after state changes\n- `setTimeout` / `Promise.resolve()` to flush microtasks\n- `setImmediate` to flush async queues\n- Direct instance method calls followed by `wrapper.update()`\n\nNone of these work in RTL. RTL provides `waitFor`, `findBy*`, and `act` instead.\n\n---\n\n## Pattern 1 - wrapper.update() After State Change\n\nEnzyme required `wrapper.update()` to force a re-render after async state changes.\n\n```jsx\n// Enzyme:\nit('loads data', async () =\u003e {\n  const wrapper = mount(\u003cUserList /\u003e);\n  await Promise.resolve(); // flush microtasks\n  wrapper.update();        // force Enzyme to sync with DOM\n  expect(wrapper.find('li')).toHaveLength(3);\n});\n```\n\n```jsx\n// RTL - waitFor handles re-renders automatically:\nimport { render, screen, waitFor } from '@testing-library/react';\n\nit('loads data', async () =\u003e {\n  render(\u003cUserList /\u003e);\n  await waitFor(() =\u003e {\n    expect(screen.getAllByRole('listitem')).toHaveLength(3);\n  });\n});\n```\n\n---\n\n## Pattern 2 - Async Action Triggered by User Interaction\n\n```jsx\n// Enzyme:\nit('fetches user on button click', async () =\u003e {\n  const wrapper = mount(\u003cUserCard /\u003e);\n  wrapper.find('button').simulate('click');\n  await new Promise(resolve =\u003e setTimeout(resolve, 0));\n  wrapper.update();\n  expect(wrapper.find('.user-name').text()).toBe('John Doe');\n});\n```\n\n```jsx\n// RTL:\nimport { render, screen, fireEvent, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\n\nit('fetches user on button click', async () =\u003e {\n  render(\u003cUserCard /\u003e);\n  await userEvent.setup().click(screen.getByRole('button', { name: /load/i }));\n  // findBy* auto-waits up to 1000ms (configurable)\n  expect(await screen.findByText('John Doe')).toBeInTheDocument();\n});\n```\n\n---\n\n## Pattern 3 - Loading State Assertion\n\n```jsx\n// Enzyme - asserted loading state synchronously then final state after flush:\nit('shows loading then result', async () =\u003e {\n  const wrapper = mount(\u003cSearchResults query=\"react\" /\u003e);\n  expect(wrapper.find('.spinner').exists()).toBe(true);\n  await new Promise(resolve =\u003e setTimeout(resolve, 100));\n  wrapper.update();\n  expect(wrapper.find('.spinner').exists()).toBe(false);\n  expect(wrapper.find('.result')).toHaveLength(5);\n});\n```\n\n```jsx\n// RTL:\nit('shows loading then result', async () =\u003e {\n  render(\u003cSearchResults query=\"react\" /\u003e);\n  // Loading state - check it appears\n  expect(screen.getByRole('progressbar')).toBeInTheDocument();\n  // Or if loading is text:\n  expect(screen.getByText(/loading/i)).toBeInTheDocument();\n\n  // Wait for results to appear (loading disappears, results show)\n  await waitFor(() =\u003e {\n    expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();\n  });\n  expect(screen.getAllByRole('listitem')).toHaveLength(5);\n});\n```\n\n---\n\n## Pattern 4 - Apollo MockedProvider Async Tests\n\n```jsx\n// Enzyme with Apollo - used to flush with multiple ticks:\nit('renders user from query', async () =\u003e {\n  const wrapper = mount(\n    \u003cMockedProvider mocks={mocks} addTypename={false}\u003e\n      \u003cUserProfile id=\"1\" /\u003e\n    \u003c/MockedProvider\u003e\n  );\n  await new Promise(resolve =\u003e setTimeout(resolve, 0)); // flush Apollo queue\n  wrapper.update();\n  expect(wrapper.find('.username').text()).toBe('Alice');\n});\n```\n\n```jsx\n// RTL with Apollo:\nimport { render, screen, waitFor } from '@testing-library/react';\nimport { MockedProvider } from '@apollo/client/testing';\n\nit('renders user from query', async () =\u003e {\n  render(\n    \u003cMockedProvider mocks={mocks} addTypename={false}\u003e\n      \u003cUserProfile id=\"1\" /\u003e\n    \u003c/MockedProvider\u003e\n  );\n\n  // Wait for Apollo to resolve the query\n  expect(await screen.findByText('Alice')).toBeInTheDocument();\n  // OR:\n  await waitFor(() =\u003e {\n    expect(screen.getByText('Alice')).toBeInTheDocument();\n  });\n});\n```\n\n**Apollo loading state in RTL:**\n\n```jsx\nit('shows loading then data', async () =\u003e {\n  render(\n    \u003cMockedProvider mocks={mocks} addTypename={false}\u003e\n      \u003cUserProfile id=\"1\" /\u003e\n    \u003c/MockedProvider\u003e\n  );\n  // Apollo loading state - check immediately after render\n  expect(screen.getByText(/loading/i)).toBeInTheDocument();\n  // Then wait for data\n  expect(await screen.findByText('Alice')).toBeInTheDocument();\n});\n```\n\n---\n\n## Pattern 5 - Error State from Async Operation\n\n```jsx\n// Enzyme:\nit('shows error on failed fetch', async () =\u003e {\n  server.use(rest.get('/api/user', (req, res, ctx) =\u003e res(ctx.status(500))));\n  const wrapper = mount(\u003cUserCard /\u003e);\n  wrapper.find('button').simulate('click');\n  await new Promise(resolve =\u003e setTimeout(resolve, 0));\n  wrapper.update();\n  expect(wrapper.find('.error-message').text()).toContain('Something went wrong');\n});\n```\n\n```jsx\n// RTL:\nit('shows error on failed fetch', async () =\u003e {\n  // (assuming MSW or jest.mock for fetch)\n  render(\u003cUserCard /\u003e);\n  await userEvent.setup().click(screen.getByRole('button', { name: /load/i }));\n  expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();\n});\n```\n\n---\n\n## Pattern 6 - act() for Manual Async Control\n\nWhen you need explicit control over async timing (rare with RTL but occasionally needed for class component tests):\n\n```jsx\n// RTL with act() for fine-grained async control:\nimport { act } from 'react';\n\nit('handles sequential state updates', async () =\u003e {\n  render(\u003cMultiStepForm /\u003e);\n\n  await act(async () =\u003e {\n    fireEvent.click(screen.getByRole('button', { name: /next/i }));\n    await Promise.resolve(); // flush microtask queue\n  });\n\n  expect(screen.getByText('Step 2')).toBeInTheDocument();\n});\n```\n\n---\n\n## RTL Async Query Guide\n\n| Method | Behavior | Use when |\n|---|---|---|\n| `getBy*` | Synchronous - throws if not found | Element is always present immediately |\n| `queryBy*` | Synchronous - returns null if not found | Checking element does NOT exist |\n| `findBy*` | Async - waits up to 1000ms, rejects if not found | Element appears asynchronously |\n| `getAllBy*` | Synchronous - throws if 0 found | Multiple elements always present |\n| `queryAllBy*` | Synchronous - returns [] if none found | Checking count or non-existence |\n| `findAllBy*` | Async - waits for elements to appear | Multiple elements appear asynchronously |\n| `waitFor(fn)` | Retries fn until no error or timeout | Custom assertion that needs polling |\n| `waitForElementToBeRemoved(el)` | Waits until element disappears | Loading states, removals |\n\n**Default timeout:** 1000ms. Configure globally in `jest.config.js`:\n\n```js\n// Increase timeout for slow CI environments\n// jest.config.js\nmodule.exports = {\n  testEnvironmentOptions: {\n    asyncUtilTimeout: 3000,\n  },\n};\n```\n\n---\n\n## Common Migration Mistakes\n\n```jsx\n// WRONG - mixing async query with sync assertion:\nconst el = await screen.findByText('Result');\n// el is already resolved here - findBy returns the element, not a promise\nexpect(await el).toBeInTheDocument(); // unnecessary second await\n\n// CORRECT:\nconst el = await screen.findByText('Result');\nexpect(el).toBeInTheDocument();\n// OR simply:\nexpect(await screen.findByText('Result')).toBeInTheDocument();\n```\n\n```jsx\n// WRONG - using getBy* for elements that appear asynchronously:\nfireEvent.click(button);\nexpect(screen.getByText('Loaded!')).toBeInTheDocument(); // throws before data loads\n\n// CORRECT:\nfireEvent.click(button);\nexpect(await screen.findByText('Loaded!')).toBeInTheDocument(); // waits\n```\n","references/enzyme-api-map.md":"# Enzyme API Map - Complete Before/After\n\n## Setup / Configure\n\n```jsx\n// Enzyme:\nimport Enzyme from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\nEnzyme.configure({ adapter: new Adapter() });\n\n// RTL: delete this entirely - no setup needed\n// (jest.config.js setupFilesAfterFramework handles @testing-library/jest-dom matchers)\n```\n\n---\n\n## Rendering\n\n```jsx\n// Enzyme - shallow (no children rendered):\nimport { shallow } from 'enzyme';\nconst wrapper = shallow(\u003cMyComponent prop=\"value\" /\u003e);\n\n// RTL - render (full render, children included):\nimport { render } from '@testing-library/react';\nrender(\u003cMyComponent prop=\"value\" /\u003e);\n// No wrapper variable needed - query via screen\n```\n\n```jsx\n// Enzyme - mount (full render with DOM):\nimport { mount } from 'enzyme';\nconst wrapper = mount(\u003cMyComponent /\u003e);\n\n// RTL - same render() call handles this\nrender(\u003cMyComponent /\u003e);\n```\n\n---\n\n## Querying\n\n```jsx\n// Enzyme - find by component type:\nconst button = wrapper.find('button');\nconst comp = wrapper.find(ChildComponent);\nconst items = wrapper.find('.list-item');\n\n// RTL - query by accessible attributes:\nconst button = screen.getByRole('button');\nconst button = screen.getByRole('button', { name: /submit/i });\nconst heading = screen.getByRole('heading', { name: /title/i });\nconst input = screen.getByLabelText('Email');\nconst items = screen.getAllByRole('listitem');\n```\n\n```jsx\n// Enzyme - find by text:\nwrapper.find('.message').text() === 'Hello'\n\n// RTL:\nscreen.getByText('Hello')\nscreen.getByText(/hello/i)  // case-insensitive regex\n```\n\n---\n\n## User Interaction\n\n```jsx\n// Enzyme:\nwrapper.find('button').simulate('click');\nwrapper.find('input').simulate('change', { target: { value: 'hello' } });\nwrapper.find('form').simulate('submit');\n\n// RTL - fireEvent (synchronous, low-level):\nimport { fireEvent } from '@testing-library/react';\nfireEvent.click(screen.getByRole('button'));\nfireEvent.change(screen.getByRole('textbox'), { target: { value: 'hello' } });\nfireEvent.submit(screen.getByRole('form'));\n\n// RTL - userEvent (preferred, simulates real user behavior):\nimport userEvent from '@testing-library/user-event';\nconst user = userEvent.setup();\nawait user.click(screen.getByRole('button'));\nawait user.type(screen.getByRole('textbox'), 'hello');\nawait user.selectOptions(screen.getByRole('combobox'), 'option1');\n```\n\n**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.\n\n---\n\n## Assertions on Props and State\n\n```jsx\n// Enzyme - prop assertion:\nexpect(wrapper.find('input').prop('disabled')).toBe(true);\nexpect(wrapper.prop('className')).toContain('active');\n\n// RTL - assert on visible attributes:\nexpect(screen.getByRole('textbox')).toBeDisabled();\nexpect(screen.getByRole('button')).toHaveAttribute('type', 'submit');\nexpect(screen.getByRole('listitem')).toHaveClass('active');\n```\n\n```jsx\n// Enzyme - state assertion (NO RTL EQUIVALENT):\nexpect(wrapper.state('count')).toBe(3);\nexpect(wrapper.state('loading')).toBe(false);\n\n// RTL - assert on what the state renders:\nexpect(screen.getByText('Count: 3')).toBeInTheDocument();\nexpect(screen.queryByText('Loading...')).not.toBeInTheDocument();\n```\n\n**Key principle:** Don't test state values - test what the state produces in the UI. If the component renders `\u003cspan\u003eCount: {this.state.count}\u003c/span\u003e`, test that span.\n\n---\n\n## Instance Methods\n\n```jsx\n// Enzyme - direct method call (NO RTL EQUIVALENT):\nwrapper.instance().handleSubmit();\nwrapper.instance().loadData();\n\n// RTL - trigger through the UI:\nawait userEvent.setup().click(screen.getByRole('button', { name: /submit/i }));\n// Or if no UI trigger exists, reconsider: should internal methods be tested directly?\n// Usually the answer is no - test the rendered outcome instead.\n```\n\n---\n\n## Existence Checks\n\n```jsx\n// Enzyme:\nexpect(wrapper.find('.error')).toHaveLength(1);\nexpect(wrapper.find('.error')).toHaveLength(0);\nexpect(wrapper.exists('.error')).toBe(true);\n\n// RTL:\nexpect(screen.getByText('Error message')).toBeInTheDocument();\nexpect(screen.queryByText('Error message')).not.toBeInTheDocument();\n// queryBy returns null instead of throwing when not found\n// getBy throws if not found - use in positive assertions\n// findBy returns a promise - use for async elements\n```\n\n---\n\n## Multiple Elements\n\n```jsx\n// Enzyme:\nexpect(wrapper.find('li')).toHaveLength(5);\nwrapper.find('li').forEach((item, i) =\u003e {\n  expect(item.text()).toBe(expectedItems[i]);\n});\n\n// RTL:\nconst items = screen.getAllByRole('listitem');\nexpect(items).toHaveLength(5);\nitems.forEach((item, i) =\u003e {\n  expect(item).toHaveTextContent(expectedItems[i]);\n});\n```\n\n---\n\n## Before/After: Complete Component Test\n\n```jsx\n// Enzyme version:\nimport { shallow } from 'enzyme';\n\ndescribe('LoginForm', () =\u003e {\n  it('submits with credentials', () =\u003e {\n    const mockSubmit = jest.fn();\n    const wrapper = shallow(\u003cLoginForm onSubmit={mockSubmit} /\u003e);\n\n    wrapper.find('input[name=\"email\"]').simulate('change', {\n      target: { value: 'user@example.com' }\n    });\n    wrapper.find('input[name=\"password\"]').simulate('change', {\n      target: { value: 'password123' }\n    });\n    wrapper.find('button[type=\"submit\"]').simulate('click');\n\n    expect(wrapper.state('loading')).toBe(true);\n    expect(mockSubmit).toHaveBeenCalledWith({\n      email: 'user@example.com',\n      password: 'password123'\n    });\n  });\n});\n```\n\n```jsx\n// RTL version:\nimport { render, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\n\ndescribe('LoginForm', () =\u003e {\n  it('submits with credentials', async () =\u003e {\n    const mockSubmit = jest.fn();\n    const user = userEvent.setup();\n    render(\u003cLoginForm onSubmit={mockSubmit} /\u003e);\n\n    await user.type(screen.getByLabelText(/email/i), 'user@example.com');\n    await user.type(screen.getByLabelText(/password/i), 'password123');\n    await user.click(screen.getByRole('button', { name: /submit/i }));\n\n    // Assert on visible output - not on state\n    expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled(); // loading state\n    expect(mockSubmit).toHaveBeenCalledWith({\n      email: 'user@example.com',\n      password: 'password123'\n    });\n  });\n});\n```\n"},"import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/tree/541b7819d8c3545c6df122491af4fa1eae415779/plugins/react18-upgrade/skills/react18-enzyme-to-rtl"}},"content_hash":[142,53,112,145,4,184,111,32,155,235,125,120,222,215,56,126,20,41,78,207,237,69,108,241,81,247,118,66,207,112,110,125],"trust_level":"unsigned","yanked":false}
