Agent Skills › getsentry/sentry › react-testing

react-testing

GitHub

提供Sentry前端React/TypeScript测试规范,指导使用Jest和RTL编写用户行为导向的测试,强调避免实现细节暴露及Mock策略。

.agents/skills/react-testing/SKILL.md getsentry/sentry

Trigger Scenarios

添加或编辑前端单元测试 修复不稳定的RTL测试 编写组件或Hook测试 模拟API响应

Install

npx skills add getsentry/sentry --skill react-testing -g -y
More Options

Non-standard path

npx skills add https://github.com/getsentry/sentry/tree/master/.agents/skills/react-testing -g -y

Use without installing

npx skills use getsentry/sentry@react-testing

指定 Agent (Claude Code)

npx skills add getsentry/sentry --skill react-testing -a claude-code -g -y

安装 repo 全部 skill

npx skills add getsentry/sentry --all -g -y

预览 repo 内 skill

npx skills add getsentry/sentry --list

SKILL.md

Frontmatter
{
    "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:

import {
  render,
  screen,
  userEvent,
  waitFor,
  within,
} from 'sentry-test/reactTestingLibrary';

Query Priority (in order of preference)

  1. getByRole - Primary selector for most elements

    screen.getByRole('button', {name: 'Save'});
    screen.getByRole('textbox', {name: 'Search'});
    
  2. getByLabelText/getByPlaceholderText - For form elements

    screen.getByLabelText('Email Address');
    screen.getByPlaceholderText('Enter Search Term');
    
  3. getByText - For non-interactive elements

    screen.getByText('Error Message');
    
  4. getByTestId - Last resort only

    screen.getByTestId('custom-component');
    

Best Practices

Avoid mocking hooks, functions, or components

Do not use jest.mocked().

// ❌ 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/.


// ❌ 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.

// ❌ 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

// ❌ 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
// ❌ Wrong
expect(screen.queryByRole('alert')).toBeInTheDocument();

// ✅ Correct
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument();

Async testing

// ❌ 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.

// ❌ 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

// ❌ Don't use fireEvent
fireEvent.change(input, {target: {value: 'text'}});

// ✅ Use userEvent
await userEvent.click(input);
await userEvent.keyboard('text');

Testing routing

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:

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

// 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:

// ❌ 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:

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();
});

Version History

  • 2f300b6 Current 2026-09-23 11:33

    新增禁止使用自定义渲染辅助函数的约定,指导在测试中直接使用render而非封装helper。

  • d3c9056 2026-08-20 20:33

Same Skill Collection

.agents/skills/bump-sentry-dependency/SKILL.md
.agents/skills/cmdk-actions/SKILL.md
.agents/skills/design-system/SKILL.md
.agents/skills/feature-flags/SKILL.md
.agents/skills/frontend-data-fetching/SKILL.md
.agents/skills/generate-frontend-forms/SKILL.md
.agents/skills/generate-migration/SKILL.md
.agents/skills/generate-snapshot-tests/SKILL.md
.agents/skills/hybrid-cloud-rpc/SKILL.md
.agents/skills/hybrid-cloud-test-gen/SKILL.md
.agents/skills/lint-fix/SKILL.md
.agents/skills/lint-new/SKILL.md
.agents/skills/migrate-container-queries/SKILL.md
.agents/skills/migrate-frontend-forms/SKILL.md
.agents/skills/notification-platform/SKILL.md
.agents/skills/react-component-documentation/SKILL.md
.agents/skills/scraps-review/SKILL.md
.agents/skills/seer-embed/SKILL.md
.agents/skills/sentry-backend-bugs/SKILL.md
.agents/skills/sentry-javascript-bugs/SKILL.md
.agents/skills/sentry-security/SKILL.md
.agents/skills/analytics/SKILL.md
.agents/skills/backend-conventions/SKILL.md
.agents/skills/cell-architecture/SKILL.md
.agents/skills/django-models/SKILL.md
.agents/skills/hybrid-cloud-outboxes/SKILL.md
.agents/skills/migrate-breadcrumb-list/SKILL.md
.agents/skills/remove-option-or-flag/SKILL.md
.agents/skills/setup-dev/SKILL.md

Metadata

Files
0
Version
991ee88
Hash
e8419de0
Indexed
2026-08-20 20:33

inicio - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-03 17:41
浙ICP备14020137号-1