Modern QA2026Визуальное тестирование компонентов в Storybook
Join

Course10 Visual & Accessibility Testing

Cutting-edge · Chapter 10

Визуальное тестирование компонентов в 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 выявляет проблемы раньше, чем скриншоты уровня страницы, при значительно меньших затратах. Это эквивалент юнит-тестов против интеграционных тестов в визуальном тестировании — делайте и то, и другое, но начинайте с компонентов.