end fase 2

This commit is contained in:
2026-01-15 00:18:35 +03:00
parent 85e7966c97
commit 739a7d172d
63 changed files with 3194 additions and 322 deletions

283
E2E_TESTING.md Normal file
View 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
```