Test IDs - Web
Test identifier patterns for web applications using data-testid attribute and @testing-library/react.
Documentation
This skill has comprehensive documentation:
- •Production Examples - Real-world code examples from the codebase
- •API Reference - Complete API documentation with official links
- •Implementation Patterns - Best practices and anti-patterns
When to Use
Use test IDs for:
- •E2E testing with tools like Playwright, Cypress
- •Component testing with @testing-library/react
- •Targeting specific elements in tests
- •Stable selectors that don't break with UI changes
Don't use test IDs for:
- •Production analytics (use dedicated tracking attributes)
- •Styling or behavior hooks (use CSS classes or data attributes)
Core Principles
1. data-testid Attribute
Use data-testid (or data-test-id) attribute for test identifiers.
✅ Good:
// app/unified-spaces/plans-sections/single-question-flow/components/steps/rte-number-of-people-step/NumberOfPeopleStep.tsx:119
<Box
as="button"
role="checkbox"
data-testid={`goalsPlanNumberOfPeople-${numberOfPeopleOption}`}
onClick={() => setSelectedNumberOfPeople(numberOfPeopleOption)}
>
{/* Content */}
</Box>
Container with base ID:
// app/unified-spaces/plans-sections/single-question-flow/components/steps/rte-number-of-people-step/NumberOfPeopleStep.tsx:102
<Box
data-test-id={`goalsPlanNumberOfPeople`}
role="group"
as="section"
>
{/* Child elements */}
</Box>
❌ Bad - Don't use testID (React Native syntax):
// This is React Native - don't use in web! <View testID="my-component">
Why: data-testid is the HTML5 data attribute standard. React Testing Library uses this by default with getByTestId().
Note: Both data-testid and data-test-id are used in the codebase, but data-testid (no hyphen) is the React Testing Library convention.
2. Naming Conventions
Use kebab-case with descriptive, context-aware names.
✅ Good:
// Contextual naming <Box data-testid="goalsPlanNumberOfPeople-justMe"> // Hierarchical with dots <Button data-test-id="upm-playground.btn.submit"> // Semantic descriptive names <circle data-test-id="radial-bg-circle"> <circle data-test-id="radial-progress-circle"> // Component-specific with state <Box data-test-id="challenge-wrapper-card"> <Box data-test-id="card-content"> <Box data-test-id="title-container">
Naming patterns:
// Context + entity data-testid="goalsPlanNumberOfPeople" // Context + entity + variant data-testid="goalsPlanNumberOfPeople-justMe" data-testid="goalsPlanNumberOfPeople-twoOfUs" // Module + element + action data-test-id="upm-playground.btn.submit" // Feature + component + part data-test-id="challenge-card-cta" data-test-id="challenge-title" data-test-id="circular-progress-text" // Descriptive semantic names data-test-id="progress-checkmark" data-test-id="feedback-modal-heading" data-test-id="radial-bg-circle"
Why: Descriptive names make tests readable and maintainable. Context prevents ID collisions.
3. Dynamic Test IDs
Use template literals for dynamic test IDs with variables.
✅ Good:
// app/unified-spaces/plans-sections/single-question-flow/components/steps/rte-number-of-people-step/NumberOfPeopleStep.tsx:119
{numberOfPeopleOptions.map((numberOfPeopleOption, index) => (
<Box
key={index}
data-testid={`goalsPlanNumberOfPeople-${numberOfPeopleOption}`}
>
{/* Content */}
</Box>
))}
Indexed test IDs:
// app/features/freebie-in-helloshare-challenge-card-feature/components/CircularProgressIndicator/index.tsx:54
{stepsArray.map((_, index) => (
<StepCircleDivider
key={index}
data-test-id={`step-circle-divider-${index}`}
/>
))}
Conditional suffix:
// app/features/freebie-in-helloshare-challenge-card-feature/components/FIHCardTasks.tsx:46
<Box
data-test-id={`step-container-step-${stepIndex}${
isCompleted ? '-completed' : ''
}`}
>
Why: Dynamic IDs allow testing of generated lists and conditional states.
4. Testing with @testing-library/react
Query elements by test ID and assert with toHaveAttribute.
✅ Good:
// app/unified-spaces/plans-sections/single-question-flow/components/steps/rte-number-of-people-step/NumberOfPeopleStep.spec.tsx:68
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
it('renders options with correct test ids', async () => {
renderComponent();
let options = await screen.findAllByRole('checkbox');
expect(options.length).toBe(3);
expect(options[0]).toHaveAttribute(
'data-testid',
'goalsPlanNumberOfPeople-justMe'
);
expect(options[1]).toHaveAttribute(
'data-testid',
'goalsPlanNumberOfPeople-twoOfUs'
);
expect(options[2]).toHaveAttribute(
'data-testid',
'goalsPlanNumberOfPeople-groupFamily'
);
await act(async () => {
userEvent.click(options[2]);
});
options = await screen.findAllByRole('checkbox');
expect(options[2]).toHaveAttribute('aria-checked', 'true');
});
Why: Combining role queries with test ID assertions provides accessible, stable tests.
5. Integration Test Pattern
Wrap components in providers for integration tests.
✅ Good:
// app/data-access/CONTRIBUTING.md:274
import { render } from '@testing-library/react';
import IntegrationTestProvider from '../test-utils/IntegrationTestProvider';
const TestComponent: React.FC = () => {
const { data } = useCustomerInfo({});
return <div data-test-id="email">{data?.email}</div>;
};
render(
<IntegrationTestProvider>
<TestComponent />
</IntegrationTestProvider>
);
With multiple providers:
// app/unified-spaces/plans-sections/single-question-flow/components/steps/rte-number-of-people-step/NumberOfPeopleStep.spec.tsx:26
const renderComponent = () =>
render(
<QueryClientProvider client={new QueryClient()}>
<ServerEnvProvider>
<SystemCountryProvider systemCountry={SystemCountry.FJ}>
<LocalStorageProvider>
<SingleQuestionPageProvider>
<QuestionnaireConfigProvider>
<NumberOfPeopleProvider>
<NumberOfPeopleStep
onGoalsPlanRecommendationSelectionChange={onSelectionChange}
/>
</NumberOfPeopleProvider>
</QuestionnaireConfigProvider>
</SingleQuestionPageProvider>
</LocalStorageProvider>
</SystemCountryProvider>
</ServerEnvProvider>
</QueryClientProvider>
);
Why: Providers supply necessary context (React Query, state, config) for components to function in tests.
Advanced Patterns
Hierarchical Test IDs
// Dot notation for hierarchy
<Box data-test-id="checkout-page.footer.submit-button">
<Box data-test-id="checkout-page.footer.error-message">
// Nested context
<Box data-test-id="challenge-card">
<Box data-test-id="challenge-card-header">
<Text data-test-id="challenge-card-title">
</Box>
<Box data-test-id="challenge-card-content">
</Box>
State-Based Test IDs
// Conditional state in ID
<Box
data-testid={`card-${isActive ? 'active' : 'inactive'}`}
>
// Status suffix
<Box data-testid={`step-${index}-${status}`}>
SVG and Icon Test IDs
// app/features/freebie-in-helloshare-challenge-card-feature/components/CircularProgressIndicator/index.tsx:38
<Svg viewBox={`0 0 ${externalRadius * 2} ${externalRadius * 2}`}>
<Background
data-test-id="radial-bg-circle"
/>
<Progress
data-test-id="radial-progress-circle"
/>
</Svg>
// Icon elements
<CheckmarkOutline16 data-test-id="progress-checkmark" />
<BoxClosedOutline24 data-test-id="progress-box" />
Form Element Test IDs
// Input fields <input data-testid="email-input" type="email" /> <input data-testid="password-input" type="password" /> // Buttons with action context <button data-test-id="submit-button"> <button data-test-id="cancel-button"> // Form containers <form data-testid="login-form">
Testing Patterns
Query by Role + Assert Test ID
// Find by semantic role, verify by test ID
const buttons = await screen.findAllByRole('button');
expect(buttons[0]).toHaveAttribute('data-testid', 'primary-action');
Direct Query by Test ID
// When role is not clear or multiple roles exist
const element = screen.getByTestId('custom-complex-component');
expect(element).toBeInTheDocument();
User Interactions
import userEvent from '@testing-library/user-event';
// Click interaction
const button = screen.getByTestId('submit-button');
await act(async () => {
userEvent.click(button);
});
// Type interaction
const input = screen.getByTestId('email-input');
await userEvent.type(input, 'test@example.com');
Async Queries
// Wait for element to appear
const element = await screen.findByTestId('async-content');
// With custom timeout
const slowElement = await screen.findByTestId('slow-content', {}, {
timeout: 5000,
});
File Organization
components/
└── MyComponent/
├── MyComponent.tsx # Component with test IDs
├── MyComponent.spec.tsx # Unit tests
├── MyComponent.test.tsx # Integration tests
└── index.ts # Exports
Common Mistakes
- •Using testID instead of data-testid - testID is React Native syntax
- •Hardcoding test IDs without context - Use descriptive, prefixed names
- •Not using template literals - Dynamic IDs need interpolation
- •Inconsistent naming - Stick to kebab-case convention
- •Over-specific IDs - Balance between specific and maintainable
- •Missing test IDs in key interaction points - Add IDs to all testable elements
- •Using data-test-id and data-testid inconsistently - Prefer
data-testidfor React Testing Library
Quick Reference
Basic Test ID
import { Box } from '@/libs/zest';
<Box data-testid="my-component">
{/* Content */}
</Box>
Dynamic Test ID
<Box data-testid={`option-${index}`}>
{/* Content */}
</Box>
<Box data-testid={`card-${id}-${status}`}>
{/* Content */}
</Box>
Testing Pattern
import { render, screen } from '@testing-library/react';
it('renders component', () => {
render(<MyComponent />);
const element = screen.getByTestId('my-component');
expect(element).toBeInTheDocument();
expect(element).toHaveAttribute('data-testid', 'my-component');
});
With User Interaction
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
it('handles click', async () => {
render(<MyComponent />);
const button = screen.getByTestId('submit-button');
await act(async () => {
userEvent.click(button);
});
expect(mockHandler).toHaveBeenCalled();
});
Multiple Providers
const renderWithProviders = (component) =>
render(
<QueryClientProvider client={queryClient}>
<SystemCountryProvider systemCountry={SystemCountry.US}>
{component}
</SystemCountryProvider>
</QueryClientProvider>
);
it('renders with context', () => {
renderWithProviders(<MyComponent />);
expect(screen.getByTestId('my-component')).toBeInTheDocument();
});
Naming Guidelines
Good Names
- •
goalsPlanNumberOfPeople-justMe- Context + entity + variant - •
upm-playground.btn.submit- Module + element + action - •
challenge-card-cta- Feature + component + part - •
radial-progress-circle- Descriptive semantic name - •
step-container-step-2-completed- State + index + status
Bad Names
- •
btn- Too generic - •
test- Not descriptive - •
component1- Meaningless - •
the-button- Unclear purpose - •
MyComponent- Not kebab-case
React Testing Library Standard
Preferred: data-testid (no hyphen between test and id)
// ✅ Preferred - React Testing Library standard <Box data-testid="my-component"> // ⚠️ Also works but less conventional <Box data-test-id="my-component"> // ❌ Wrong - React Native only <View testID="my-component">
Why: React Testing Library's getByTestId() looks for data-testid by default. Using the standard convention ensures consistency across the codebase.