10 KiB
10 KiB
E2E Testing Guide
Руководство по написанию e2e тестов для Team Planner.
Принципы
1. Тесты следуют требованиям, а не коду
Тесты должны проверять пользовательские сценарии из требований, а не адаптироваться под текущую реализацию.
❌ Плохо: "Проверить что кнопка имеет класс .MuiButton-contained"
✅ Хорошо: "Проверить что пользователь может создать новую идею"
Порядок работы:
- Прочитать требования к фазе/фиче в
ROADMAP.mdиREQUIREMENTS.md - Выделить пользовательские сценарии
- Написать тесты для каждого сценария
- Убедиться что тесты проверяют бизнес-логику, а не детали реализации
2. Стабильные селекторы через data-testid
Никогда не использовать:
- Позиционные селекторы:
tbody tr,.nth(2),:first-child - CSS классы MUI:
.MuiButton-root,.MuiTableCell-body - Структурные селекторы:
table > tbody > tr > td
Всегда использовать:
data-testidдля уникальной идентификации элементов[role="..."]только для стандартных ARIA ролей (tab, dialog, listbox)- Текстовые селекторы только для статичного контента
// ❌ Плохо - сломается при изменении структуры
const row = page.locator('tbody tr').nth(2);
const button = page.locator('.MuiIconButton-root').first();
// ✅ Хорошо - стабильно при рефакторинге
const row = page.locator('[data-testid="idea-row-123"]');
const button = page.locator('[data-testid="delete-idea-button"]');
Соглашения по data-testid
Именование
| Паттерн | Пример | Использование |
|---|---|---|
{component}-{element} |
ideas-table |
Основные элементы |
{component}-{element}-{id} |
idea-row-123 |
Динамические элементы |
{action}-{target}-button |
delete-idea-button |
Кнопки действий |
{name}-input |
member-name-input |
Поля ввода |
{name}-modal |
team-member-modal |
Модальные окна |
filter-{name} |
filter-status |
Фильтры |
Обязательные data-testid по компонентам
Таблицы
{name}-table - сам table элемент
{name}-table-container - обёртка таблицы
{name}-empty-state - состояние "нет данных"
{item}-row-{id} - строка с данными
Формы и модалки
{name}-modal - Dialog компонент
{name}-form - form элемент
{field}-input - поля ввода (TextField)
{field}-select - выпадающие списки (FormControl)
submit-{action}-button - кнопка отправки
cancel-{action}-button - кнопка отмены
Действия в строках
edit-{item}-button - редактирование
delete-{item}-button - удаление
toggle-{feature}-button - переключение
Работа с MUI компонентами
Popover / Menu
MUI Popover рендерится через Portal в <body>. Для добавления data-testid используй slotProps:
<Popover
slotProps={{
paper: {
'data-testid': 'color-picker-popover',
} as React.HTMLAttributes<HTMLDivElement>,
}}
>
Dialog
Dialog также использует Portal. Добавляй data-testid напрямую:
<Dialog data-testid="team-member-modal">
Select / Combobox
Для работы с MUI Select:
// Открыть dropdown
await page.locator('[data-testid="filter-status"] [role="combobox"]').click();
// Выбрать опцию из listbox
const listbox = page.locator('[role="listbox"]');
await listbox.locator('[role="option"]').filter({ hasText: 'Бэклог' }).click();
TextField
TextField в MUI оборачивает input в несколько div. Для доступа к самому input:
// data-testid на TextField
<TextField data-testid="member-name-input" />
// В тесте - добавляем input селектор
const input = page.locator('[data-testid="member-name-input"] input');
await input.fill('Имя');
Структура тестов
Файловая организация
tests/
├── e2e/
│ ├── auth.setup.ts # Аутентификация (запускается первой)
│ ├── phase1.spec.ts # Тесты фазы 1
│ ├── phase2.spec.ts # Тесты фазы 2
│ └── phase3.spec.ts # Тесты фазы 3
└── playwright.config.ts
Шаблон тестового файла
import { test, expect } from '@playwright/test';
/**
* E2E тесты для Фазы N Team Planner
* - Фича 1
* - Фича 2
*
* Используем data-testid для стабильных селекторов
*/
test.describe('Фаза N: Название фичи', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
// Ждём загрузки основного элемента
await page.waitForSelector('[data-testid="main-element"]', { timeout: 10000 });
});
test('Описание сценария', async ({ page }) => {
// Arrange - подготовка
const element = page.locator('[data-testid="element"]');
// Act - действие
await element.click();
// Assert - проверка
await expect(element).toBeVisible();
});
});
Группировка тестов
Группируй тесты по фичам/сценариям, а не по компонентам:
// ❌ Плохо - группировка по компонентам
test.describe('Button tests', () => { ... });
test.describe('Modal tests', () => { ... });
// ✅ Хорошо - группировка по фичам
test.describe('Фаза 2: Управление командой - CRUD участников', () => { ... });
test.describe('Фаза 2: Управление командой - Вкладка Роли', () => { ... });
Обработка edge cases
Проверка наличия данных
test('Тест с данными', async ({ page }) => {
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
const hasData = !(await emptyState.isVisible().catch(() => false));
// Пропускаем тест если нет данных
test.skip(!hasData, 'Нет данных для тестирования');
// Продолжаем тест...
});
Работа с динамическими ID
// Для элементов с динамическими ID используй prefix-селектор
const ideaRows = page.locator('[data-testid^="idea-row-"]');
const rowCount = await ideaRows.count();
Ожидание после действий
// После клика, который вызывает API запрос
await button.click();
await page.waitForTimeout(500); // Даём время на запрос
// Лучше - ждать конкретный результат
await expect(newElement).toBeVisible({ timeout: 5000 });
Чеклист перед написанием тестов
- Прочитаны требования к фиче в ROADMAP.md
- Определены пользовательские сценарии
- Проверено наличие data-testid в компонентах
- Если data-testid отсутствуют - добавить их в компоненты
- Тесты не зависят от порядка/позиции элементов в DOM
- Тесты корректно обрабатывают случай отсутствия данных
Добавление data-testid в компоненты
При добавлении новых компонентов или фич, сразу добавляй data-testid:
// Таблица
<Table data-testid="ideas-table">
<TableBody>
{items.map(item => (
<TableRow key={item.id} data-testid={`idea-row-${item.id}`}>
<TableCell>
<IconButton data-testid="delete-idea-button">
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
// Модалка с формой
<Dialog data-testid="create-idea-modal">
<form data-testid="create-idea-form">
<TextField data-testid="idea-title-input" />
<Button data-testid="submit-create-idea">Создать</Button>
<Button data-testid="cancel-create-idea">Отмена</Button>
</form>
</Dialog>
Запуск тестов
# Все тесты (из корня проекта)
npm run test
# Конкретный файл
npx playwright test e2e/phase2.spec.ts
# Конкретный тест по имени
npx playwright test -g "Drag handle имеет правильный курсор"
# С UI режимом для отладки
npx playwright test --ui
# Только упавшие тесты
npx playwright test --last-failed
Правила исправления тестов
ВАЖНО: При исправлении сломанных тестов:
- НЕ запускай полный прогон после каждого исправления
- Запускай только сломанный тест для проверки исправления:
npx playwright test -g "Название теста" - Полный прогон делай только когда все сломанные тесты исправлены
- Это экономит время и ресурсы при отладке