diff --git a/web/frontend/src/__tests__/execution-mode-unification.test.ts b/web/frontend/src/__tests__/execution-mode-unification.test.ts index 25ea209..d968531 100644 --- a/web/frontend/src/__tests__/execution-mode-unification.test.ts +++ b/web/frontend/src/__tests__/execution-mode-unification.test.ts @@ -176,13 +176,14 @@ describe('KIN-FIX-002: execution_mode унификация на "auto_complete"' }) await flushPromises() - // Найти и кликнуть кнопку тоггла режима - const toggleBtn = wrapper.findAll('button').find(b => - b.text().includes('Auto') || b.text().includes('Review') - ) + // Открываем ⚙ Mode меню и кликаем по кнопке авто/ревью + const trigger = wrapper.find('[data-testid="mode-menu-trigger"]') + if (trigger.exists()) { + await trigger.trigger('click') + await flushPromises() - if (toggleBtn) { - await toggleBtn.trigger('click') + const autoBtn = wrapper.find('[data-testid="mode-toggle-auto"]') + await autoBtn.trigger('click') await flushPromises() // Проверяем, что localStorage содержит 'auto_complete', не 'auto' @@ -547,11 +548,10 @@ describe('KIN-097: runTask синхронизирует execution_mode с тог }) await flushPromises() - // Тоггл должен показывать Auto - const toggleBtn = wrapper.findAll('button').find(b => - b.text().includes('Auto') || b.text().includes('Review') - ) - expect(toggleBtn!.text()).toContain('Auto') + // Триггер ⚙ 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') // DB переключается на review (например, другой клиент изменил режим) vi.mocked(api.project).mockResolvedValue( @@ -561,17 +561,14 @@ describe('KIN-097: runTask синхронизирует execution_mode с тог // После load() тоггл должен обновиться на Review // Имитируем внешний load (например, после создания задачи) vi.mocked(api.patchProject).mockResolvedValue({ execution_mode: 'review' } as any) - // Триггерим reload через toggleAutocommit (который вызывает patchProject, но не load) - // Вместо этого напрямую проверим что при новом mount с review — кнопка Review + // Вместо этого напрямую проверим что при новом mount с review — data-mode="review" const wrapper2 = mount(ProjectView, { props: { id: 'KIN' }, global: { plugins: [router] }, }) await flushPromises() - const toggleBtn2 = wrapper2.findAll('button').find(b => - b.text().includes('Auto') || b.text().includes('Review') - ) - expect(toggleBtn2!.text()).toContain('Review') + const trigger2 = wrapper2.find('[data-testid="mode-menu-trigger"]') + expect(trigger2.attributes('data-mode')).toBe('review') }) }) diff --git a/web/frontend/src/__tests__/filter-persistence.test.ts b/web/frontend/src/__tests__/filter-persistence.test.ts index b313950..7f0e95c 100644 --- a/web/frontend/src/__tests__/filter-persistence.test.ts +++ b/web/frontend/src/__tests__/filter-persistence.test.ts @@ -517,7 +517,14 @@ describe('KIN-047: TaskDetail — Approve/Reject в статусе review', () = // ───────────────────────────────────────────────────────────── describe('KIN-065: ProjectView — Autocommit toggle', () => { - it('Кнопка Autocommit присутствует в DOM', async () => { + 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 () => { const router = makeRouter() await router.push('/project/KIN') @@ -527,8 +534,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) - expect(btn?.exists()).toBe(true) + const btn = await openModeMenu(wrapper) + expect(btn.exists()).toBe(true) }) it('Кнопка имеет title "Autocommit: off" когда autocommit_enabled=0', async () => { @@ -542,8 +549,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) - expect(btn?.attributes('title')).toBe('Autocommit: off') + const btn = await openModeMenu(wrapper) + expect(btn.attributes('title')).toBe('Autocommit: off') }) it('Кнопка имеет title "Autocommit: on..." когда autocommit_enabled=1', async () => { @@ -557,8 +564,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) - expect(btn?.attributes('title')).toContain('Autocommit: on') + const btn = await openModeMenu(wrapper) + expect(btn.attributes('title')).toContain('Autocommit: on') }) it('Клик по кнопке вызывает patchProject с autocommit_enabled=true (включение)', async () => { @@ -574,8 +581,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) - await btn!.trigger('click') + const btn = await openModeMenu(wrapper) + await btn.trigger('click') await flushPromises() expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: true }) @@ -594,8 +601,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) - await btn!.trigger('click') + const btn = await openModeMenu(wrapper) + await btn.trigger('click') await flushPromises() expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: false }) @@ -616,8 +623,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { }) await flushPromises() - const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) - await btn!.trigger('click') + const btn = await openModeMenu(wrapper) + await btn.trigger('click') await flushPromises() // Catch-блок установил error.value → компонент показывает сообщение об ошибке diff --git a/web/frontend/src/views/ProjectView.vue b/web/frontend/src/views/ProjectView.vue index 81f7f33..c1b17f9 100644 --- a/web/frontend/src/views/ProjectView.vue +++ b/web/frontend/src/views/ProjectView.vue @@ -15,6 +15,8 @@ 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([]) @@ -172,6 +174,34 @@ 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'] @@ -278,6 +308,8 @@ async function toggleWorktrees() { } } +const anyModeActive = computed(() => autoMode.value || autocommit.value || autoTest.value || worktrees.value) + // Settings form const settingsForm = ref({ execution_mode: 'review', @@ -1072,24 +1104,38 @@ async function addDecision() {
- + +
+
+ +
+ +
+
@@ -1130,38 +1176,52 @@ async function addDecision() { class="px-1.5 py-0.5 text-xs text-gray-600 hover:text-red-400 rounded">✕
- - - - + +
+
+ +
+ + + + +
+