Some checks reported errors
continuous-integration/drone/push Build encountered an error
464 lines
21 KiB
TypeScript
464 lines
21 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
|
||
/**
|
||
* E2E тесты для Фазы 3 Team Planner
|
||
* - AI-оценка трудозатрат
|
||
*
|
||
* Используем data-testid для стабильных селекторов
|
||
*/
|
||
|
||
test.describe('Фаза 3: AI-оценка', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForSelector('[data-testid="ideas-table"]', { timeout: 10000 });
|
||
});
|
||
|
||
test('Кнопка AI-оценки присутствует в каждой строке', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
if (hasData) {
|
||
const estimateButtons = page.locator('[data-testid="estimate-idea-button"]');
|
||
const buttonCount = await estimateButtons.count();
|
||
expect(buttonCount).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
test('Клик на кнопку AI-оценки открывает модалку', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
// Кликаем на кнопку AI-оценки первой идеи
|
||
const estimateButton = page.locator('[data-testid="estimate-idea-button"]').first();
|
||
await estimateButton.click();
|
||
|
||
// Проверяем что модалка открылась
|
||
const modal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
});
|
||
|
||
test('Модалка AI-оценки показывает загрузку', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const estimateButton = page.locator('[data-testid="estimate-idea-button"]').first();
|
||
await estimateButton.click();
|
||
|
||
const modal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Должен быть либо индикатор загрузки, либо результат, либо ошибка
|
||
const hasContent = await modal.locator('text=Анализируем').isVisible().catch(() => false) ||
|
||
await modal.locator('text=Общее время').isVisible().catch(() => false) ||
|
||
await modal.locator('text=Не удалось').isVisible().catch(() => false);
|
||
|
||
expect(hasContent).toBeTruthy();
|
||
});
|
||
|
||
test('AI-оценка возвращает результат с часами и сложностью', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const estimateButton = page.locator('[data-testid="estimate-idea-button"]').first();
|
||
await estimateButton.click();
|
||
|
||
const modal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём результат (до 30 секунд - AI может отвечать долго)
|
||
const totalTimeLabel = modal.locator('text=Общее время');
|
||
await expect(totalTimeLabel).toBeVisible({ timeout: 30000 });
|
||
|
||
// Проверяем наличие сложности
|
||
const complexityLabel = modal.locator('text=Сложность');
|
||
await expect(complexityLabel).toBeVisible();
|
||
});
|
||
|
||
test('AI-оценка показывает разбивку по ролям', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const estimateButton = page.locator('[data-testid="estimate-idea-button"]').first();
|
||
await estimateButton.click();
|
||
|
||
const modal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём результат
|
||
await expect(modal.locator('text=Общее время')).toBeVisible({ timeout: 30000 });
|
||
|
||
// Проверяем наличие таблицы разбивки по ролям
|
||
const breakdownLabel = modal.locator('text=Разбивка по ролям');
|
||
// Разбивка опциональна (может не быть если команда не указана)
|
||
const hasBreakdown = await breakdownLabel.isVisible().catch(() => false);
|
||
|
||
if (hasBreakdown) {
|
||
const breakdownRows = modal.locator('[data-testid^="estimate-breakdown-row-"]');
|
||
const rowCount = await breakdownRows.count();
|
||
expect(rowCount).toBeGreaterThanOrEqual(0);
|
||
}
|
||
});
|
||
|
||
test('Кнопка закрытия модалки работает', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const estimateButton = page.locator('[data-testid="estimate-idea-button"]').first();
|
||
await estimateButton.click();
|
||
|
||
const modal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Закрываем модалку
|
||
const closeButton = page.locator('[data-testid="close-estimate-modal-button"]');
|
||
await closeButton.click();
|
||
|
||
// Модалка должна закрыться
|
||
await expect(modal).not.toBeVisible({ timeout: 3000 });
|
||
});
|
||
|
||
test('После оценки результат сохраняется в строке таблицы', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
// Запоминаем первую строку
|
||
const firstRow = page.locator('[data-testid^="idea-row-"]').first();
|
||
|
||
// Кликаем на оценку
|
||
const estimateButton = firstRow.locator('[data-testid="estimate-idea-button"]');
|
||
await estimateButton.click();
|
||
|
||
const modal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём результат
|
||
await expect(modal.locator('text=Общее время')).toBeVisible({ timeout: 30000 });
|
||
|
||
// Закрываем модалку
|
||
await page.locator('[data-testid="close-estimate-modal-button"]').click();
|
||
await expect(modal).not.toBeVisible({ timeout: 3000 });
|
||
|
||
// Проверяем что в строке появилась оценка (часы или дни)
|
||
// Ищем текст типа "8ч" или "2д" в строке
|
||
await page.waitForTimeout(500);
|
||
|
||
// Колонка "Оценка" должна содержать данные
|
||
const rowText = await firstRow.textContent();
|
||
const hasEstimate = rowText?.match(/\d+[чд]/) !== null;
|
||
|
||
expect(hasEstimate).toBeTruthy();
|
||
});
|
||
|
||
test('Колонка "Оценка" отображается в таблице', async ({ page }) => {
|
||
const table = page.locator('[data-testid="ideas-table"]');
|
||
await expect(table).toBeVisible();
|
||
|
||
// Проверяем наличие заголовка колонки
|
||
const header = table.locator('th', { hasText: 'Оценка' });
|
||
await expect(header).toBeVisible();
|
||
});
|
||
|
||
test('Клик по оценке открывает модалку с деталями', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
// Ищем строку с оценкой (кнопка view-estimate-button появляется только если есть оценка)
|
||
const viewEstimateButton = page.locator('[data-testid="view-estimate-button"]').first();
|
||
const hasEstimate = await viewEstimateButton.isVisible().catch(() => false);
|
||
|
||
test.skip(!hasEstimate, 'Нет идей с оценкой для тестирования');
|
||
|
||
// Кликаем по оценке
|
||
await viewEstimateButton.click();
|
||
|
||
// Модалка должна открыться с деталями
|
||
const modal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Должны быть видны результаты (без загрузки)
|
||
await expect(modal.locator('text=Общее время')).toBeVisible();
|
||
await expect(modal.locator('text=Сложность')).toBeVisible();
|
||
});
|
||
});
|
||
|
||
test.describe('Фаза 3: AI-оценка - создание данных для теста', () => {
|
||
test('Создание идеи и запуск AI-оценки', async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForSelector('[data-testid="ideas-table"]', { timeout: 10000 });
|
||
|
||
// Проверяем есть ли кнопка создания идеи
|
||
const createButton = page.locator('[data-testid="create-idea-button"]');
|
||
const hasCreateButton = await createButton.isVisible().catch(() => false);
|
||
|
||
if (hasCreateButton) {
|
||
// Создаём идею
|
||
await createButton.click();
|
||
|
||
const modal = page.locator('[data-testid="create-idea-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Заполняем форму
|
||
await page.locator('[data-testid="idea-title-input"] input').fill('Тестовая идея для AI-оценки');
|
||
await page.locator('[data-testid="idea-description-input"] textarea').first().fill(
|
||
'Реализовать систему уведомлений. Нужны email и push-уведомления для важных событий.'
|
||
);
|
||
|
||
// Сохраняем
|
||
await page.locator('[data-testid="submit-create-idea"]').click();
|
||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём появления новой строки
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// Теперь проверяем AI-оценку
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Не удалось создать данные для тестирования');
|
||
|
||
// Запускаем AI-оценку
|
||
const estimateButton = page.locator('[data-testid="estimate-idea-button"]').first();
|
||
await estimateButton.click();
|
||
|
||
const estimateModal = page.locator('[data-testid="ai-estimate-modal"]');
|
||
await expect(estimateModal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём результат (до 60 секунд - AI может отвечать долго)
|
||
// Или ошибку (текст "Не удалось" из компонента)
|
||
const resultOrError = estimateModal.locator('text=/Общее время|Не удалось/');
|
||
await expect(resultOrError).toBeVisible({ timeout: 60000 });
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Тесты для генерации мини-ТЗ (Phase 3.1)
|
||
*/
|
||
test.describe('Фаза 3.1: Генерация мини-ТЗ', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForSelector('[data-testid="ideas-table"]', { timeout: 10000 });
|
||
});
|
||
|
||
test('Кнопка ТЗ присутствует в каждой строке', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
if (hasData) {
|
||
const specButtons = page.locator('[data-testid="specification-button"]');
|
||
const buttonCount = await specButtons.count();
|
||
expect(buttonCount).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
test('Клик на кнопку ТЗ открывает модалку', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
// Кликаем на кнопку ТЗ первой идеи
|
||
const specButton = page.locator('[data-testid="specification-button"]').first();
|
||
await specButton.click();
|
||
|
||
// Проверяем что модалка открылась
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
});
|
||
|
||
test('Модалка ТЗ показывает загрузку при генерации', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
// Ищем строку без ТЗ (кнопка не подсвечена синим)
|
||
const specButton = page.locator('[data-testid="specification-button"]').first();
|
||
await specButton.click();
|
||
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Должен быть либо индикатор загрузки, либо контент, либо ошибка
|
||
const hasContent = await modal.locator('[data-testid="specification-loading"]').isVisible().catch(() => false) ||
|
||
await modal.locator('[data-testid="specification-content"]').isVisible().catch(() => false) ||
|
||
await modal.locator('[data-testid="specification-error"]').isVisible().catch(() => false);
|
||
|
||
expect(hasContent).toBeTruthy();
|
||
});
|
||
|
||
test('Генерация ТЗ возвращает результат', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const specButton = page.locator('[data-testid="specification-button"]').first();
|
||
await specButton.click();
|
||
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём результат (до 60 секунд - AI может отвечать долго)
|
||
const content = modal.locator('[data-testid="specification-content"]');
|
||
const error = modal.locator('[data-testid="specification-error"]');
|
||
|
||
// Ожидаем либо контент, либо ошибку
|
||
await expect(content.or(error)).toBeVisible({ timeout: 60000 });
|
||
});
|
||
|
||
test('Кнопка закрытия модалки ТЗ работает', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const specButton = page.locator('[data-testid="specification-button"]').first();
|
||
await specButton.click();
|
||
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём пока загрузится контент или ошибка
|
||
const content = modal.locator('[data-testid="specification-content"]');
|
||
const error = modal.locator('[data-testid="specification-error"]');
|
||
await expect(content.or(error)).toBeVisible({ timeout: 60000 });
|
||
|
||
// Закрываем модалку
|
||
const closeButton = page.locator('[data-testid="specification-close-button"]');
|
||
await closeButton.click();
|
||
|
||
// Модалка должна закрыться
|
||
await expect(modal).not.toBeVisible({ timeout: 3000 });
|
||
});
|
||
|
||
test('Кнопка редактирования ТЗ появляется после генерации', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const specButton = page.locator('[data-testid="specification-button"]').first();
|
||
await specButton.click();
|
||
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём контент
|
||
const content = modal.locator('[data-testid="specification-content"]');
|
||
const hasContent = await content.isVisible({ timeout: 60000 }).catch(() => false);
|
||
|
||
if (hasContent) {
|
||
// Проверяем наличие кнопки редактирования
|
||
const editButton = modal.locator('[data-testid="specification-edit-button"]');
|
||
await expect(editButton).toBeVisible();
|
||
}
|
||
});
|
||
|
||
test('Редактирование ТЗ открывает textarea', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const specButton = page.locator('[data-testid="specification-button"]').first();
|
||
await specButton.click();
|
||
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём контент
|
||
const content = modal.locator('[data-testid="specification-content"]');
|
||
const hasContent = await content.isVisible({ timeout: 60000 }).catch(() => false);
|
||
|
||
test.skip(!hasContent, 'Не удалось сгенерировать ТЗ');
|
||
|
||
// Кликаем редактировать
|
||
const editButton = modal.locator('[data-testid="specification-edit-button"]');
|
||
await editButton.click();
|
||
|
||
// Должен появиться textarea
|
||
const textarea = modal.locator('[data-testid="specification-textarea"]');
|
||
await expect(textarea).toBeVisible();
|
||
});
|
||
|
||
test('Сохранение отредактированного ТЗ', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
const specButton = page.locator('[data-testid="specification-button"]').first();
|
||
await specButton.click();
|
||
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Ждём контент
|
||
const content = modal.locator('[data-testid="specification-content"]');
|
||
const hasContent = await content.isVisible({ timeout: 60000 }).catch(() => false);
|
||
|
||
test.skip(!hasContent, 'Не удалось сгенерировать ТЗ');
|
||
|
||
// Кликаем редактировать
|
||
const editButton = modal.locator('[data-testid="specification-edit-button"]');
|
||
await editButton.click();
|
||
|
||
// Редактируем текст
|
||
const textarea = modal.locator('[data-testid="specification-textarea"] textarea');
|
||
const testText = '\n\n## Дополнительно\nТестовая правка ' + Date.now();
|
||
await textarea.fill(await textarea.inputValue() + testText);
|
||
|
||
// Сохраняем
|
||
const saveButton = modal.locator('[data-testid="specification-save-button"]');
|
||
await saveButton.click();
|
||
|
||
// Должен вернуться режим просмотра
|
||
await expect(content).toBeVisible({ timeout: 5000 });
|
||
|
||
// Проверяем что изменения сохранились
|
||
const contentText = await content.textContent();
|
||
expect(contentText).toContain('Дополнительно');
|
||
});
|
||
|
||
test('Повторное открытие показывает сохранённое ТЗ', async ({ page }) => {
|
||
const emptyState = page.locator('[data-testid="ideas-empty-state"]');
|
||
const hasData = !(await emptyState.isVisible().catch(() => false));
|
||
|
||
test.skip(!hasData, 'Нет данных для тестирования');
|
||
|
||
// Ищем идею с уже сгенерированным ТЗ (кнопка синяя)
|
||
const blueSpecButton = page.locator('[data-testid="specification-button"][class*="primary"]').first();
|
||
const hasExistingSpec = await blueSpecButton.isVisible().catch(() => false);
|
||
|
||
test.skip(!hasExistingSpec, 'Нет идей с ТЗ для тестирования');
|
||
|
||
await blueSpecButton.click();
|
||
|
||
const modal = page.locator('[data-testid="specification-modal"]');
|
||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||
|
||
// Контент должен появиться сразу (без загрузки)
|
||
const content = modal.locator('[data-testid="specification-content"]');
|
||
await expect(content).toBeVisible({ timeout: 3000 });
|
||
|
||
// Не должно быть индикатора загрузки
|
||
const loading = modal.locator('[data-testid="specification-loading"]');
|
||
await expect(loading).not.toBeVisible();
|
||
});
|
||
});
|