Визуальное тестирование компонентов в Storybook
Updated Jul 2026
Почему тестирование компонентов выявляет проблемы раньше
Storybook позволяет проводить визуальное тестирование на уровне компонентов, выявляя визуальные регрессии до того, как они накопятся в проблемы на уровне страниц. Компонент кнопки, протестированный в изоляции во всех его состояниях (по умолчанию, при наведении, в фокусе, отключённый, загружающийся), ловит баги стилей в источнике — а не после того, как они распространились на 20 разных страниц.
Storybook + тестирование доступности
Настройте Storybook для запуска проверок доступности на каждой истории:
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
a11y: {
// axe-core configuration for all stories
config: {
rules: [
{ id: 'color-contrast', enabled: true },
{ id: 'image-alt', enabled: true },
{ id: 'label', enabled: true },
{ id: 'button-name', enabled: true },
],
},
},
},
};
export default preview;
Эта конфигурация добавляет панель «Accessibility» к каждой истории в Storybook. Разработчики видят нарушения в реальном времени при создании компонентов.
Написание историй для визуального тестирования
Компонент DataTable
// DataTable.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { DataTable } from './DataTable';
import { within, userEvent } from '@storybook/testing-library';
const meta: Meta<typeof DataTable> = {
title: 'Components/DataTable',
component: DataTable,
parameters: {
chromatic: {
viewports: [375, 768, 1200],
},
},
};
export default meta;
export const WithData: StoryObj<typeof DataTable> = {
args: {
columns: [
{ key: 'name', header: 'Product Name', sortable: true },
{ key: 'price', header: 'Price', sortable: true },
{ key: 'stock', header: 'In Stock', sortable: false },
],
data: [
{ name: 'Widget A', price: '$29.99', stock: 'Yes' },
{ name: 'Widget B', price: '$49.99', stock: 'No' },
{ name: 'Widget C', price: '$19.99', stock: 'Yes' },
],
},
};
export const Empty: StoryObj<typeof DataTable> = {
args: {
columns: [{ key: 'name', header: 'Product Name' }],
data: [],
},
};
export const Loading: StoryObj<typeof DataTable> = {
args: {
columns: [{ key: 'name', header: 'Product Name' }],
data: [],
loading: true,
},
};
export const WithError: StoryObj<typeof DataTable> = {
args: {
columns: [{ key: 'name', header: 'Product Name' }],
data: [],
error: 'Failed to load product data. Please try again.',
},
};
// Interaction testing: verify sort behavior
export const SortedByPrice: StoryObj<typeof DataTable> = {
...WithData,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Click the Price column header to sort
await userEvent.click(canvas.getByText('Price'));
// Visual snapshot captures the sorted state
},
};
Тестирование всех состояний компонента
Набор визуальной регрессии для одного компонента должен покрывать все значимые состояния:
// Button.stories.tsx -- comprehensive state coverage
export const AllStates: StoryObj<typeof Button> = {
render: () => (
<div style={{ display: 'grid', gap: '1rem', maxWidth: '400px' }}>
<h3>Primary</h3>
<Button variant="primary">Default</Button>
<Button variant="primary" disabled>Disabled</Button>
<Button variant="primary" loading>Loading</Button>
<h3>Secondary</h3>
<Button variant="secondary">Default</Button>
<Button variant="secondary" disabled>Disabled</Button>
<h3>Danger</h3>
<Button variant="danger">Default</Button>
<Button variant="danger" disabled>Disabled</Button>
<h3>Sizes</h3>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<h3>With Icons</h3>
<Button icon="plus">Add Item</Button>
<Button icon="trash" variant="danger">Delete</Button>
</div>
),
parameters: {
chromatic: {
viewports: [375, 1200],
},
},
};
// Focus state -- important for accessibility visual testing
export const FocusState: StoryObj<typeof Button> = {
args: { variant: 'primary', children: 'Focused Button' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await button.focus();
// Snapshot captures the focus ring
},
};
Истории доступности для компонентов форм
// TextField.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { TextField } from './TextField';
const meta: Meta<typeof TextField> = {
title: 'Components/TextField',
component: TextField,
};
export default meta;
export const Default: StoryObj<typeof TextField> = {
args: {
label: 'Email address',
placeholder: 'you@example.com',
type: 'email',
},
};
export const WithError: StoryObj<typeof TextField> = {
args: {
label: 'Email address',
value: 'not-an-email',
error: 'Please enter a valid email address',
},
};
export const WithHelperText: StoryObj<typeof TextField> = {
args: {
label: 'Password',
type: 'password',
helperText: 'Must be at least 8 characters with one number',
},
};
export const Required: StoryObj<typeof TextField> = {
args: {
label: 'Full Name',
required: true,
},
};
export const Disabled: StoryObj<typeof TextField> = {
args: {
label: 'Username',
value: 'johndoe',
disabled: true,
},
};
Автоматическое тестирование Storybook в CI
С Chromatic
- name: Build and test Storybook
run: |
npx storybook build -o storybook-static
npx chromatic --project-token=${{ secrets.CHROMATIC_TOKEN }} \
--exit-once-uploaded \
--only-changed
С Playwright
// tests/storybook/visual.spec.ts
import { test, expect } from '@playwright/test';
const STORYBOOK_URL = 'http://localhost:6006';
// Generate tests for each story
const stories = [
'components-button--primary',
'components-button--disabled',
'components-button--loading',
'components-datatable--with-data',
'components-datatable--empty',
'components-textfield--with-error',
];
for (const story of stories) {
test(`visual: ${story}`, async ({ page }) => {
await page.goto(`${STORYBOOK_URL}/iframe.html?id=${story}`);
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot(`${story}.png`, {
maxDiffPixelRatio: 0.01,
animations: 'disabled',
});
});
test(`a11y: ${story}`, async ({ page }) => {
await page.goto(`${STORYBOOK_URL}/iframe.html?id=${story}`);
await page.waitForLoadState('networkidle');
const AxeBuilder = (await import('@axe-core/playwright')).default;
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
}
Преимущества тестирования на уровне компонентов
| Преимущество | Тестирование на уровне страницы | Тестирование на уровне компонента |
|---|---|---|
| Скорость | Медленнее (полный рендеринг страницы) | Быстрее (изолированный компонент) |
| Изоляция | Побочные эффекты от других компонентов | Чистое поведение компонента |
| Покрытие состояний | Трудно достичь всех состояний | Легко отрендерить каждое состояние |
| Отладка | «Что-то изменилось на странице» | «Этот конкретный компонент изменился» |
| Поддержка | Тесты ломаются при изменении вёрстки | Тесты стабильны, если API компонента не изменился |
Визуальное тестирование на уровне компонентов в Storybook выявляет проблемы раньше, чем скриншоты уровня страницы, при значительно меньших затратах. Это эквивалент юнит-тестов против интеграционных тестов в визуальном тестировании — делайте и то, и другое, но начинайте с компонентов.