add ai functions
Some checks reported errors
continuous-integration/drone/push Build encountered an error

This commit is contained in:
2026-01-15 01:59:16 +03:00
parent 739a7d172d
commit dea0676169
33 changed files with 4850 additions and 104 deletions

463
tests/e2e/phase3.spec.ts Normal file
View File

@ -0,0 +1,463 @@
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();
});
});