| name | testing-infrastructure |
| description | Complete testing infrastructure and patterns for the monorepo. Use this skill when writing tests, setting up test configuration, troubleshooting test failures, or working with the CI pipeline. Covers vitest configuration, Storybook addon-vitest integration, React Testing Library patterns, mock data setup, and the test-suite-runner agent for comprehensive testing. |
Testing Infrastructure
Test Runners
Vitest
Primary test runner for:
- Unit tests (
.test.ts, .test.tsx)
- Storybook story tests (via addon-vitest)
Running Tests
pnpm test
pnpm test --filter @lsst-sqre/squared
pnpm test-storybook
pnpm test-storybook:watch
pnpm test-storybook --filter @lsst-sqre/squared
pnpm run localci
Vitest Configuration
Squared Package
Two test projects in vitest.config.ts:
Unit tests:
- Files:
src/**/*.test.{ts,tsx}
- Environment: jsdom
- Setup:
src/test-setup.ts
- CSS Modules: non-scoped strategy
Storybook tests:
- Browser: Playwright (chromium)
- Environment: browser + jsdom
- Setup:
.storybook/vitest.setup.ts
- Plugin:
@storybook/addon-vitest
Writing Unit Tests
Basic Pattern
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent title="Test" />);
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('handles user interaction', async () => {
const handleClick = vi.fn();
render(<MyComponent onClick={handleClick} />);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalled();
});
});
React Testing Library
Query priority:
getByRole - Most accessible
getByLabelText - For forms
getByPlaceholderText - For inputs
getByText - For content
getByTestId - Last resort
screen.getByRole('button', { name: 'Submit' });
screen.getByLabelText('Email address');
container.querySelector('.button');
User Interactions
import userEvent from '@testing-library/user-event';
it('handles form submission', async () => {
const user = userEvent.setup();
render(<MyForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
});
});
Storybook Tests
In Stories
import { expect, within, userEvent } from '@storybook/test';
export const WithInteraction: Story = {
tags: ['test'],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('Click me')).toBeInTheDocument();
await userEvent.click(canvas.getByRole('button'));
expect(canvas.getByText('Clicked!')).toBeInTheDocument();
},
};
Running Story Tests
pnpm test-storybook --filter @lsst-sqre/squared
pnpm test-storybook:watch --filter @lsst-sqre/squared
pnpm test-storybook --filter @lsst-sqre/squared -- --grep "MyComponent"
Mocking
Mock Functions
import { vi } from 'vitest';
const mockFn = vi.fn();
const mockFnWithReturn = vi.fn(() => 'result');
expect(mockFn).toHaveBeenCalledWith('arg');
expect(mockFn).toHaveBeenCalledTimes(2);
Mock Modules
vi.mock('../api/client', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'mock' })),
}));
Mock SWR
import useSWR from 'swr';
vi.mock('swr');
it('renders with data', () => {
vi.mocked(useSWR).mockReturnValue({
data: { name: 'Test' },
error: undefined,
isLoading: false,
isValidating: false,
mutate: vi.fn(),
});
render(<MyComponent />);
expect(screen.getByText('Test')).toBeInTheDocument();
});
Mock Data
Mock Data Patterns
export const mockUserData = {
username: 'testuser',
email: 'test@example.com',
groups: ['admin'],
};
export const mockUserDataLoading = {
data: undefined,
error: undefined,
isLoading: true,
};
export const mockUserDataError = {
data: undefined,
error: new Error('Failed to fetch'),
isLoading: false,
};
MSW (Mock Service Worker)
For API mocking in Storybook:
import { initialize, mswLoader } from 'msw-storybook-addon';
import { handlers } from '../src/mocks/handlers';
initialize({ onUnhandledRequest: 'bypass' });
export const loaders = [mswLoader];
export const parameters = {
msw: {
handlers,
},
};
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/user-info', () => {
return HttpResponse.json({
username: 'testuser',
email: 'test@example.com',
});
}),
];
Testing Configuration Components
import { AppConfigProvider } from '../contexts/AppConfigContext';
const mockConfig = {
siteName: 'Test Site',
baseUrl: 'http://localhost:3000',
};
function renderWithConfig(ui: React.ReactElement) {
return render(
<AppConfigProvider config={mockConfig}>
{ui}
</AppConfigProvider>
);
}
it('uses config', () => {
renderWithConfig(<MyComponent />);
expect(screen.getByText('Test Site')).toBeInTheDocument();
});
Async Testing
Waiting for Elements
await screen.findByText('Loaded data');
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
await waitFor(() => {
expect(screen.getByText('Done')).toBeInTheDocument();
});
Testing Async Functions
it('fetches data', async () => {
const { result } = renderHook(() => useMyData());
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toEqual(expectedData);
});
Coverage
pnpm test -- --coverage
test: {
coverage: {
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
}
Debugging Tests
pnpm test --filter @lsst-sqre/squared -- MyComponent.test.tsx
pnpm test --filter @lsst-sqre/squared -- --watch
pnpm test-storybook:watch --filter @lsst-sqre/squared
Debug Queries
import { screen } from '@testing-library/react';
screen.debug();
screen.debug(screen.getByRole('button'));
screen.logTestingPlaygroundURL();
CI Pipeline
test-suite-runner Agent
Use the test-suite-runner agent (via Task tool) for comprehensive testing:
The agent:
- Runs full CI pipeline
- Analyzes failures across all stages
- Provides detailed failure reports
- Suggests fixes
Best Practices
- Test behavior, not implementation
- Use semantic queries (getByRole, etc.)
- Mock external dependencies (APIs, modules)
- Test user interactions with userEvent
- Use descriptive test names
- Keep tests focused and small
- Avoid testing internal state
- Test accessibility with role queries
- Use story tests for visual testing
- Run tests before committing