[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6ENZ0B4S9C48XGPB0FNAAVW":3,"skill-same:01M6ENZ0B4S9C48XGPB0FNAAVW":81},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":47,"linkOnly":48,"markdownContent":49,"markdownBody":50,"frontMatter":51,"refName":52,"commitSha":53,"skillDirectory":54,"forksCount":55,"repositoryHtmlUrl":56,"repositoryHomepageUrl":57,"ownerHtmlUrl":58,"ownerBlogUrl":6,"ownerDescription":59,"updatedAt":60,"install":61,"resources":74},"01M6ENZ0B4S9C48XGPB0FNAAVW","react-testing",null,"Testing patterns for React with Jest and React Testing Library. Use when writing tests, mocking modules, testing Zustand stores, or debugging test failures in React web applications.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fcjharmath\u002Freact-testing\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",[27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],"react","react testing","testing","applications","react web","mocking","tests","jest","patterns","modules","use","zustand","testing patterns","library","testing library","writing tests","debugging","web","test","writing","2026-11-10 10:12:30",false,"---\nname: react-testing\ndescription: Testing patterns for React with Jest and React Testing Library. Use when writing tests, mocking modules, testing Zustand stores, or debugging test failures in React web applications.\n---\n\n# React Testing (Web)\n\n## Problem Statement\n\nReact testing requires understanding component rendering, user interactions, and async state management. This skill covers Jest with React Testing Library patterns for web applications.\n\n---\n\n## Pattern: Zustand Store Testing\n\n**Problem:** Store state persists between tests, causing flaky tests.\n\n```typescript\nimport { useAppStore } from '@\u002Fstores\u002FappStore';\n\nconst initialState = {\n  items: [],\n  loading: false,\n  error: null,\n};\n\ndescribe('App Store', () => {\n  \u002F\u002F Reset store before each test\n  beforeEach(() => {\n    useAppStore.setState(initialState, true); \u002F\u002F true = replace entire state\n  });\n\n  it('adds item to store', async () => {\n    const store = useAppStore.getState();\n\n    await store.addItem({ id: '1', name: 'Test' });\n\n    expect(useAppStore.getState().items).toHaveLength(1);\n  });\n\n  it('handles loading state', async () => {\n    const store = useAppStore.getState();\n\n    const loadPromise = store.fetchItems();\n    expect(useAppStore.getState().loading).toBe(true);\n\n    await loadPromise;\n    expect(useAppStore.getState().loading).toBe(false);\n  });\n});\n```\n\n**Key points:**\n- Use `setState(initialState, true)` to replace (not merge) state\n- Get fresh state with `getState()` after async operations\n- Don't rely on component re-renders in store tests\n\n---\n\n## Pattern: Async Store Operations\n\n**Problem:** Testing async Zustand actions with proper waiting.\n\n```typescript\nimport { act, waitFor } from '@testing-library\u002Freact';\n\nit('loads data correctly', async () => {\n  const store = useAppStore.getState();\n\n  \u002F\u002F Wrap async store operations in act\n  await act(async () => {\n    await store.loadData('123');\n  });\n\n  \u002F\u002F Verify state after async completes\n  await waitFor(() => {\n    const state = useAppStore.getState();\n    expect(Object.keys(state.data).length).toBeGreaterThan(0);\n  });\n});\n\n\u002F\u002F For complex flows, verify each step\nit('completes multi-step flow', async () => {\n  const store = useAppStore.getState();\n\n  \u002F\u002F Step 1\n  await act(async () => {\n    await store.loadItems();\n  });\n  expect(useAppStore.getState().items).toBeDefined();\n\n  \u002F\u002F Step 2\n  await act(async () => {\n    await store.processItems();\n  });\n  expect(useAppStore.getState().processed).toBe(true);\n});\n```\n\n---\n\n## Pattern: Component Testing\n\n```typescript\nimport { render, screen, fireEvent, waitFor } from '@testing-library\u002Freact';\nimport userEvent from '@testing-library\u002Fuser-event';\n\ndescribe('ItemCard', () => {\n  const mockItem = {\n    id: '1',\n    title: 'Test Item',\n    price: 99.99,\n  };\n\n  it('displays item data', () => {\n    render(\u003CItemCard item={mockItem} \u002F>);\n\n    expect(screen.getByText('Test Item')).toBeInTheDocument();\n    expect(screen.getByText('$99.99')).toBeInTheDocument();\n  });\n\n  it('calls onClick when clicked', async () => {\n    const user = userEvent.setup();\n    const onClick = jest.fn();\n\n    render(\u003CItemCard item={mockItem} onClick={onClick} \u002F>);\n\n    await user.click(screen.getByRole('button'));\n\n    expect(onClick).toHaveBeenCalledWith(mockItem.id);\n  });\n\n  it('shows loading state', () => {\n    render(\u003CItemCard item={mockItem} loading \u002F>);\n\n    expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();\n  });\n});\n```\n\n---\n\n## Pattern: React Query Testing\n\n**Problem:** Components using React Query need QueryClientProvider.\n\n```typescript\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query';\nimport { render, screen, waitFor } from '@testing-library\u002Freact';\n\nfunction createTestQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,\n        gcTime: 0,\n      },\n    },\n  });\n}\n\nfunction renderWithQuery(ui: React.ReactElement) {\n  const queryClient = createTestQueryClient();\n  return render(\n    \u003CQueryClientProvider client={queryClient}>\n      {ui}\n    \u003C\u002FQueryClientProvider>\n  );\n}\n\n\u002F\u002F Usage in tests\nit('fetches and displays data', async () => {\n  renderWithQuery(\u003CUserProfile userId=\"123\" \u002F>);\n\n  \u002F\u002F Shows loading initially\n  expect(screen.getByText('Loading...')).toBeInTheDocument();\n\n  \u002F\u002F Wait for data\n  await waitFor(() => {\n    expect(screen.getByText('John Doe')).toBeInTheDocument();\n  });\n});\n```\n\n---\n\n## Pattern: Custom Hook Testing\n\n```typescript\nimport { renderHook, act, waitFor } from '@testing-library\u002Freact';\n\ndescribe('useAuth', () => {\n  it('signs in user', async () => {\n    const { result } = renderHook(() => useAuth(), {\n      wrapper: AuthProvider, \u002F\u002F If hook needs context\n    });\n\n    await act(async () => {\n      await result.current.signIn('test@example.com', 'password');\n    });\n\n    expect(result.current.user).toBeDefined();\n    expect(result.current.isAuthenticated).toBe(true);\n  });\n\n  it('handles sign in error', async () => {\n    const { result } = renderHook(() => useAuth(), {\n      wrapper: AuthProvider,\n    });\n\n    await act(async () => {\n      try {\n        await result.current.signIn('invalid@example.com', 'wrong');\n      } catch (e) {\n        \u002F\u002F Expected\n      }\n    });\n\n    expect(result.current.error).toBe('Invalid credentials');\n  });\n});\n\n\u002F\u002F Hook with Zustand\ndescribe('useUserData', () => {\n  beforeEach(() => {\n    useUserStore.setState(initialState, true);\n  });\n\n  it('returns current user data', () => {\n    \u002F\u002F Pre-populate store\n    useUserStore.setState({ user: { id: '1', name: 'Test' } });\n\n    const { result } = renderHook(() => useUserData());\n\n    expect(result.current.user.name).toBe('Test');\n  });\n});\n```\n\n---\n\n## Pattern: Mocking API Calls\n\n```typescript\n\u002F\u002F Mock fetch globally\nglobal.fetch = jest.fn();\n\nbeforeEach(() => {\n  (fetch as jest.Mock).mockClear();\n});\n\nit('fetches user data', async () => {\n  (fetch as jest.Mock).mockResolvedValueOnce({\n    ok: true,\n    json: async () => ({ id: '1', name: 'John' }),\n  });\n\n  render(\u003CUserProfile userId=\"1\" \u002F>);\n\n  await waitFor(() => {\n    expect(screen.getByText('John')).toBeInTheDocument();\n  });\n\n  expect(fetch).toHaveBeenCalledWith('\u002Fapi\u002Fusers\u002F1');\n});\n\n\u002F\u002F Mock specific module\njest.mock('@\u002Fapi\u002Fusers', () => ({\n  getUser: jest.fn(),\n  updateUser: jest.fn(),\n}));\n\nimport { getUser, updateUser } from '@\u002Fapi\u002Fusers';\n\nit('loads and updates user', async () => {\n  (getUser as jest.Mock).mockResolvedValue({ id: '1', name: 'John' });\n  (updateUser as jest.Mock).mockResolvedValue({ id: '1', name: 'Jane' });\n\n  \u002F\u002F Test component that uses these\n});\n```\n\n---\n\n## Pattern: Router Testing\n\n```typescript\nimport { MemoryRouter, Routes, Route } from 'react-router-dom';\n\nfunction renderWithRouter(ui: React.ReactElement, { route = '\u002F' } = {}) {\n  return render(\n    \u003CMemoryRouter initialEntries={[route]}>\n      {ui}\n    \u003C\u002FMemoryRouter>\n  );\n}\n\n\u002F\u002F Test navigation\nit('navigates to profile on button click', async () => {\n  const user = userEvent.setup();\n\n  renderWithRouter(\n    \u003CRoutes>\n      \u003CRoute path=\"\u002F\" element={\u003CHomePage \u002F>} \u002F>\n      \u003CRoute path=\"\u002Fprofile\" element={\u003CProfilePage \u002F>} \u002F>\n    \u003C\u002FRoutes>\n  );\n\n  await user.click(screen.getByText('Go to Profile'));\n\n  expect(screen.getByText('Profile Page')).toBeInTheDocument();\n});\n\n\u002F\u002F Test with route params\nit('displays user from route params', async () => {\n  renderWithRouter(\n    \u003CRoutes>\n      \u003CRoute path=\"\u002Fusers\u002F:id\" element={\u003CUserPage \u002F>} \u002F>\n    \u003C\u002FRoutes>,\n    { route: '\u002Fusers\u002F123' }\n  );\n\n  await waitFor(() => {\n    expect(screen.getByText('User 123')).toBeInTheDocument();\n  });\n});\n```\n\n---\n\n## Pattern: Form Testing\n\n```typescript\nimport userEvent from '@testing-library\u002Fuser-event';\n\ndescribe('LoginForm', () => {\n  it('submits form with entered data', async () => {\n    const user = userEvent.setup();\n    const onSubmit = jest.fn();\n\n    render(\u003CLoginForm onSubmit={onSubmit} \u002F>);\n\n    await user.type(screen.getByLabelText('Email'), 'test@example.com');\n    await user.type(screen.getByLabelText('Password'), 'password123');\n    await user.click(screen.getByRole('button', { name: 'Sign In' }));\n\n    expect(onSubmit).toHaveBeenCalledWith({\n      email: 'test@example.com',\n      password: 'password123',\n    });\n  });\n\n  it('shows validation errors', async () => {\n    const user = userEvent.setup();\n\n    render(\u003CLoginForm onSubmit={jest.fn()} \u002F>);\n\n    \u002F\u002F Submit without filling form\n    await user.click(screen.getByRole('button', { name: 'Sign In' }));\n\n    expect(screen.getByText('Email is required')).toBeInTheDocument();\n    expect(screen.getByText('Password is required')).toBeInTheDocument();\n  });\n\n  it('disables submit while loading', async () => {\n    render(\u003CLoginForm onSubmit={jest.fn()} loading \u002F>);\n\n    expect(screen.getByRole('button', { name: 'Sign In' })).toBeDisabled();\n  });\n});\n```\n\n---\n\n## Pattern: Avoiding act() Warnings\n\n**Problem:** \"Warning: An update inside a test was not wrapped in act(...)\"\n\n```typescript\n\u002F\u002F WRONG - state update happens after test\nit('loads data', () => {\n  render(\u003CDataComponent \u002F>);\n  \u002F\u002F Component fetches data async, updates state after test ends\n});\n\n\u002F\u002F CORRECT - wait for async completion\nit('loads data', async () => {\n  render(\u003CDataComponent \u002F>);\n\n  \u002F\u002F Wait for loading to complete\n  await waitFor(() => {\n    expect(screen.getByText('Data loaded')).toBeInTheDocument();\n  });\n});\n\n\u002F\u002F CORRECT - use findBy* (has built-in waitFor)\nit('loads data', async () => {\n  render(\u003CDataComponent \u002F>);\n\n  const element = await screen.findByText('Data loaded');\n  expect(element).toBeInTheDocument();\n});\n```\n\n---\n\n## Pattern: Snapshot Testing\n\n**When to use:**\n- UI components with stable structure\n- Design system components\n- Components where visual regression matters\n\n**When to avoid:**\n- Components with dynamic content\n- Components that change frequently\n- Large component trees (brittle)\n\n```typescript\n\u002F\u002F Good snapshot candidate - stable UI component\nit('renders correctly', () => {\n  const { container } = render(\u003CButton variant=\"primary\">Submit\u003C\u002FButton>);\n  expect(container).toMatchSnapshot();\n});\n\n\u002F\u002F Bad snapshot candidate - dynamic content\nit('renders user list', () => {\n  \u002F\u002F Don't snapshot - list content varies\n  \u002F\u002F Instead, test specific behaviors\n});\n```\n\n---\n\n## Pattern: Testing Context Providers\n\n```typescript\n\u002F\u002F Create a wrapper with all providers\nfunction AllProviders({ children }: { children: React.ReactNode }) {\n  const queryClient = createTestQueryClient();\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      \u003CAuthProvider>\n        \u003CThemeProvider>\n          {children}\n        \u003C\u002FThemeProvider>\n      \u003C\u002FAuthProvider>\n    \u003C\u002FQueryClientProvider>\n  );\n}\n\nfunction renderWithProviders(ui: React.ReactElement) {\n  return render(ui, { wrapper: AllProviders });\n}\n\n\u002F\u002F Use in tests\nit('renders with all context', () => {\n  renderWithProviders(\u003CDashboard \u002F>);\n  \u002F\u002F Component has access to all providers\n});\n```\n\n---\n\n## Test Commands\n\n```bash\nnpm test                      # Run all tests\nnpm test -- --watch           # Watch mode\nnpm test -- --coverage        # Coverage report\nnpm test -- Button            # Run specific test file\nnpm test -- --updateSnapshot  # Update snapshots\nnpm test -- --runInBand       # Run tests serially (debugging)\n```\n\n---\n\n## Common Issues\n\n| Issue | Solution |\n|-------|----------|\n| \"Cannot find module\" | Check jest moduleNameMapper config |\n| act() warning | Wrap state updates in act(), use waitFor\u002FfindBy |\n| Store state bleeding | Add beforeEach with setState reset |\n| Async test timeout | Increase timeout or check for hanging promises |\n| Mock not working | Verify mock path matches import path exactly |\n| Query not found | Use findBy* for async content, check accessibility |\n\n---\n\n## Recommended File Structure\n\n```\n__tests__\u002F\n  utils\u002F\n    test-utils.tsx         # Custom render with providers\n    query-test-utils.tsx   # QueryClient wrapper\njest.setup.js              # Global mocks and setup\njest.config.js             # Jest configuration\n```\n","# React Testing (Web)\n\n## Problem Statement\n\nReact testing requires understanding component rendering, user interactions, and async state management. This skill covers Jest with React Testing Library patterns for web applications.\n\n---\n\n## Pattern: Zustand Store Testing\n\n**Problem:** Store state persists between tests, causing flaky tests.\n\n```typescript\nimport { useAppStore } from '@\u002Fstores\u002FappStore';\n\nconst initialState = {\n  items: [],\n  loading: false,\n  error: null,\n};\n\ndescribe('App Store', () => {\n  \u002F\u002F Reset store before each test\n  beforeEach(() => {\n    useAppStore.setState(initialState, true); \u002F\u002F true = replace entire state\n  });\n\n  it('adds item to store', async () => {\n    const store = useAppStore.getState();\n\n    await store.addItem({ id: '1', name: 'Test' });\n\n    expect(useAppStore.getState().items).toHaveLength(1);\n  });\n\n  it('handles loading state', async () => {\n    const store = useAppStore.getState();\n\n    const loadPromise = store.fetchItems();\n    expect(useAppStore.getState().loading).toBe(true);\n\n    await loadPromise;\n    expect(useAppStore.getState().loading).toBe(false);\n  });\n});\n```\n\n**Key points:**\n- Use `setState(initialState, true)` to replace (not merge) state\n- Get fresh state with `getState()` after async operations\n- Don't rely on component re-renders in store tests\n\n---\n\n## Pattern: Async Store Operations\n\n**Problem:** Testing async Zustand actions with proper waiting.\n\n```typescript\nimport { act, waitFor } from '@testing-library\u002Freact';\n\nit('loads data correctly', async () => {\n  const store = useAppStore.getState();\n\n  \u002F\u002F Wrap async store operations in act\n  await act(async () => {\n    await store.loadData('123');\n  });\n\n  \u002F\u002F Verify state after async completes\n  await waitFor(() => {\n    const state = useAppStore.getState();\n    expect(Object.keys(state.data).length).toBeGreaterThan(0);\n  });\n});\n\n\u002F\u002F For complex flows, verify each step\nit('completes multi-step flow', async () => {\n  const store = useAppStore.getState();\n\n  \u002F\u002F Step 1\n  await act(async () => {\n    await store.loadItems();\n  });\n  expect(useAppStore.getState().items).toBeDefined();\n\n  \u002F\u002F Step 2\n  await act(async () => {\n    await store.processItems();\n  });\n  expect(useAppStore.getState().processed).toBe(true);\n});\n```\n\n---\n\n## Pattern: Component Testing\n\n```typescript\nimport { render, screen, fireEvent, waitFor } from '@testing-library\u002Freact';\nimport userEvent from '@testing-library\u002Fuser-event';\n\ndescribe('ItemCard', () => {\n  const mockItem = {\n    id: '1',\n    title: 'Test Item',\n    price: 99.99,\n  };\n\n  it('displays item data', () => {\n    render(\u003CItemCard item={mockItem} \u002F>);\n\n    expect(screen.getByText('Test Item')).toBeInTheDocument();\n    expect(screen.getByText('$99.99')).toBeInTheDocument();\n  });\n\n  it('calls onClick when clicked', async () => {\n    const user = userEvent.setup();\n    const onClick = jest.fn();\n\n    render(\u003CItemCard item={mockItem} onClick={onClick} \u002F>);\n\n    await user.click(screen.getByRole('button'));\n\n    expect(onClick).toHaveBeenCalledWith(mockItem.id);\n  });\n\n  it('shows loading state', () => {\n    render(\u003CItemCard item={mockItem} loading \u002F>);\n\n    expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();\n  });\n});\n```\n\n---\n\n## Pattern: React Query Testing\n\n**Problem:** Components using React Query need QueryClientProvider.\n\n```typescript\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query';\nimport { render, screen, waitFor } from '@testing-library\u002Freact';\n\nfunction createTestQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,\n        gcTime: 0,\n      },\n    },\n  });\n}\n\nfunction renderWithQuery(ui: React.ReactElement) {\n  const queryClient = createTestQueryClient();\n  return render(\n    \u003CQueryClientProvider client={queryClient}>\n      {ui}\n    \u003C\u002FQueryClientProvider>\n  );\n}\n\n\u002F\u002F Usage in tests\nit('fetches and displays data', async () => {\n  renderWithQuery(\u003CUserProfile userId=\"123\" \u002F>);\n\n  \u002F\u002F Shows loading initially\n  expect(screen.getByText('Loading...')).toBeInTheDocument();\n\n  \u002F\u002F Wait for data\n  await waitFor(() => {\n    expect(screen.getByText('John Doe')).toBeInTheDocument();\n  });\n});\n```\n\n---\n\n## Pattern: Custom Hook Testing\n\n```typescript\nimport { renderHook, act, waitFor } from '@testing-library\u002Freact';\n\ndescribe('useAuth', () => {\n  it('signs in user', async () => {\n    const { result } = renderHook(() => useAuth(), {\n      wrapper: AuthProvider, \u002F\u002F If hook needs context\n    });\n\n    await act(async () => {\n      await result.current.signIn('test@example.com', 'password');\n    });\n\n    expect(result.current.user).toBeDefined();\n    expect(result.current.isAuthenticated).toBe(true);\n  });\n\n  it('handles sign in error', async () => {\n    const { result } = renderHook(() => useAuth(), {\n      wrapper: AuthProvider,\n    });\n\n    await act(async () => {\n      try {\n        await result.current.signIn('invalid@example.com', 'wrong');\n      } catch (e) {\n        \u002F\u002F Expected\n      }\n    });\n\n    expect(result.current.error).toBe('Invalid credentials');\n  });\n});\n\n\u002F\u002F Hook with Zustand\ndescribe('useUserData', () => {\n  beforeEach(() => {\n    useUserStore.setState(initialState, true);\n  });\n\n  it('returns current user data', () => {\n    \u002F\u002F Pre-populate store\n    useUserStore.setState({ user: { id: '1', name: 'Test' } });\n\n    const { result } = renderHook(() => useUserData());\n\n    expect(result.current.user.name).toBe('Test');\n  });\n});\n```\n\n---\n\n## Pattern: Mocking API Calls\n\n```typescript\n\u002F\u002F Mock fetch globally\nglobal.fetch = jest.fn();\n\nbeforeEach(() => {\n  (fetch as jest.Mock).mockClear();\n});\n\nit('fetches user data', async () => {\n  (fetch as jest.Mock).mockResolvedValueOnce({\n    ok: true,\n    json: async () => ({ id: '1', name: 'John' }),\n  });\n\n  render(\u003CUserProfile userId=\"1\" \u002F>);\n\n  await waitFor(() => {\n    expect(screen.getByText('John')).toBeInTheDocument();\n  });\n\n  expect(fetch).toHaveBeenCalledWith('\u002Fapi\u002Fusers\u002F1');\n});\n\n\u002F\u002F Mock specific module\njest.mock('@\u002Fapi\u002Fusers', () => ({\n  getUser: jest.fn(),\n  updateUser: jest.fn(),\n}));\n\nimport { getUser, updateUser } from '@\u002Fapi\u002Fusers';\n\nit('loads and updates user', async () => {\n  (getUser as jest.Mock).mockResolvedValue({ id: '1', name: 'John' });\n  (updateUser as jest.Mock).mockResolvedValue({ id: '1', name: 'Jane' });\n\n  \u002F\u002F Test component that uses these\n});\n```\n\n---\n\n## Pattern: Router Testing\n\n```typescript\nimport { MemoryRouter, Routes, Route } from 'react-router-dom';\n\nfunction renderWithRouter(ui: React.ReactElement, { route = '\u002F' } = {}) {\n  return render(\n    \u003CMemoryRouter initialEntries={[route]}>\n      {ui}\n    \u003C\u002FMemoryRouter>\n  );\n}\n\n\u002F\u002F Test navigation\nit('navigates to profile on button click', async () => {\n  const user = userEvent.setup();\n\n  renderWithRouter(\n    \u003CRoutes>\n      \u003CRoute path=\"\u002F\" element={\u003CHomePage \u002F>} \u002F>\n      \u003CRoute path=\"\u002Fprofile\" element={\u003CProfilePage \u002F>} \u002F>\n    \u003C\u002FRoutes>\n  );\n\n  await user.click(screen.getByText('Go to Profile'));\n\n  expect(screen.getByText('Profile Page')).toBeInTheDocument();\n});\n\n\u002F\u002F Test with route params\nit('displays user from route params', async () => {\n  renderWithRouter(\n    \u003CRoutes>\n      \u003CRoute path=\"\u002Fusers\u002F:id\" element={\u003CUserPage \u002F>} \u002F>\n    \u003C\u002FRoutes>,\n    { route: '\u002Fusers\u002F123' }\n  );\n\n  await waitFor(() => {\n    expect(screen.getByText('User 123')).toBeInTheDocument();\n  });\n});\n```\n\n---\n\n## Pattern: Form Testing\n\n```typescript\nimport userEvent from '@testing-library\u002Fuser-event';\n\ndescribe('LoginForm', () => {\n  it('submits form with entered data', async () => {\n    const user = userEvent.setup();\n    const onSubmit = jest.fn();\n\n    render(\u003CLoginForm onSubmit={onSubmit} \u002F>);\n\n    await user.type(screen.getByLabelText('Email'), 'test@example.com');\n    await user.type(screen.getByLabelText('Password'), 'password123');\n    await user.click(screen.getByRole('button', { name: 'Sign In' }));\n\n    expect(onSubmit).toHaveBeenCalledWith({\n      email: 'test@example.com',\n      password: 'password123',\n    });\n  });\n\n  it('shows validation errors', async () => {\n    const user = userEvent.setup();\n\n    render(\u003CLoginForm onSubmit={jest.fn()} \u002F>);\n\n    \u002F\u002F Submit without filling form\n    await user.click(screen.getByRole('button', { name: 'Sign In' }));\n\n    expect(screen.getByText('Email is required')).toBeInTheDocument();\n    expect(screen.getByText('Password is required')).toBeInTheDocument();\n  });\n\n  it('disables submit while loading', async () => {\n    render(\u003CLoginForm onSubmit={jest.fn()} loading \u002F>);\n\n    expect(screen.getByRole('button', { name: 'Sign In' })).toBeDisabled();\n  });\n});\n```\n\n---\n\n## Pattern: Avoiding act() Warnings\n\n**Problem:** \"Warning: An update inside a test was not wrapped in act(...)\"\n\n```typescript\n\u002F\u002F WRONG - state update happens after test\nit('loads data', () => {\n  render(\u003CDataComponent \u002F>);\n  \u002F\u002F Component fetches data async, updates state after test ends\n});\n\n\u002F\u002F CORRECT - wait for async completion\nit('loads data', async () => {\n  render(\u003CDataComponent \u002F>);\n\n  \u002F\u002F Wait for loading to complete\n  await waitFor(() => {\n    expect(screen.getByText('Data loaded')).toBeInTheDocument();\n  });\n});\n\n\u002F\u002F CORRECT - use findBy* (has built-in waitFor)\nit('loads data', async () => {\n  render(\u003CDataComponent \u002F>);\n\n  const element = await screen.findByText('Data loaded');\n  expect(element).toBeInTheDocument();\n});\n```\n\n---\n\n## Pattern: Snapshot Testing\n\n**When to use:**\n- UI components with stable structure\n- Design system components\n- Components where visual regression matters\n\n**When to avoid:**\n- Components with dynamic content\n- Components that change frequently\n- Large component trees (brittle)\n\n```typescript\n\u002F\u002F Good snapshot candidate - stable UI component\nit('renders correctly', () => {\n  const { container } = render(\u003CButton variant=\"primary\">Submit\u003C\u002FButton>);\n  expect(container).toMatchSnapshot();\n});\n\n\u002F\u002F Bad snapshot candidate - dynamic content\nit('renders user list', () => {\n  \u002F\u002F Don't snapshot - list content varies\n  \u002F\u002F Instead, test specific behaviors\n});\n```\n\n---\n\n## Pattern: Testing Context Providers\n\n```typescript\n\u002F\u002F Create a wrapper with all providers\nfunction AllProviders({ children }: { children: React.ReactNode }) {\n  const queryClient = createTestQueryClient();\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      \u003CAuthProvider>\n        \u003CThemeProvider>\n          {children}\n        \u003C\u002FThemeProvider>\n      \u003C\u002FAuthProvider>\n    \u003C\u002FQueryClientProvider>\n  );\n}\n\nfunction renderWithProviders(ui: React.ReactElement) {\n  return render(ui, { wrapper: AllProviders });\n}\n\n\u002F\u002F Use in tests\nit('renders with all context', () => {\n  renderWithProviders(\u003CDashboard \u002F>);\n  \u002F\u002F Component has access to all providers\n});\n```\n\n---\n\n## Test Commands\n\n```bash\nnpm test                      # Run all tests\nnpm test -- --watch           # Watch mode\nnpm test -- --coverage        # Coverage report\nnpm test -- Button            # Run specific test file\nnpm test -- --updateSnapshot  # Update snapshots\nnpm test -- --runInBand       # Run tests serially (debugging)\n```\n\n---\n\n## Common Issues\n\n| Issue | Solution |\n|-------|----------|\n| \"Cannot find module\" | Check jest moduleNameMapper config |\n| act() warning | Wrap state updates in act(), use waitFor\u002FfindBy |\n| Store state bleeding | Add beforeEach with setState reset |\n| Async test timeout | Increase timeout or check for hanging promises |\n| Mock not working | Verify mock path matches import path exactly |\n| Query not found | Use findBy* for async content, check accessibility |\n\n---\n\n## Recommended File Structure\n\n```\n__tests__\u002F\n  utils\u002F\n    test-utils.tsx         # Custom render with providers\n    query-test-utils.tsx   # QueryClient wrapper\njest.setup.js              # Global mocks and setup\njest.config.js             # Jest configuration\n```\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Faiskillstore\u002Fmarketplace\u002Fcjharmath\u002Freact-testing",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":73},[63,66,69],{"tool":64,"command":65},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill react-testing",{"tool":67,"command":68},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill react-testing",{"tool":70,"command":71},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill react-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fcjharmath\u002Freact-testing\u002FSKILL.md",true,[75],{"path":76,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":79,"sourceHtmlUrl":80},"skill-report.json","REFERENCE","参考文档",11089,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fcjharmath\u002Freact-testing\u002Fskill-report.json",[82,94,104,113,119,126],{"skillNo":83,"name":84,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":90,"publishedAt":47},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[87],{"categoryNo":88,"categoryName":89,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[91,92,93],"model","model hierarchy","model hierarchy skill",{"skillNo":95,"name":96,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":100,"publishedAt":47},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[99],{"categoryNo":88,"categoryName":89,"taxonomyType":21,"taxonomyTypeDesc":22},[101,102,103],"MCP","zoom","zoom mcp",{"skillNo":105,"name":106,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":109,"publishedAt":47},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[110,111,112,102],"sdk","zoom apps sdk","apps",{"skillNo":114,"name":115,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":118,"publishedAt":47},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[115],{"skillNo":120,"name":121,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":122,"categories":123,"keywords":125,"publishedAt":47},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[124],{"categoryNo":88,"categoryName":89,"taxonomyType":21,"taxonomyTypeDesc":22},[121],{"skillNo":127,"name":128,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":131,"publishedAt":47},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[128]]