[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EP02FX8ZSNZ1ECKKXACH40":3,"skill-same:01M6EP02FX8ZSNZ1ECKKXACH40":133},{"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},"01M6EP02FX8ZSNZ1ECKKXACH40","rn-testing",null,"Testing patterns for React Native with Jest and React Native Testing Library. Use when writing tests, mocking Expo modules, testing Zustand stores, or debugging test failures.","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\u002Frn-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],"rn testing","testing","react native","native","mocking","tests","jest","patterns","expo","modules","use","zustand","testing patterns","library","testing library","writing tests","debugging","react","test","writing","2026-11-10 10:12:30",false,"---\nname: rn-testing\ndescription: Testing patterns for React Native with Jest and React Native Testing Library. Use when writing tests, mocking Expo modules, testing Zustand stores, or debugging test failures.\n---\n\n# React Native Testing\n\n## Problem Statement\n\nReact Native testing requires extensive mocking of native modules, careful handling of async operations, and understanding of Zustand store testing patterns. This codebase has 30+ test files with established patterns.\n\n---\n\n## Pattern: Zustand Store Testing\n\n**Problem:** Store state persists between tests, causing flaky tests.\n\n```typescript\nimport { useAssessmentStore } from '@\u002Fstores\u002FassessmentStore';\n\nconst initialState = {\n  userAnswers: {},\n  completedAssessmentAnswers: {},\n  retakeAreas: new Set\u003Cstring>(),\n  loading: false,\n};\n\ndescribe('Assessment Store', () => {\n  \u002F\u002F Reset store before each test\n  beforeEach(() => {\n    useAssessmentStore.setState(initialState, true); \u002F\u002F true = replace entire state\n  });\n\n  it('saves answer to store', async () => {\n    const store = useAssessmentStore.getState();\n    \n    await store.saveAnswer('q1', 4);\n    \n    expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);\n  });\n\n  it('enables retake for skill area', async () => {\n    const store = useAssessmentStore.getState();\n    \n    await store.enableSkillAreaRetake('fundamentals');\n    \n    expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);\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-native';\n\nit('loads completed answers', async () => {\n  const store = useAssessmentStore.getState();\n  \n  \u002F\u002F Wrap async store operations in act\n  await act(async () => {\n    await store.loadCompletedAssessmentAnswers('assessment-123');\n  });\n  \n  \u002F\u002F Verify state after async completes\n  await waitFor(() => {\n    const state = useAssessmentStore.getState();\n    expect(Object.keys(state.completedAssessmentAnswers).length).toBeGreaterThan(0);\n  });\n});\n\n\u002F\u002F For complex flows, verify each step\nit('completes retake flow', async () => {\n  const store = useAssessmentStore.getState();\n  \n  \u002F\u002F Step 1\n  await act(async () => {\n    await store.loadCompletedAssessmentAnswers('assessment-123');\n  });\n  expect(useAssessmentStore.getState().completedAssessmentAnswers).toBeDefined();\n  \n  \u002F\u002F Step 2\n  await act(async () => {\n    await store.enableSkillAreaRetake('fundamentals');\n  });\n  expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);\n  \n  \u002F\u002F Step 3\n  await act(async () => {\n    await store.saveAnswer('q1', 4);\n  });\n  expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);\n});\n```\n\n---\n\n## Pattern: Expo Module Mocking\n\n**Problem:** Expo modules require mocks for Jest.\n\n```typescript\n\u002F\u002F __mocks__\u002Fexpo-router.ts (or in jest.setup.js)\njest.mock('expo-router', () => ({\n  useRouter: () => ({\n    push: jest.fn(),\n    replace: jest.fn(),\n    back: jest.fn(),\n    dismiss: jest.fn(),\n  }),\n  useLocalSearchParams: () => ({}),\n  useSegments: () => [],\n  usePathname: () => '\u002F',\n  Link: ({ children }: { children: React.ReactNode }) => children,\n  Stack: {\n    Screen: () => null,\n  },\n}));\n\n\u002F\u002F expo-secure-store\njest.mock('expo-secure-store', () => ({\n  getItemAsync: jest.fn(),\n  setItemAsync: jest.fn(),\n  deleteItemAsync: jest.fn(),\n}));\n\n\u002F\u002F expo-constants\njest.mock('expo-constants', () => ({\n  expoConfig: {\n    extra: {\n      apiUrl: 'http:\u002F\u002Ftest-api.local',\n    },\n  },\n}));\n\n\u002F\u002F expo-haptics\njest.mock('expo-haptics', () => ({\n  impactAsync: jest.fn(),\n  notificationAsync: jest.fn(),\n  selectionAsync: jest.fn(),\n}));\n```\n\n**Check `jest.setup.js`** - many mocks are already configured globally.\n\n---\n\n## Pattern: React Query Testing\n\n**Problem:** Components using React Query need QueryClientProvider.\n\n```typescript\n\u002F\u002F Use existing utility from codebase\nimport { createTestQueryClient, QueryWrapper } from '@\u002F__tests__\u002Futils\u002Freact-query-test-utils';\n\n\u002F\u002F Or create wrapper\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query';\n\nfunction createTestQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,\n        gcTime: 0,\n      },\n    },\n  });\n}\n\nfunction QueryWrapper({ children }: { children: React.ReactNode }) {\n  const queryClient = createTestQueryClient();\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      {children}\n    \u003C\u002FQueryClientProvider>\n  );\n}\n\n\u002F\u002F Usage in tests\nimport { render, waitFor } from '@testing-library\u002Freact-native';\n\nit('fetches and displays data', async () => {\n  const { getByText } = render(\n    \u003CQueryWrapper>\n      \u003CMyComponent \u002F>\n    \u003C\u002FQueryWrapper>\n  );\n  \n  await waitFor(() => {\n    expect(getByText('Loaded data')).toBeTruthy();\n  });\n});\n```\n\n---\n\n## Pattern: Custom Hook Testing\n\n```typescript\nimport { renderHook, act, waitFor } from '@testing-library\u002Freact-native';\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('token', mockUser);\n    });\n    \n    expect(result.current.user).toEqual(mockUser);\n    expect(result.current.token).toBe('token');\n  });\n});\n\n\u002F\u002F Hook with Zustand\ndescribe('useAssessmentAnswers', () => {\n  beforeEach(() => {\n    useAssessmentStore.setState(initialState, true);\n  });\n\n  it('returns current answers', () => {\n    \u002F\u002F Pre-populate store\n    useAssessmentStore.setState({ userAnswers: { q1: 4 } });\n    \n    const { result } = renderHook(() => useAssessmentAnswers());\n    \n    expect(result.current.answers).toEqual({ q1: 4 });\n  });\n});\n```\n\n---\n\n## Pattern: Component Testing\n\n```typescript\nimport { render, fireEvent, waitFor } from '@testing-library\u002Freact-native';\n\ndescribe('SessionCard', () => {\n  const mockSession = {\n    id: '1',\n    title: 'Serve Practice',\n    totalDuration: 45, \u002F\u002F Backend-calculated\n  };\n\n  it('displays session data from backend', () => {\n    const { getByText } = render(\u003CSessionCard session={mockSession} \u002F>);\n    \n    expect(getByText('Serve Practice')).toBeTruthy();\n    expect(getByText('45 min')).toBeTruthy();\n  });\n\n  it('calls onPress when tapped', () => {\n    const onPress = jest.fn();\n    const { getByTestId } = render(\n      \u003CSessionCard session={mockSession} onPress={onPress} \u002F>\n    );\n    \n    fireEvent.press(getByTestId('session-card'));\n    \n    expect(onPress).toHaveBeenCalledWith(mockSession.id);\n  });\n});\n```\n\n---\n\n## Pattern: Navigation Testing\n\n```typescript\nimport { useRouter } from 'expo-router';\n\njest.mock('expo-router');\n\ndescribe('SettingsScreen', () => {\n  const mockPush = jest.fn();\n  \n  beforeEach(() => {\n    jest.clearAllMocks();\n    (useRouter as jest.Mock).mockReturnValue({\n      push: mockPush,\n      back: jest.fn(),\n    });\n  });\n\n  it('navigates to profile on button press', () => {\n    const { getByText } = render(\u003CSettingsScreen \u002F>);\n    \n    fireEvent.press(getByText('Edit Profile'));\n    \n    expect(mockPush).toHaveBeenCalledWith('\u002Fprofile\u002Fedit');\n  });\n});\n\n\u002F\u002F Testing with route params\njest.mock('expo-router', () => ({\n  useLocalSearchParams: () => ({ id: 'test-assessment-123' }),\n  useRouter: () => ({ push: jest.fn() }),\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  const { getByText } = render(\u003CDataComponent \u002F>);\n  \n  \u002F\u002F Wait for loading to complete\n  await waitFor(() => {\n    expect(getByText('Data loaded')).toBeTruthy();\n  });\n});\n\n\u002F\u002F CORRECT - use findBy* (has built-in waitFor)\nit('loads data', async () => {\n  const { findByText } = render(\u003CDataComponent \u002F>);\n  \n  const element = await findByText('Data loaded');\n  expect(element).toBeTruthy();\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 tree = render(\u003CButton title=\"Submit\" \u002F>);\n  expect(tree.toJSON()).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: Mocking API Calls\n\n```typescript\n\u002F\u002F Mock Orval-generated hooks\njest.mock('@\u002Fapi\u002Fgenerated\u002Fassessments', () => ({\n  useGetAssessment: jest.fn(() => ({\n    data: mockAssessment,\n    isLoading: false,\n    error: null,\n  })),\n  useSubmitAssessment: jest.fn(() => ({\n    mutate: jest.fn(),\n    isLoading: false,\n  })),\n}));\n\n\u002F\u002F Mock with different states\nimport { useGetAssessment } from '@\u002Fapi\u002Fgenerated\u002Fassessments';\n\nit('shows loading state', () => {\n  (useGetAssessment as jest.Mock).mockReturnValue({\n    data: null,\n    isLoading: true,\n    error: null,\n  });\n  \n  const { getByTestId } = render(\u003CAssessmentScreen \u002F>);\n  expect(getByTestId('loading-spinner')).toBeTruthy();\n});\n\nit('shows error state', () => {\n  (useGetAssessment as jest.Mock).mockReturnValue({\n    data: null,\n    isLoading: false,\n    error: new Error('Failed to load'),\n  });\n  \n  const { getByText } = render(\u003CAssessmentScreen \u002F>);\n  expect(getByText('Failed to load')).toBeTruthy();\n});\n```\n\n---\n\n## Recommended Test Utilities\n\n```\n__tests__\u002Futils\u002Freact-query-test-utils.tsx  # QueryClient wrapper\njest.setup.js                                # Global mocks\n```\n\n### Test Commands\n```bash\nnpm test                      # Run all tests\nnpm test -- --watch           # Watch mode\nnpm test -- --coverage        # Coverage report\nnpm test -- SessionCard       # Run specific test file\nnpm test -- --updateSnapshot  # Update snapshots\n```\n\n---\n\n## Common Issues\n\n| Issue | Solution |\n|-------|----------|\n| \"Cannot find module\" | Check jest.setup.js module mappings |\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\n---\n\n## Relationship to Other Skills\n\n- **rn-async-patterns**: Use post-condition validation in tests to verify async flows\n- **rn-zustand-patterns**: Understand `getState()` behavior for store tests\n- **rn-state-flows**: Integration tests should cover entire flows, not just units\n","# React Native Testing\n\n## Problem Statement\n\nReact Native testing requires extensive mocking of native modules, careful handling of async operations, and understanding of Zustand store testing patterns. This codebase has 30+ test files with established patterns.\n\n---\n\n## Pattern: Zustand Store Testing\n\n**Problem:** Store state persists between tests, causing flaky tests.\n\n```typescript\nimport { useAssessmentStore } from '@\u002Fstores\u002FassessmentStore';\n\nconst initialState = {\n  userAnswers: {},\n  completedAssessmentAnswers: {},\n  retakeAreas: new Set\u003Cstring>(),\n  loading: false,\n};\n\ndescribe('Assessment Store', () => {\n  \u002F\u002F Reset store before each test\n  beforeEach(() => {\n    useAssessmentStore.setState(initialState, true); \u002F\u002F true = replace entire state\n  });\n\n  it('saves answer to store', async () => {\n    const store = useAssessmentStore.getState();\n    \n    await store.saveAnswer('q1', 4);\n    \n    expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);\n  });\n\n  it('enables retake for skill area', async () => {\n    const store = useAssessmentStore.getState();\n    \n    await store.enableSkillAreaRetake('fundamentals');\n    \n    expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);\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-native';\n\nit('loads completed answers', async () => {\n  const store = useAssessmentStore.getState();\n  \n  \u002F\u002F Wrap async store operations in act\n  await act(async () => {\n    await store.loadCompletedAssessmentAnswers('assessment-123');\n  });\n  \n  \u002F\u002F Verify state after async completes\n  await waitFor(() => {\n    const state = useAssessmentStore.getState();\n    expect(Object.keys(state.completedAssessmentAnswers).length).toBeGreaterThan(0);\n  });\n});\n\n\u002F\u002F For complex flows, verify each step\nit('completes retake flow', async () => {\n  const store = useAssessmentStore.getState();\n  \n  \u002F\u002F Step 1\n  await act(async () => {\n    await store.loadCompletedAssessmentAnswers('assessment-123');\n  });\n  expect(useAssessmentStore.getState().completedAssessmentAnswers).toBeDefined();\n  \n  \u002F\u002F Step 2\n  await act(async () => {\n    await store.enableSkillAreaRetake('fundamentals');\n  });\n  expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);\n  \n  \u002F\u002F Step 3\n  await act(async () => {\n    await store.saveAnswer('q1', 4);\n  });\n  expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);\n});\n```\n\n---\n\n## Pattern: Expo Module Mocking\n\n**Problem:** Expo modules require mocks for Jest.\n\n```typescript\n\u002F\u002F __mocks__\u002Fexpo-router.ts (or in jest.setup.js)\njest.mock('expo-router', () => ({\n  useRouter: () => ({\n    push: jest.fn(),\n    replace: jest.fn(),\n    back: jest.fn(),\n    dismiss: jest.fn(),\n  }),\n  useLocalSearchParams: () => ({}),\n  useSegments: () => [],\n  usePathname: () => '\u002F',\n  Link: ({ children }: { children: React.ReactNode }) => children,\n  Stack: {\n    Screen: () => null,\n  },\n}));\n\n\u002F\u002F expo-secure-store\njest.mock('expo-secure-store', () => ({\n  getItemAsync: jest.fn(),\n  setItemAsync: jest.fn(),\n  deleteItemAsync: jest.fn(),\n}));\n\n\u002F\u002F expo-constants\njest.mock('expo-constants', () => ({\n  expoConfig: {\n    extra: {\n      apiUrl: 'http:\u002F\u002Ftest-api.local',\n    },\n  },\n}));\n\n\u002F\u002F expo-haptics\njest.mock('expo-haptics', () => ({\n  impactAsync: jest.fn(),\n  notificationAsync: jest.fn(),\n  selectionAsync: jest.fn(),\n}));\n```\n\n**Check `jest.setup.js`** - many mocks are already configured globally.\n\n---\n\n## Pattern: React Query Testing\n\n**Problem:** Components using React Query need QueryClientProvider.\n\n```typescript\n\u002F\u002F Use existing utility from codebase\nimport { createTestQueryClient, QueryWrapper } from '@\u002F__tests__\u002Futils\u002Freact-query-test-utils';\n\n\u002F\u002F Or create wrapper\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query';\n\nfunction createTestQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,\n        gcTime: 0,\n      },\n    },\n  });\n}\n\nfunction QueryWrapper({ children }: { children: React.ReactNode }) {\n  const queryClient = createTestQueryClient();\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      {children}\n    \u003C\u002FQueryClientProvider>\n  );\n}\n\n\u002F\u002F Usage in tests\nimport { render, waitFor } from '@testing-library\u002Freact-native';\n\nit('fetches and displays data', async () => {\n  const { getByText } = render(\n    \u003CQueryWrapper>\n      \u003CMyComponent \u002F>\n    \u003C\u002FQueryWrapper>\n  );\n  \n  await waitFor(() => {\n    expect(getByText('Loaded data')).toBeTruthy();\n  });\n});\n```\n\n---\n\n## Pattern: Custom Hook Testing\n\n```typescript\nimport { renderHook, act, waitFor } from '@testing-library\u002Freact-native';\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('token', mockUser);\n    });\n    \n    expect(result.current.user).toEqual(mockUser);\n    expect(result.current.token).toBe('token');\n  });\n});\n\n\u002F\u002F Hook with Zustand\ndescribe('useAssessmentAnswers', () => {\n  beforeEach(() => {\n    useAssessmentStore.setState(initialState, true);\n  });\n\n  it('returns current answers', () => {\n    \u002F\u002F Pre-populate store\n    useAssessmentStore.setState({ userAnswers: { q1: 4 } });\n    \n    const { result } = renderHook(() => useAssessmentAnswers());\n    \n    expect(result.current.answers).toEqual({ q1: 4 });\n  });\n});\n```\n\n---\n\n## Pattern: Component Testing\n\n```typescript\nimport { render, fireEvent, waitFor } from '@testing-library\u002Freact-native';\n\ndescribe('SessionCard', () => {\n  const mockSession = {\n    id: '1',\n    title: 'Serve Practice',\n    totalDuration: 45, \u002F\u002F Backend-calculated\n  };\n\n  it('displays session data from backend', () => {\n    const { getByText } = render(\u003CSessionCard session={mockSession} \u002F>);\n    \n    expect(getByText('Serve Practice')).toBeTruthy();\n    expect(getByText('45 min')).toBeTruthy();\n  });\n\n  it('calls onPress when tapped', () => {\n    const onPress = jest.fn();\n    const { getByTestId } = render(\n      \u003CSessionCard session={mockSession} onPress={onPress} \u002F>\n    );\n    \n    fireEvent.press(getByTestId('session-card'));\n    \n    expect(onPress).toHaveBeenCalledWith(mockSession.id);\n  });\n});\n```\n\n---\n\n## Pattern: Navigation Testing\n\n```typescript\nimport { useRouter } from 'expo-router';\n\njest.mock('expo-router');\n\ndescribe('SettingsScreen', () => {\n  const mockPush = jest.fn();\n  \n  beforeEach(() => {\n    jest.clearAllMocks();\n    (useRouter as jest.Mock).mockReturnValue({\n      push: mockPush,\n      back: jest.fn(),\n    });\n  });\n\n  it('navigates to profile on button press', () => {\n    const { getByText } = render(\u003CSettingsScreen \u002F>);\n    \n    fireEvent.press(getByText('Edit Profile'));\n    \n    expect(mockPush).toHaveBeenCalledWith('\u002Fprofile\u002Fedit');\n  });\n});\n\n\u002F\u002F Testing with route params\njest.mock('expo-router', () => ({\n  useLocalSearchParams: () => ({ id: 'test-assessment-123' }),\n  useRouter: () => ({ push: jest.fn() }),\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  const { getByText } = render(\u003CDataComponent \u002F>);\n  \n  \u002F\u002F Wait for loading to complete\n  await waitFor(() => {\n    expect(getByText('Data loaded')).toBeTruthy();\n  });\n});\n\n\u002F\u002F CORRECT - use findBy* (has built-in waitFor)\nit('loads data', async () => {\n  const { findByText } = render(\u003CDataComponent \u002F>);\n  \n  const element = await findByText('Data loaded');\n  expect(element).toBeTruthy();\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 tree = render(\u003CButton title=\"Submit\" \u002F>);\n  expect(tree.toJSON()).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: Mocking API Calls\n\n```typescript\n\u002F\u002F Mock Orval-generated hooks\njest.mock('@\u002Fapi\u002Fgenerated\u002Fassessments', () => ({\n  useGetAssessment: jest.fn(() => ({\n    data: mockAssessment,\n    isLoading: false,\n    error: null,\n  })),\n  useSubmitAssessment: jest.fn(() => ({\n    mutate: jest.fn(),\n    isLoading: false,\n  })),\n}));\n\n\u002F\u002F Mock with different states\nimport { useGetAssessment } from '@\u002Fapi\u002Fgenerated\u002Fassessments';\n\nit('shows loading state', () => {\n  (useGetAssessment as jest.Mock).mockReturnValue({\n    data: null,\n    isLoading: true,\n    error: null,\n  });\n  \n  const { getByTestId } = render(\u003CAssessmentScreen \u002F>);\n  expect(getByTestId('loading-spinner')).toBeTruthy();\n});\n\nit('shows error state', () => {\n  (useGetAssessment as jest.Mock).mockReturnValue({\n    data: null,\n    isLoading: false,\n    error: new Error('Failed to load'),\n  });\n  \n  const { getByText } = render(\u003CAssessmentScreen \u002F>);\n  expect(getByText('Failed to load')).toBeTruthy();\n});\n```\n\n---\n\n## Recommended Test Utilities\n\n```\n__tests__\u002Futils\u002Freact-query-test-utils.tsx  # QueryClient wrapper\njest.setup.js                                # Global mocks\n```\n\n### Test Commands\n```bash\nnpm test                      # Run all tests\nnpm test -- --watch           # Watch mode\nnpm test -- --coverage        # Coverage report\nnpm test -- SessionCard       # Run specific test file\nnpm test -- --updateSnapshot  # Update snapshots\n```\n\n---\n\n## Common Issues\n\n| Issue | Solution |\n|-------|----------|\n| \"Cannot find module\" | Check jest.setup.js module mappings |\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\n---\n\n## Relationship to Other Skills\n\n- **rn-async-patterns**: Use post-condition validation in tests to verify async flows\n- **rn-zustand-patterns**: Understand `getState()` behavior for store tests\n- **rn-state-flows**: Integration tests should cover entire flows, not just units\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Faiskillstore\u002Fmarketplace\u002Fcjharmath\u002Frn-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 rn-testing",{"tool":67,"command":68},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill rn-testing",{"tool":70,"command":71},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill rn-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fcjharmath\u002Frn-testing\u002FSKILL.md",true,[75,81,85,89,93,97,103,107,111,117,121,125,129],{"path":76,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":79,"sourceHtmlUrl":80},"assets\u002Ftemplates\u002Farchitecture.drawio","ASSET","资产文件",8077,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fassets\u002Ftemplates\u002Farchitecture.drawio",{"path":82,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":83,"sourceHtmlUrl":84},"assets\u002Ftemplates\u002Fer-diagram.drawio",26593,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fassets\u002Ftemplates\u002Fer-diagram.drawio",{"path":86,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":87,"sourceHtmlUrl":88},"assets\u002Ftemplates\u002Fflowchart.drawio",4590,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fassets\u002Ftemplates\u002Fflowchart.drawio",{"path":90,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":91,"sourceHtmlUrl":92},"assets\u002Ftemplates\u002Fsequence.drawio",7290,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fassets\u002Ftemplates\u002Fsequence.drawio",{"path":94,"resourceType":77,"resourceTypeDesc":78,"sizeBytes":95,"sourceHtmlUrl":96},"assets\u002Ftemplates\u002Fuml-class.drawio",12960,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fassets\u002Ftemplates\u002Fuml-class.drawio",{"path":98,"resourceType":99,"resourceTypeDesc":100,"sizeBytes":101,"sourceHtmlUrl":102},"references\u002Fdrawio-xml-schema.md","REFERENCE","参考文档",10796,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Freferences\u002Fdrawio-xml-schema.md",{"path":104,"resourceType":99,"resourceTypeDesc":100,"sizeBytes":105,"sourceHtmlUrl":106},"references\u002Fshape-libraries.md",13126,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Freferences\u002Fshape-libraries.md",{"path":108,"resourceType":99,"resourceTypeDesc":100,"sizeBytes":109,"sourceHtmlUrl":110},"references\u002Fstyle-reference.md",15491,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Freferences\u002Fstyle-reference.md",{"path":112,"resourceType":113,"resourceTypeDesc":114,"sizeBytes":115,"sourceHtmlUrl":116},"scripts\u002F.gitignore","SCRIPT","脚本",39,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fscripts\u002F.gitignore",{"path":118,"resourceType":113,"resourceTypeDesc":114,"sizeBytes":119,"sourceHtmlUrl":120},"scripts\u002Fadd-shape.py",6642,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fscripts\u002Fadd-shape.py",{"path":122,"resourceType":113,"resourceTypeDesc":114,"sizeBytes":123,"sourceHtmlUrl":124},"scripts\u002FREADME.md",3332,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fscripts\u002FREADME.md",{"path":126,"resourceType":113,"resourceTypeDesc":114,"sizeBytes":127,"sourceHtmlUrl":128},"scripts\u002Fvalidate-drawio.py",7643,"https:\u002F\u002Fgithub.com\u002FTorchiaHub\u002FSkills\u002Fblob\u002Fmaster\u002F01-Inbox\u002Fdownloads\u002Fgithub-awesome-copilot\u002F20260429T123413Z\u002Fsource\u002Fskills\u002Fdraw-io-diagram-generator\u002Fscripts\u002Fvalidate-drawio.py",{"path":130,"resourceType":99,"resourceTypeDesc":100,"sizeBytes":131,"sourceHtmlUrl":132},"skill-report.json",11800,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fcjharmath\u002Frn-testing\u002Fskill-report.json",[134,146,156,165,171,178],{"skillNo":135,"name":136,"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":137,"categories":138,"keywords":142,"publishedAt":47},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[139],{"categoryNo":140,"categoryName":141,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[143,144,145],"model","model hierarchy","model hierarchy skill",{"skillNo":147,"name":148,"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":149,"categories":150,"keywords":152,"publishedAt":47},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[151],{"categoryNo":140,"categoryName":141,"taxonomyType":21,"taxonomyTypeDesc":22},[153,154,155],"MCP","zoom","zoom mcp",{"skillNo":157,"name":158,"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":159,"categories":160,"keywords":161,"publishedAt":47},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[162,163,164,154],"sdk","zoom apps sdk","apps",{"skillNo":166,"name":167,"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":168,"categories":169,"keywords":170,"publishedAt":47},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[167],{"skillNo":172,"name":173,"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":174,"categories":175,"keywords":177,"publishedAt":47},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[176],{"categoryNo":140,"categoryName":141,"taxonomyType":21,"taxonomyTypeDesc":22},[173],{"skillNo":179,"name":180,"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":181,"categories":182,"keywords":183,"publishedAt":47},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[180]]