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".
Installs into .claude/skills of the current project.
Are you the author of React Testing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/getsentry-react-testing)
---
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();
});
```