Files
team-planner/E2E_TESTING.md
vigdorov 2e46cc41a1
Some checks failed
continuous-integration/drone/push Build is failing
fix lint
2026-01-15 02:36:24 +03:00

10 KiB
Raw Permalink Blame History

E2E Testing Guide

Руководство по написанию e2e тестов для Team Planner.

Принципы

1. Тесты следуют требованиям, а не коду

Тесты должны проверять пользовательские сценарии из требований, а не адаптироваться под текущую реализацию.

❌ Плохо: "Проверить что кнопка имеет класс .MuiButton-contained"
✅ Хорошо: "Проверить что пользователь может создать новую идею"

Порядок работы:

  1. Прочитать требования к фазе/фиче в ROADMAP.md и REQUIREMENTS.md
  2. Выделить пользовательские сценарии
  3. Написать тесты для каждого сценария
  4. Убедиться что тесты проверяют бизнес-логику, а не детали реализации

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

Правила исправления тестов

ВАЖНО: При исправлении сломанных тестов:

  1. НЕ запускай полный прогон после каждого исправления
  2. Запускай только сломанный тест для проверки исправления:
    npx playwright test -g "Название теста"
    
  3. Полный прогон делай только когда все сломанные тесты исправлены
  4. Это экономит время и ресурсы при отладке