diff --git a/web/frontend/src/__tests__/execution-mode-unification.test.ts b/web/frontend/src/__tests__/execution-mode-unification.test.ts index d968531..25ea209 100644 --- a/web/frontend/src/__tests__/execution-mode-unification.test.ts +++ b/web/frontend/src/__tests__/execution-mode-unification.test.ts @@ -176,14 +176,13 @@ describe('KIN-FIX-002: execution_mode унификация на "auto_complete"' }) await flushPromises() - // Открываем ⚙ Mode меню и кликаем по кнопке авто/ревью - const trigger = wrapper.find('[data-testid="mode-menu-trigger"]') - if (trigger.exists()) { - await trigger.trigger('click') - await flushPromises() + // Найти и кликнуть кнопку тоггла режима + const toggleBtn = wrapper.findAll('button').find(b => + b.text().includes('Auto') || b.text().includes('Review') + ) - const autoBtn = wrapper.find('[data-testid="mode-toggle-auto"]') - await autoBtn.trigger('click') + if (toggleBtn) { + await toggleBtn.trigger('click') await flushPromises() // Проверяем, что localStorage содержит 'auto_complete', не 'auto' @@ -548,10 +547,11 @@ describe('KIN-097: runTask синхронизирует execution_mode с тог }) await flushPromises() - // Триггер ⚙ Mode должен иметь data-mode="auto" при execution_mode=auto_complete - const trigger = wrapper.find('[data-testid="mode-menu-trigger"]') - expect(trigger.exists()).toBe(true) - expect(trigger.attributes('data-mode')).toBe('auto') + // Тоггл должен показывать Auto + const toggleBtn = wrapper.findAll('button').find(b => + b.text().includes('Auto') || b.text().includes('Review') + ) + expect(toggleBtn!.text()).toContain('Auto') // DB переключается на review (например, другой клиент изменил режим) vi.mocked(api.project).mockResolvedValue( @@ -561,14 +561,17 @@ describe('KIN-097: runTask синхронизирует execution_mode с тог // После load() тоггл должен обновиться на Review // Имитируем внешний load (например, после создания задачи) vi.mocked(api.patchProject).mockResolvedValue({ execution_mode: 'review' } as any) - // Вместо этого напрямую проверим что при новом mount с review — data-mode="review" + // Триггерим reload через toggleAutocommit (который вызывает patchProject, но не load) + // Вместо этого напрямую проверим что при новом mount с review — кнопка Review const wrapper2 = mount(ProjectView, { props: { id: 'KIN' }, global: { plugins: [router] }, }) await flushPromises() - const trigger2 = wrapper2.find('[data-testid="mode-menu-trigger"]') - expect(trigger2.attributes('data-mode')).toBe('review') + const toggleBtn2 = wrapper2.findAll('button').find(b => + b.text().includes('Auto') || b.text().includes('Review') + ) + expect(toggleBtn2!.text()).toContain('Review') }) }) diff --git a/web/frontend/src/__tests__/filter-persistence.test.ts b/web/frontend/src/__tests__/filter-persistence.test.ts index 7f0e95c..b313950 100644 --- a/web/frontend/src/__tests__/filter-persistence.test.ts +++ b/web/frontend/src/__tests__/filter-persistence.test.ts @@ -517,14 +517,7 @@ describe('KIN-047: TaskDetail — Approve/Reject в статусе review', () = // ───────────────────────────────────────────────────────────── describe('KIN-065: ProjectView — Autocommit toggle', () => { - async function openModeMenu(wrapper: ReturnType) { - const trigger = wrapper.find('[data-testid="mode-menu-trigger"]') - await trigger.trigger('click') - await flushPromises() - return wrapper.find('[data-testid="mode-toggle-autocommit"]') - } - - it('Кнопка Autocommit присутствует в меню ⚙ Mode', async () => { + it('Кнопка Autocommit присутствует в DOM', async () => { const router = makeRouter() await router.push('/project/KIN') @@ -534,8 +527,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = await openModeMenu(wrapper) - expect(btn.exists()).toBe(true) + const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) + expect(btn?.exists()).toBe(true) }) it('Кнопка имеет title "Autocommit: off" когда autocommit_enabled=0', async () => { @@ -549,8 +542,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = await openModeMenu(wrapper) - expect(btn.attributes('title')).toBe('Autocommit: off') + const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) + expect(btn?.attributes('title')).toBe('Autocommit: off') }) it('Кнопка имеет title "Autocommit: on..." когда autocommit_enabled=1', async () => { @@ -564,8 +557,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = await openModeMenu(wrapper) - expect(btn.attributes('title')).toContain('Autocommit: on') + const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) + expect(btn?.attributes('title')).toContain('Autocommit: on') }) it('Клик по кнопке вызывает patchProject с autocommit_enabled=true (включение)', async () => { @@ -581,8 +574,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = await openModeMenu(wrapper) - await btn.trigger('click') + const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) + await btn!.trigger('click') await flushPromises() expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: true }) @@ -601,8 +594,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = await openModeMenu(wrapper) - await btn.trigger('click') + const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) + await btn!.trigger('click') await flushPromises() expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: false }) @@ -623,8 +616,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = await openModeMenu(wrapper) - await btn.trigger('click') + const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) + await btn!.trigger('click') await flushPromises() // Catch-блок установил error.value → компонент показывает сообщение об ошибке diff --git a/web/frontend/src/__tests__/kin-ui-028-action-banner-vertical-pipeline.test.ts b/web/frontend/src/__tests__/kin-ui-028-action-banner-vertical-pipeline.test.ts deleted file mode 100644 index 0aa1430..0000000 --- a/web/frontend/src/__tests__/kin-ui-028-action-banner-vertical-pipeline.test.ts +++ /dev/null @@ -1,455 +0,0 @@ -/** - * KIN-UI-028: Тесты для action-banner и vertical pipeline в TaskDetail.vue - * - * Feature 1: Action-banner при status='review' && !autoMode - * - Показывается при review + execution_mode=review (autoMode=false) - * - Скрывается при autoMode=true (execution_mode=auto_complete) - * - Скрывается при status != review - * - Кнопки Approve/Revise/Reject вызывают корректные обработчики - * - Кнопка Auto mode вызывает toggleMode → api.patchTask - * - * Feature 2: Vertical timeline при pipeline_steps.length > 5 - * - ≤5 шагов → горизонтальный скролл (overflow-x-auto) - * - >5 шагов → вертикальный timeline с chevron - * - Иконки: success=true/1 → ✓ green, success=false/0 → ✗ red, null → ● blue - * - Поля роли, duration_seconds, cost_usd отображаются - * - Chevron переключается по клику - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { mount, flushPromises } from '@vue/test-utils' -import { createRouter, createMemoryHistory } from 'vue-router' -import * as fs from 'node:fs' -import * as path from 'node:path' -import enJson from '../locales/en.json' -import ruJson from '../locales/ru.json' -import TaskDetail from '../views/TaskDetail.vue' - -vi.mock('../api', () => ({ - api: { - taskFull: vi.fn(), - patchTask: vi.fn(), - runTask: vi.fn(), - approveTask: vi.fn(), - rejectTask: vi.fn(), - reviseTask: vi.fn(), - followupTask: vi.fn(), - deployProject: vi.fn(), - getAttachments: vi.fn(), - resolveAction: vi.fn(), - }, -})) - -import { api } from '../api' - -const localStorageMock = (() => { - let store: Record = {} - return { - getItem: (k: string) => store[k] ?? null, - setItem: (k: string, v: string) => { store[k] = v }, - removeItem: (k: string) => { delete store[k] }, - clear: () => { store = {} }, - } -})() -Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, configurable: true }) - -const Stub = { template: '
' } - -function makeRouter() { - return createRouter({ - history: createMemoryHistory(), - routes: [ - { path: '/', component: Stub }, - { path: '/task/:id', component: TaskDetail, props: true }, - ], - }) -} - -function makeStep(id: number, role: string, success: boolean | number | null = true, overrides: Record = {}): Record { - return { - id, - agent_role: role, - success, - output_summary: null, - duration_seconds: 10, - tokens_used: 500, - cost_usd: 0.01, - created_at: '2026-01-01T00:00:00', - ...overrides, - } -} - -function makeTask(overrides: Record = {}): Record { - return { - id: 'KIN-UI-028', - project_id: 'KIN', - title: 'Test Task', - status: 'review', - priority: 5, - assigned_role: null, - parent_task_id: null, - brief: null, - spec: null, - execution_mode: 'review', - blocked_reason: null, - dangerously_skipped: null, - category: null, - acceptance_criteria: null, - created_at: '2026-01-01', - updated_at: '2026-01-01', - pipeline_steps: [], - related_decisions: [], - pending_actions: [], - pipeline_id: null, - project_deploy_command: null, - project_deploy_runtime: null, - ...overrides, - } -} - -async function mountTask(taskData: Record) { - vi.mocked(api.taskFull).mockResolvedValue(taskData as any) - const router = makeRouter() - await router.push('/task/KIN-UI-028') - const wrapper = mount(TaskDetail, { - props: { id: 'KIN-UI-028' }, - global: { plugins: [router] }, - }) - await flushPromises() - return wrapper -} - -beforeEach(() => { - localStorageMock.clear() - vi.clearAllMocks() - vi.mocked(api.getAttachments).mockResolvedValue([]) -}) - -// ============================================================================ -// Feature 1: Action-banner — i18n ключи -// ============================================================================ - -describe('KIN-UI-028 Feature 1: i18n ключи для banner в en.json', () => { - it('ключ taskDetail.review_required присутствует в en.json', () => { - expect((enJson.taskDetail as Record).review_required).toBeDefined() - }) - - it('ключ taskDetail.banner_auto_mode присутствует в en.json', () => { - expect((enJson.taskDetail as Record).banner_auto_mode).toBeDefined() - }) -}) - -describe('KIN-UI-028 Feature 1: i18n ключи для banner в ru.json', () => { - it('ключ taskDetail.review_required присутствует в ru.json', () => { - expect((ruJson.taskDetail as Record).review_required).toBeDefined() - }) - - it('ключ taskDetail.banner_auto_mode присутствует в ru.json', () => { - expect((ruJson.taskDetail as Record).banner_auto_mode).toBeDefined() - }) -}) - -// ============================================================================ -// Feature 1: Action-banner — структура шаблона -// ============================================================================ - -describe('KIN-UI-028 Feature 1: Action-banner в шаблоне TaskDetail.vue', () => { - it('banner имеет v-if с условием task.status === review && !autoMode', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain("task.status === 'review' && !autoMode") - }) - - it('banner использует ключ taskDetail.review_required', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain("taskDetail.review_required") - }) - - it('banner использует ключ taskDetail.banner_auto_mode', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain("taskDetail.banner_auto_mode") - }) -}) - -// ============================================================================ -// Feature 1: Action-banner — видимость -// ============================================================================ - -describe('KIN-UI-028 Feature 1: Action-banner виден при status=review, autoMode=false', () => { - it('banner показывается при status=review и execution_mode=review', async () => { - const wrapper = await mountTask(makeTask({ status: 'review', execution_mode: 'review' })) - expect(wrapper.html()).toContain('Review required') - }) - - it('banner показывается при status=review и execution_mode=null (review-fallback)', async () => { - const wrapper = await mountTask(makeTask({ status: 'review', execution_mode: null })) - expect(wrapper.html()).toContain('Review required') - }) -}) - -describe('KIN-UI-028 Feature 1: Action-banner скрыт при autoMode=true', () => { - it('banner отсутствует при status=review и execution_mode=auto_complete', async () => { - const wrapper = await mountTask(makeTask({ status: 'review', execution_mode: 'auto_complete' })) - expect(wrapper.html()).not.toContain('Review required') - }) -}) - -describe('KIN-UI-028 Feature 1: Action-banner скрыт при status != review', () => { - it('banner отсутствует при status=pending', async () => { - const wrapper = await mountTask(makeTask({ status: 'pending', execution_mode: 'review' })) - expect(wrapper.html()).not.toContain('Review required') - }) - - it('banner отсутствует при status=done', async () => { - const wrapper = await mountTask(makeTask({ status: 'done', execution_mode: 'review' })) - expect(wrapper.html()).not.toContain('Review required') - }) - - it('banner отсутствует при status=in_progress', async () => { - const wrapper = await mountTask(makeTask({ status: 'in_progress', execution_mode: 'review' })) - expect(wrapper.html()).not.toContain('Review required') - }) - - it('banner отсутствует при status=blocked', async () => { - const wrapper = await mountTask(makeTask({ status: 'blocked', execution_mode: 'review' })) - expect(wrapper.html()).not.toContain('Review required') - }) -}) - -// ============================================================================ -// Feature 1: Action-banner — обработчики кнопок -// ============================================================================ - -describe('KIN-UI-028 Feature 1: Action-banner кнопка Approve открывает Approve Modal', () => { - it('клик кнопки Approve в banner открывает модальное окно', async () => { - const wrapper = await mountTask(makeTask({ status: 'review', execution_mode: 'review' })) - const buttons = wrapper.findAll('button') - const approveBtn = buttons.find(b => b.text().includes('Approve')) - expect(approveBtn?.exists()).toBe(true) - await approveBtn!.trigger('click') - expect(wrapper.html()).toContain('Approve Task') - }) -}) - -describe('KIN-UI-028 Feature 1: Action-banner кнопка Revise открывает Revise Modal', () => { - it('клик кнопки Revise в banner открывает модальное окно', async () => { - const wrapper = await mountTask(makeTask({ status: 'review', execution_mode: 'review' })) - const buttons = wrapper.findAll('button') - const reviseBtn = buttons.find(b => b.text().includes('Revise')) - expect(reviseBtn?.exists()).toBe(true) - await reviseBtn!.trigger('click') - // Modal title uses t('taskDetail.send_to_revision') = "🔄 Send for revision" - expect(wrapper.html()).toContain('revision') - }) -}) - -describe('KIN-UI-028 Feature 1: Action-banner кнопка Reject открывает Reject Modal', () => { - it('клик кнопки Reject в banner открывает модальное окно', async () => { - const wrapper = await mountTask(makeTask({ status: 'review', execution_mode: 'review' })) - const buttons = wrapper.findAll('button') - const rejectBtn = buttons.find(b => b.text().includes('Reject')) - expect(rejectBtn?.exists()).toBe(true) - await rejectBtn!.trigger('click') - expect(wrapper.html()).toContain('Reject Task') - }) -}) - -describe('KIN-UI-028 Feature 1: Action-banner кнопка Auto mode вызывает toggleMode', () => { - it('клик Auto mode в banner вызывает api.patchTask с execution_mode=auto_complete', async () => { - vi.mocked(api.patchTask).mockResolvedValue(makeTask({ status: 'review', execution_mode: 'auto_complete' }) as any) - const wrapper = await mountTask(makeTask({ status: 'review', execution_mode: 'review' })) - const buttons = wrapper.findAll('button') - const autoBtn = buttons.find(b => b.text().includes('Auto mode') || b.text().includes('auto')) - expect(autoBtn?.exists()).toBe(true) - await autoBtn!.trigger('click') - await flushPromises() - expect(vi.mocked(api.patchTask)).toHaveBeenCalledWith('KIN-UI-028', { execution_mode: 'auto_complete' }) - }) -}) - -// ============================================================================ -// Feature 2: Vertical pipeline — computed и структура -// ============================================================================ - -describe('KIN-UI-028 Feature 2: useVerticalPipeline computed в TaskDetail.vue', () => { - it('useVerticalPipeline определён в скрипте компонента', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('useVerticalPipeline') - }) - - it('useVerticalPipeline использует порог > 5', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('> 5') - }) -}) - -// ============================================================================ -// Feature 2: Horizontal pipeline при ≤5 шагах -// ============================================================================ - -describe('KIN-UI-028 Feature 2: горизонтальный pipeline при ≤5 шагах', () => { - it('при 1 шаге рендерится горизонтальный overflow-x-auto', async () => { - const steps = [makeStep(1, 'pm', true)] - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('overflow-x-auto') - }) - - it('при 5 шагах рендерится горизонтальный overflow-x-auto', async () => { - const steps = Array.from({ length: 5 }, (_, i) => makeStep(i + 1, 'frontend_dev', true)) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('overflow-x-auto') - }) - - it('при 5 шагах НЕ рендерится chevron вертикального timeline', async () => { - const steps = Array.from({ length: 5 }, (_, i) => makeStep(i + 1, 'frontend_dev', true)) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).not.toContain('▼') - }) -}) - -// ============================================================================ -// Feature 2: Vertical pipeline при >5 шагах -// ============================================================================ - -describe('KIN-UI-028 Feature 2: вертикальный timeline при >5 шагах', () => { - function make6Steps(successVal: boolean | number | null = true): Record[] { - return Array.from({ length: 6 }, (_, i) => makeStep(i + 1, 'frontend_dev', successVal)) - } - - it('при 6 шагах рендерится chevron ▼ вертикального timeline', async () => { - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: make6Steps() })) - expect(wrapper.html()).toContain('▼') - }) - - it('при 6 шагах НЕ рендерится горизонтальный overflow-x-auto', async () => { - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: make6Steps() })) - expect(wrapper.html()).not.toContain('overflow-x-auto') - }) - - it('вертикальный timeline отображает имя роли агента', async () => { - const steps = [ - makeStep(1, 'pm', true), makeStep(2, 'backend_dev', true), makeStep(3, 'frontend_dev', true), - makeStep(4, 'tester', true), makeStep(5, 'reviewer', true), makeStep(6, 'security', true), - ] - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('pm') - expect(wrapper.html()).toContain('backend_dev') - expect(wrapper.html()).toContain('security') - }) - - it('вертикальный timeline отображает duration_seconds', async () => { - const steps = Array.from({ length: 6 }, (_, i) => makeStep(i + 1, 'pm', true, { duration_seconds: 77 })) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('77s') - }) - - it('вертикальный timeline отображает cost_usd', async () => { - const steps = Array.from({ length: 6 }, (_, i) => makeStep(i + 1, 'pm', true, { cost_usd: 0.042 })) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('$0.042') - }) -}) - -// ============================================================================ -// Feature 2: Вертикальный timeline — иконки success/fail/running -// ============================================================================ - -describe('KIN-UI-028 Feature 2: verticalStepIcon и verticalStepIconColor — статическая проверка', () => { - it('функция verticalStepIcon покрывает success === true', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('step.success === true') - }) - - it('функция verticalStepIcon покрывает success === 1 (number)', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('step.success === 1') - }) - - it('функция verticalStepIcon покрывает success === false', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('step.success === false') - }) - - it('функция verticalStepIcon покрывает success === 0 (number)', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('step.success === 0') - }) - - it('функция verticalStepIconColor возвращает text-green-400 для успеха', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('text-green-400') - }) - - it('функция verticalStepIconColor возвращает text-red-400 для провала', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('text-red-400') - }) - - it('функция verticalStepIconColor возвращает text-blue-400 для running/null', () => { - const source = fs.readFileSync(path.resolve(__dirname, '../views/TaskDetail.vue'), 'utf-8') - expect(source).toContain('text-blue-400') - }) -}) - -describe('KIN-UI-028 Feature 2: вертикальный timeline — CSS-классы иконок в рендере', () => { - it('шаги с success=true рендерят text-green-400', async () => { - const steps = Array.from({ length: 6 }, (_, i) => makeStep(i + 1, 'pm', true)) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('text-green-400') - }) - - it('шаги с success=false рендерят text-red-400', async () => { - const steps = Array.from({ length: 6 }, (_, i) => makeStep(i + 1, 'pm', false)) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('text-red-400') - }) - - it('шаги с success=null рендерят text-blue-400', async () => { - const steps = Array.from({ length: 6 }, (_, i) => makeStep(i + 1, 'pm', null)) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('text-blue-400') - }) -}) - -// ============================================================================ -// Feature 2: Вертикальный timeline — chevron и раскрытие шага -// ============================================================================ - -describe('KIN-UI-028 Feature 2: вертикальный timeline — chevron и раскрытие', () => { - it('шаги изначально свёрнуты — chevron ▼ присутствует', async () => { - const steps = Array.from({ length: 6 }, (_, i) => makeStep(i + 1, 'pm', true)) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - expect(wrapper.html()).toContain('▼') - expect(wrapper.html()).not.toContain('▲') - }) - - it('клик по шагу раскрывает его — chevron ▲ появляется', async () => { - const steps = Array.from({ length: 6 }, (_, i) => - makeStep(i + 1, 'pm', true, { output_summary: 'Summary output' }) - ) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - - // Find first row containing ▼ and click it - const allDivs = wrapper.findAll('div') - const stepRow = allDivs.find(d => d.classes().includes('cursor-pointer') && d.text().includes('▼')) - expect(stepRow).toBeDefined() - await stepRow!.trigger('click') - await wrapper.vm.$nextTick() - - expect(wrapper.html()).toContain('▲') - }) - - it('раскрытый шаг показывает содержимое output_summary', async () => { - const steps = Array.from({ length: 6 }, (_, i) => - makeStep(i + 1, 'pm', true, { output_summary: 'UniqueOutputMarker9z' }) - ) - const wrapper = await mountTask(makeTask({ status: 'done', pipeline_steps: steps })) - - const allDivs = wrapper.findAll('div') - const stepRow = allDivs.find(d => d.classes().includes('cursor-pointer') && d.text().includes('▼')) - expect(stepRow).toBeDefined() - await stepRow!.trigger('click') - await wrapper.vm.$nextTick() - - expect(wrapper.html()).toContain('UniqueOutputMarker9z') - }) -}) diff --git a/web/frontend/src/views/ProjectView.vue b/web/frontend/src/views/ProjectView.vue index c1b17f9..81f7f33 100644 --- a/web/frontend/src/views/ProjectView.vue +++ b/web/frontend/src/views/ProjectView.vue @@ -15,8 +15,6 @@ const project = ref(null) const loading = ref(true) const error = ref('') const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links' | 'settings'>('tasks') -const showModeMenu = ref(false) -const showMoreMenu = ref(false) // Phases const phases = ref([]) @@ -174,34 +172,6 @@ function phaseStatusColor(s: string) { return m[s] || 'gray' } -// Tab groups -const PRIMARY_TABS = ['tasks', 'kanban', 'phases', 'decisions'] as const -const MORE_TABS = ['modules', 'environments', 'links', 'settings'] as const - -function tabLabel(tab: string): string { - const labels: Record = { - tasks: t('projectView.tasks_tab'), - phases: t('projectView.phases_tab'), - decisions: t('projectView.decisions_tab'), - modules: t('projectView.modules_tab'), - kanban: t('projectView.kanban_tab'), - environments: t('projectView.environments'), - links: t('projectView.links_tab'), - settings: t('projectView.settings_tab'), - } - return labels[tab] ?? tab -} - -function tabCount(tab: string): string | number { - if (tab === 'tasks' || tab === 'kanban') return project.value!.tasks.length - if (tab === 'phases') return phases.value.length - if (tab === 'decisions') return project.value!.decisions.length - if (tab === 'modules') return project.value!.modules.length - if (tab === 'environments') return environments.value.length - if (tab === 'links') return links.value.length - return '' -} - // Filters const ALL_TASK_STATUSES = ['pending', 'in_progress', 'review', 'blocked', 'decomposed', 'done', 'revising', 'cancelled'] @@ -308,8 +278,6 @@ async function toggleWorktrees() { } } -const anyModeActive = computed(() => autoMode.value || autocommit.value || autoTest.value || worktrees.value) - // Settings form const settingsForm = ref({ execution_mode: 'review', @@ -1104,38 +1072,24 @@ async function addDecision() {
- - -
-
- -
- -
-
@@ -1176,52 +1130,38 @@ async function addDecision() { class="px-1.5 py-0.5 text-xs text-gray-600 hover:text-red-400 rounded">✕
- -
-
- -
- - - - -
-
+ + + +