end fase 2
This commit is contained in:
283
E2E_TESTING.md
Normal file
283
E2E_TESTING.md
Normal file
@ -0,0 +1,283 @@
|
||||
# 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)
|
||||
- Текстовые селекторы только для статичного контента
|
||||
|
||||
```typescript
|
||||
// ❌ Плохо - сломается при изменении структуры
|
||||
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`:
|
||||
|
||||
```tsx
|
||||
<Popover
|
||||
slotProps={{
|
||||
paper: {
|
||||
'data-testid': 'color-picker-popover',
|
||||
} as React.HTMLAttributes<HTMLDivElement>,
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
### Dialog
|
||||
|
||||
Dialog также использует Portal. Добавляй `data-testid` напрямую:
|
||||
|
||||
```tsx
|
||||
<Dialog data-testid="team-member-modal">
|
||||
```
|
||||
|
||||
### Select / Combobox
|
||||
|
||||
Для работы с MUI Select:
|
||||
|
||||
```typescript
|
||||
// Открыть 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:
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
```
|
||||
|
||||
### Шаблон тестового файла
|
||||
|
||||
```typescript
|
||||
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();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Группировка тестов
|
||||
|
||||
Группируй тесты по фичам/сценариям, а не по компонентам:
|
||||
|
||||
```typescript
|
||||
// ❌ Плохо - группировка по компонентам
|
||||
test.describe('Button tests', () => { ... });
|
||||
test.describe('Modal tests', () => { ... });
|
||||
|
||||
// ✅ Хорошо - группировка по фичам
|
||||
test.describe('Фаза 2: Управление командой - CRUD участников', () => { ... });
|
||||
test.describe('Фаза 2: Управление командой - Вкладка Роли', () => { ... });
|
||||
```
|
||||
|
||||
## Обработка edge cases
|
||||
|
||||
### Проверка наличия данных
|
||||
|
||||
```typescript
|
||||
test('Тест с данными', async ({ page }) => {
|
||||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||||
|
||||
// Пропускаем тест если нет данных
|
||||
test.skip(!hasData, 'Нет данных для тестирования');
|
||||
|
||||
// Продолжаем тест...
|
||||
});
|
||||
```
|
||||
|
||||
### Работа с динамическими ID
|
||||
|
||||
```typescript
|
||||
// Для элементов с динамическими ID используй prefix-селектор
|
||||
const ideaRows = page.locator('[data-testid^="idea-row-"]');
|
||||
const rowCount = await ideaRows.count();
|
||||
```
|
||||
|
||||
### Ожидание после действий
|
||||
|
||||
```typescript
|
||||
// После клика, который вызывает 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:
|
||||
|
||||
```tsx
|
||||
// Таблица
|
||||
<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>
|
||||
```
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
```bash
|
||||
# Все тесты
|
||||
npx playwright test
|
||||
|
||||
# Конкретный файл
|
||||
npx playwright test e2e/phase2.spec.ts
|
||||
|
||||
# С UI режимом для отладки
|
||||
npx playwright test --ui
|
||||
|
||||
# Только упавшие тесты
|
||||
npx playwright test --last-failed
|
||||
```
|
||||
Reference in New Issue
Block a user