Skip to main content

react-testing

Write and review React/TypeScript tests for Sentry's frontend using Jest and React Testing Library. Use when adding or editing tests in static/ (*.spec.tsx), writing component/hook tests, mocking API responses with MockApiClient, testing routing or network requests, or when asked to "write a frontend test", "add a React test", "test this component", or "fix a flaky RTL test".

소스 정보

저장소
getsentry/sentry
최근 소스 활동
2026년 9월 22일 20:52
감지된 SKILL.md 언어
영어
스타
45,528
포크
4,915

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
react-testing
description
Write and review React/TypeScript tests for Sentry's frontend using Jest and React Testing Library. Use when adding or editing tests in static/ (*.spec.tsx), writing component/hook tests, mocking API responses with MockApiClient, testing routing or network requests, or when asked to "write a frontend test", "add a React test", "test this component", or "fix a flaky RTL test".
# React Testing Guidelines ## Testing Philosophy - **User-centric testing**: Write tests that resemble how users interact with the app. - **Avoid implementation details**: Focus on behavior, not internal component structure. - **Do not share state between tests**: Behavior should not be influenced by other tests in the test suite. ## Imports **Always** import from `sentry-test/reactTestingLibrary`, not directly from `@testing-library/react`: ```tsx import { render, screen, userEvent, waitFor, within, } from 'sentry-test/reactTestingLibrary'; ``` ## Query Priority (in order of preference) 1. **`getByRole`** - Primary selector for most elements ```tsx screen.getByRole('button', {name: 'Save'}); screen.getByRole('textbox', {name: 'Search'}); ``` 2. **`getByLabelText`/`getByPlaceholderText`** - For form elements ```tsx screen.getByLabelText('Email Address'); screen.getByPlaceholderText('Enter Search Term'); ``` 3. **`getByText`** - For non-interactive elements ```tsx screen.getByText('Error Message'); ``` 4. **`getByTestId`** - Last resort only ```tsx screen.getByTestId('custom-component'); ``` ## Best Practices ### Avoid mocking hooks, functions, or components Do not use `jest.mocked()`. ```tsx // ❌ Don't mock hooks jest.mocked(useDataFetchingHook) // ✅ Set the response data MockApiClient.addMockResponse({ url: '/data/', body: DataFixture(), }) // ❌ Don't mock contexts jest.mocked(useOrganization) // ✅ Use the provided organization config on render() render(<Component />, {organization: OrganizationFixture({...})}) // ❌ Don't mock router hooks jest.mocked(useLocation) // ✅ Use the provided router config render(<TestComponent />, { initialRouterConfig: { location: { pathname: "/foo/", }, }, }); // ❌ Don't mock page filters hook jest.mocked(usePageFilters) // ✅ Update the corresponding data store with your data PageFiltersStore.onInitializeUrlState( PageFiltersFixture({ projects: [1]}), ) // ❌ Don't recreate the basic context providers renderHook(useNavigate, { wrapper: (children) => (<AllTheProviders>{children}</AllTheProviders>), }) // ✅ Use the provided helpers that mock everything renderHookWithProviders(useNavigate) ``` ### Use fixtures Sentry fixtures are located in tests/js/fixtures/ while GetSentry fixtures are located in tests/js/getsentry-test/fixtures/. ```tsx // ❌ Don't import type and initialize it import type {Project} from 'sentry/types/project'; const project: Project = {...} // ✅ Import a fixture instead import {ProjectFixture} from 'sentry-fixture/project'; const project = ProjectFixture(partialProject) ``` ### Render a component, not a `renderFoo()` helper A local helper hides the JSX from every test that calls it, and `rerender` takes an element, so the call site can no longer control what renders. ```tsx // ❌ Don't wrap render() in a helper function renderComponent(props: Props = {}) { return render(<Widget {...props}>hello</Widget>); } // ✅ Put the fixed parts in a component function ExampleWidget(props: Props) { return <Widget {...props}>hello</Widget>; } render(<ExampleWidget isLoading />); ``` Declare it at module scope so `rerender` keeps the same component type, and keep `render()` options at the call site. If the helper adds nothing over the component's own props, drop it and call `render(<Widget />)` directly. Helpers that only register mocks, or that take the element as a parameter, are fine. ### Use `screen` instead of destructuring ```tsx // ❌ Don't do this const {getByRole} = render(<Component />); // ✅ Do this render(<Component />); const button = screen.getByRole('button'); ``` ### Query selection guidelines - Use `getBy...` for elements that should exist - Use `queryBy...` ONLY when checking for non-existence - Use `await findBy...` when waiting for elements to appear ```tsx // ❌ Wrong expect(screen.queryByRole('alert')).toBeInTheDocument(); // ✅ Correct expect(screen.getByRole('alert')).toBeInTheDocument(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); ``` ### Async testing ```tsx // ❌ Don't use waitFor for appearance await waitFor(() => { expect(screen.getByRole('alert')).toBeInTheDocument(); }); // ✅ Use findBy for appearance expect(await screen.findByRole('alert')).toBeInTheDocument(); // ✅ Use waitForElementToBeRemoved for disappearance await waitForElementToBeRemoved(() => screen.getByRole('alert')); ``` ### Avoid waiting for loading indicators Do not use `findBy` with `.not.toBeInTheDocument()` for loading indicators. `findBy` will error if the element is not found, but we're asserting it should NOT exist. Loading indicators are also flakey since they appear on screen for only a few ticks. ```tsx // ❌ Wrong - findBy errors if element not found, and loading indicators are flakey expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); // ✅ Correct - wait for the actual content you care about await waitFor(() => { expect(screen.getByRole('button', {name: 'Submit'})).toBeInTheDocument(); }); // ✅ Also correct - use findBy on the content that appears after loading expect(await screen.findByRole('button', {name: 'Submit'})).toBeInTheDocument(); ``` ### User interactions ```tsx // ❌ Don't use fireEvent fireEvent.change(input, {target: {value: 'text'}}); // ✅ Use userEvent await userEvent.click(input); await userEvent.keyboard('text'); ``` ### Testing routing ```tsx const {router} = render(<TestComponent />, { initialRouterConfig: { location: { pathname: '/foo/', query: {page: '1'}, }, }, }); // Uses passes in config to set initial location expect(router.location.pathname).toBe('/foo'); expect(router.location.query.page).toBe('1'); // Clicking links goes to the correct location await userEvent.click(screen.getByRole('link', {name: 'Go to /bar/'})); // Can check current route on the returned router expect(router.location.pathname).toBe('/bar/'); // Can test manual route changes with router.navigate router.navigate('/new/path/'); router.navigate(-1); // Simulates clicking the back button ``` If the component uses `useParams()`, the `route` property can be used: ```tsx function TestComponent() { const {id} = useParams(); return <div>{id}</div>; } const {router} = render(<TestComponent />, { initialRouterConfig: { location: { pathname: '/foo/123/', }, route: '/foo/:id/', }, }); expect(screen.getByText('123')).toBeInTheDocument(); ``` ### Testing components that make network requests ```tsx // Simple GET request MockApiClient.addMockResponse({ url: '/projects/', body: [{id: 1, name: 'my project'}], }); // POST request MockApiClient.addMockResponse({ url: '/projects/', method: 'POST', body: {id: 1, name: 'my project'}, }); // Complex matching with query params and request body MockApiClient.addMockResponse({ url: '/projects/', method: 'POST', body: {id: 2, name: 'other'}, match: [ MockApiClient.matchQuery({param: '1'}), MockApiClient.matchData({name: 'other'}), ], }); // Error responses MockApiClient.addMockResponse({ url: '/projects/', body: { detail: 'Internal Error', }, statusCode: 500, }); ``` #### Always Await Async Assertions Network requests are asynchronous. Always use `findBy` queries or properly await assertions: ```tsx // ❌ Wrong - will fail intermittently expect(screen.getByText('Loaded Data')).toBeInTheDocument(); // ✅ Correct - waits for element to appear expect(await screen.findByText('Loaded Data')).toBeInTheDocument(); ``` #### Handle Refetches in Mutations When testing mutations that trigger data refetches, update mocks before the refetch occurs: ```tsx it('adds item and updates list', async () => { // Initial empty state MockApiClient.addMockResponse({ url: '/items/', body: [], }); const createRequest = MockApiClient.addMockResponse({ url: '/items/', method: 'POST', body: {id: 1, name: 'New Item'}, }); render(<ItemList />); await userEvent.click(screen.getByRole('button', {name: 'Add Item'})); // CRITICAL: Override mock before refetch happens MockApiClient.addMockResponse({ url: '/items/', body: [{id: 1, name: 'New Item'}], }); await waitFor(() => expect(createRequest).toHaveBeenCalled()); expect(await screen.findByText('New Item')).toBeInTheDocument(); }); ```
GitHub에서 보기