一键导入
test-front-component
Creates unit tests for React frontend components using Vitest and React Testing Library, following accessibility-first best practices.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Creates unit tests for React frontend components using Vitest and React Testing Library, following accessibility-first best practices.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Extracts methods from the monolithic RoseauGateway/RoseauRepository into a dedicated gateway/repository pair within the same folder. Use this when moving domain-specific methods out of roseau.gateway into their own gateway.
Creates a full-stack paginated table page with filters, sorting, and detail view — from SQL to React — using MasaProvider.
Creates new functionalities that fetch reference data via MasaProvider instead of directly using LanceleauGateway or RoseauGateway.
Creates unit and e2e tests for NestJS applications using Jest and Supertest. Includes patterns for testing services, controllers, guards, and e2e tests with testcontainers.
Creates a typed API endpoint across shared route definition, NestJS controller, frontend service, and Query hook.
Develops, styles, and tests DSFR-compliant React 19 components.
| name | test-front-component |
| description | Creates unit tests for React frontend components using Vitest and React Testing Library, following accessibility-first best practices. |
This skill provides instructions and best practices for testing React components in the Verseau frontend using Vitest and React Testing Library.
@testing-library/react@testing-library/user-event (preferred) or fireEvent@testing-library/jest-dom/vitestStructure tests using Arrange-Act-Assert:
it('loads and displays greeting', async () => {
// ARRANGE - setup the component
const user = userEvent.setup();
render(<MyComponent prop="value" />);
// ACT - perform user interactions
await user.click(screen.getByRole('button', { name: /submit/i }));
// ASSERT - verify the expected outcome
expect(screen.getByRole('heading')).toHaveTextContent('Success');
});
Use queries in this priority order to write accessible tests:
getByRole - Top preference for most elements (buttons, headings, links, etc.)
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByRole('link', { name: /learn more/i });
getByLabelText - Preferred for form fields
screen.getByLabelText('Email');
screen.getByLabelText(/mot de passe/i);
getByPlaceholderText - Only if no label exists
screen.getByPlaceholderText('Search...');
getByText - For non-interactive elements (divs, spans, paragraphs)
screen.getByText(/bienvenue/i);
screen.getByText('Erreur détectée');
getByDisplayValue - For form elements with filled-in values
screen.getByDisplayValue('current value');
getByAltText - For images
screen.getByAltText('Logo de l\'application');
getByTestId - Last resort when other queries aren't possible
screen.getByTestId('custom-element');
| Query Type | No Match | 1 Match | >1 Match | Async |
|---|---|---|---|---|
getBy... | Throws | Returns | Throws | No |
queryBy... | Returns null | Returns | Throws | No |
findBy... | Throws | Returns | Throws | Yes |
getAllBy... | Throws | Returns [] | Returns [] | No |
queryAllBy... | Returns [] | Returns [] | Returns [] | No |
findAllBy... | Throws | Returns [] | Returns [] | Yes |
Usage Guidelines:
getBy* for elements that should be present immediatelyqueryBy* to assert an element is NOT present: expect(screen.queryByText('Error')).not.toBeInTheDocument()findBy* for elements that appear asynchronously (returns Promise)*All* variants when multiple elements are expectedPrefer userEvent over fireEvent:
userEvent simulates realistic user behavior with proper event sequences, whereas fireEvent only dispatches single DOM events.
import userEvent from '@testing-library/user-event';
it('should handle user input', async () => {
const user = userEvent.setup();
render(<Form />);
// Type in an input field
await user.type(screen.getByLabelText('Email'), 'test@example.com');
// Click a button
await user.click(screen.getByRole('button', { name: /submit/i }));
// Select from dropdown
await user.selectOptions(screen.getByRole('combobox'), 'option1');
// Keyboard interactions
await user.keyboard('{Enter}');
});
Use fireEvent only when:
userEvent doesn't support the specific interactionimport { fireEvent } from '@testing-library/react';
// fireEvent for specific cases
fireEvent.scroll(container, { target: { scrollY: 100 } });
withinUse within to query within a specific container:
import { render, screen, within } from '@testing-library/react';
it('should find elements within container', () => {
render(<DataTable />);
const row = screen.getByRole('row', { name: /john doe/i });
const editButton = within(row).getByRole('button', { name: /edit/i });
expect(editButton).toBeInTheDocument();
});
Create reusable helper functions for common queries or actions:
// Extract common queries as helper functions at the top of the file
const getStatCard = (label: string) => {
const labelElement = screen.getByText(label);
const container = labelElement.closest('.fr-col') as HTMLElement;
expect(container).toBeInTheDocument();
return container;
};
const clickButton = (name: string | RegExp) => {
const button = screen.getByRole('button', { name });
fireEvent.click(button);
};
const expectStatCardCount = (label: string, count: string) => {
const statCard = getStatCard(label);
const countElement = within(statCard).getByText(count);
expect(countElement).toHaveClass('fr-h2', 'fr-mb-0');
return countElement;
};
MyComponent.spec.tsxapps/front/src/components MUST have a corresponding .spec.tsx filedescribe blocks to group related testsit for individual test cases with descriptive namesimport { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MyComponent } from './MyComponent';
// Helper functions at the top
const getSubmitButton = () => screen.getByRole('button', { name: /submit/i });
describe('MyComponent', () => {
// Test fixture data
const defaultProps = {
title: 'Test Title',
onSubmit: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it('renders null when no data is provided', () => {
const { container } = render(<MyComponent {...defaultProps} data={[]} />);
expect(container.firstChild).toBeNull();
});
it('renders the title and main elements', () => {
render(<MyComponent {...defaultProps} data={['item1']} />);
expect(screen.getByRole('heading', { name: /test title/i })).toBeInTheDocument();
expect(getSubmitButton()).toBeEnabled();
});
it('calls onSubmit when form is submitted', async () => {
const user = userEvent.setup();
render(<MyComponent {...defaultProps} data={['item1']} />);
await user.click(getSubmitButton());
expect(defaultProps.onSubmit).toHaveBeenCalledTimes(1);
});
it('disables submit button during loading', () => {
render(<MyComponent {...defaultProps} data={['item1']} isLoading />);
expect(getSubmitButton()).toBeDisabled();
});
it('shows error message when validation fails', async () => {
const user = userEvent.setup();
render(<MyComponent {...defaultProps} data={['item1']} />);
// Leave required field empty and submit
await user.click(getSubmitButton());
expect(screen.getByRole('alert')).toHaveTextContent(/champ requis/i);
});
describe('with async data', () => {
it('shows loading state initially', () => {
render(<MyComponent {...defaultProps} isLoading />);
expect(screen.getByText(/chargement/i)).toBeInTheDocument();
});
it('displays data after loading', async () => {
render(<MyComponent {...defaultProps} data={['result']} />);
expect(await screen.findByText('result')).toBeInTheDocument();
});
});
});
// Presence
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();
// State
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toBeVisible();
expect(element).toBeChecked();
// Content
expect(element).toHaveTextContent('text');
expect(element).toHaveValue('value');
expect(element).toHaveDisplayValue('display');
// Attributes & Classes
expect(element).toHaveAttribute('href', '/path');
expect(element).toHaveClass('fr-btn', 'fr-btn--primary');
expect(element).toHaveStyle({ color: 'red' });
// Accessibility
expect(element).toHaveAccessibleName('Submit form');
expect(element).toHaveAccessibleDescription('Click to submit');
DSFR components use specific class patterns. Use role-based queries when possible:
// Button
screen.getByRole('button', { name: /envoyer/i });
// Input with label
screen.getByLabelText('Identifiant');
// Alert/Notice
screen.getByRole('alert');
// Accordion - click the button to expand
const accordionButton = screen.getByRole('button', { name: /voir les détails/i });
await user.click(accordionButton);
expect(screen.getByText('Contenu détaillé')).toBeVisible();
// Modal/Dialog
screen.getByRole('dialog');
// Navigation links
screen.getByRole('link', { name: /accueil/i });
# Run all frontend tests
npm run test --workspace=apps/front
# Run tests in watch mode
npm run test -- --watch --workspace=apps/front
# Run specific test file
npm run test -- ControleGroup.spec.tsx --workspace=apps/front
# Run with coverage
npm run test -- --coverage --workspace=apps/front
screen queries for better test readabilitydata-testid as first choice - Use accessible queries first (role, label, text)userEvent methods - They return PromisesfindBy* for async content - Not getBy* with waitForvi.clearAllMocks() in beforeEachtoHaveClass('fr-btn', 'fr-btn--primary') not regex matching