Compare commits
No commits in common. "477fc68cd3002bffb30cdc627fcd300f9611c26c" and "993a8447d2c021b8cc532904c8b2f0cf6dff7a4a" have entirely different histories.
477fc68cd3
...
993a8447d2
4 changed files with 74 additions and 593 deletions
|
|
@ -176,14 +176,13 @@ describe('KIN-FIX-002: execution_mode унификация на "auto_complete"'
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Открываем ⚙ Mode меню и кликаем по кнопке авто/ревью
|
// Найти и кликнуть кнопку тоггла режима
|
||||||
const trigger = wrapper.find('[data-testid="mode-menu-trigger"]')
|
const toggleBtn = wrapper.findAll('button').find(b =>
|
||||||
if (trigger.exists()) {
|
b.text().includes('Auto') || b.text().includes('Review')
|
||||||
await trigger.trigger('click')
|
)
|
||||||
await flushPromises()
|
|
||||||
|
|
||||||
const autoBtn = wrapper.find('[data-testid="mode-toggle-auto"]')
|
if (toggleBtn) {
|
||||||
await autoBtn.trigger('click')
|
await toggleBtn.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Проверяем, что localStorage содержит 'auto_complete', не 'auto'
|
// Проверяем, что localStorage содержит 'auto_complete', не 'auto'
|
||||||
|
|
@ -548,10 +547,11 @@ describe('KIN-097: runTask синхронизирует execution_mode с тог
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Триггер ⚙ Mode должен иметь data-mode="auto" при execution_mode=auto_complete
|
// Тоггл должен показывать Auto
|
||||||
const trigger = wrapper.find('[data-testid="mode-menu-trigger"]')
|
const toggleBtn = wrapper.findAll('button').find(b =>
|
||||||
expect(trigger.exists()).toBe(true)
|
b.text().includes('Auto') || b.text().includes('Review')
|
||||||
expect(trigger.attributes('data-mode')).toBe('auto')
|
)
|
||||||
|
expect(toggleBtn!.text()).toContain('Auto')
|
||||||
|
|
||||||
// DB переключается на review (например, другой клиент изменил режим)
|
// DB переключается на review (например, другой клиент изменил режим)
|
||||||
vi.mocked(api.project).mockResolvedValue(
|
vi.mocked(api.project).mockResolvedValue(
|
||||||
|
|
@ -561,14 +561,17 @@ describe('KIN-097: runTask синхронизирует execution_mode с тог
|
||||||
// После load() тоггл должен обновиться на Review
|
// После load() тоггл должен обновиться на Review
|
||||||
// Имитируем внешний load (например, после создания задачи)
|
// Имитируем внешний load (например, после создания задачи)
|
||||||
vi.mocked(api.patchProject).mockResolvedValue({ execution_mode: 'review' } as any)
|
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, {
|
const wrapper2 = mount(ProjectView, {
|
||||||
props: { id: 'KIN' },
|
props: { id: 'KIN' },
|
||||||
global: { plugins: [router] },
|
global: { plugins: [router] },
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const trigger2 = wrapper2.find('[data-testid="mode-menu-trigger"]')
|
const toggleBtn2 = wrapper2.findAll('button').find(b =>
|
||||||
expect(trigger2.attributes('data-mode')).toBe('review')
|
b.text().includes('Auto') || b.text().includes('Review')
|
||||||
|
)
|
||||||
|
expect(toggleBtn2!.text()).toContain('Review')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -517,14 +517,7 @@ describe('KIN-047: TaskDetail — Approve/Reject в статусе review', () =
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
||||||
async function openModeMenu(wrapper: ReturnType<typeof mount>) {
|
it('Кнопка Autocommit присутствует в DOM', async () => {
|
||||||
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()
|
const router = makeRouter()
|
||||||
await router.push('/project/KIN')
|
await router.push('/project/KIN')
|
||||||
|
|
||||||
|
|
@ -534,8 +527,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const btn = await openModeMenu(wrapper)
|
const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit'))
|
||||||
expect(btn.exists()).toBe(true)
|
expect(btn?.exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Кнопка имеет title "Autocommit: off" когда autocommit_enabled=0', async () => {
|
it('Кнопка имеет title "Autocommit: off" когда autocommit_enabled=0', async () => {
|
||||||
|
|
@ -549,8 +542,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const btn = await openModeMenu(wrapper)
|
const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit'))
|
||||||
expect(btn.attributes('title')).toBe('Autocommit: off')
|
expect(btn?.attributes('title')).toBe('Autocommit: off')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Кнопка имеет title "Autocommit: on..." когда autocommit_enabled=1', async () => {
|
it('Кнопка имеет title "Autocommit: on..." когда autocommit_enabled=1', async () => {
|
||||||
|
|
@ -564,8 +557,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const btn = await openModeMenu(wrapper)
|
const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit'))
|
||||||
expect(btn.attributes('title')).toContain('Autocommit: on')
|
expect(btn?.attributes('title')).toContain('Autocommit: on')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Клик по кнопке вызывает patchProject с autocommit_enabled=true (включение)', async () => {
|
it('Клик по кнопке вызывает patchProject с autocommit_enabled=true (включение)', async () => {
|
||||||
|
|
@ -581,8 +574,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const btn = await openModeMenu(wrapper)
|
const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit'))
|
||||||
await btn.trigger('click')
|
await btn!.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: true })
|
expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: true })
|
||||||
|
|
@ -601,8 +594,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const btn = await openModeMenu(wrapper)
|
const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit'))
|
||||||
await btn.trigger('click')
|
await btn!.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: false })
|
expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: false })
|
||||||
|
|
@ -623,8 +616,8 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const btn = await openModeMenu(wrapper)
|
const btn = wrapper.findAll('button').find(b => b.text().includes('Autocommit'))
|
||||||
await btn.trigger('click')
|
await btn!.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Catch-блок установил error.value → компонент показывает сообщение об ошибке
|
// Catch-блок установил error.value → компонент показывает сообщение об ошибке
|
||||||
|
|
|
||||||
|
|
@ -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<string, string> = {}
|
|
||||||
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: '<div />' }
|
|
||||||
|
|
||||||
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<string, unknown> = {}): Record<string, unknown> {
|
|
||||||
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<string, unknown> = {}): Record<string, unknown> {
|
|
||||||
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<string, unknown>) {
|
|
||||||
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<string, unknown>).review_required).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('ключ taskDetail.banner_auto_mode присутствует в en.json', () => {
|
|
||||||
expect((enJson.taskDetail as Record<string, unknown>).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<string, unknown>).review_required).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('ключ taskDetail.banner_auto_mode присутствует в ru.json', () => {
|
|
||||||
expect((ruJson.taskDetail as Record<string, unknown>).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<string, unknown>[] {
|
|
||||||
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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -15,8 +15,6 @@ const project = ref<ProjectDetail | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links' | 'settings'>('tasks')
|
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links' | 'settings'>('tasks')
|
||||||
const showModeMenu = ref(false)
|
|
||||||
const showMoreMenu = ref(false)
|
|
||||||
|
|
||||||
// Phases
|
// Phases
|
||||||
const phases = ref<Phase[]>([])
|
const phases = ref<Phase[]>([])
|
||||||
|
|
@ -174,34 +172,6 @@ function phaseStatusColor(s: string) {
|
||||||
return m[s] || 'gray'
|
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<string, string> = {
|
|
||||||
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
|
// Filters
|
||||||
const ALL_TASK_STATUSES = ['pending', 'in_progress', 'review', 'blocked', 'decomposed', 'done', 'revising', 'cancelled']
|
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
|
// Settings form
|
||||||
const settingsForm = ref({
|
const settingsForm = ref({
|
||||||
execution_mode: 'review',
|
execution_mode: 'review',
|
||||||
|
|
@ -1104,38 +1072,24 @@ async function addDecision() {
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="flex gap-1 mb-4 border-b border-gray-800 flex-wrap">
|
<div class="flex gap-1 mb-4 border-b border-gray-800 flex-wrap">
|
||||||
<button v-for="tab in PRIMARY_TABS" :key="tab"
|
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban', 'environments', 'links', 'settings'] as const)" :key="tab"
|
||||||
@click="activeTab = tab"
|
@click="activeTab = tab"
|
||||||
class="px-4 py-2 text-sm border-b-2 transition-colors"
|
class="px-4 py-2 text-sm border-b-2 transition-colors"
|
||||||
:class="activeTab === tab
|
:class="activeTab === tab
|
||||||
? 'text-gray-200 border-blue-500'
|
? 'text-gray-200 border-blue-500'
|
||||||
: 'text-gray-500 border-transparent hover:text-gray-300'">
|
: 'text-gray-500 border-transparent hover:text-gray-300'">
|
||||||
{{ tabLabel(tab) }}
|
{{ tab === 'tasks' ? t('projectView.tasks_tab') : tab === 'phases' ? t('projectView.phases_tab') : tab === 'decisions' ? t('projectView.decisions_tab') : tab === 'modules' ? t('projectView.modules_tab') : tab === 'kanban' ? t('projectView.kanban_tab') : tab === 'environments' ? t('projectView.environments') : tab === 'links' ? t('projectView.links_tab') : t('projectView.settings_tab') }}
|
||||||
<span class="text-xs text-gray-600 ml-1">{{ tabCount(tab) }}</span>
|
<span class="text-xs text-gray-600 ml-1">
|
||||||
|
{{ tab === 'tasks' ? project.tasks.length
|
||||||
|
: tab === 'phases' ? phases.length
|
||||||
|
: tab === 'decisions' ? project.decisions.length
|
||||||
|
: tab === 'modules' ? project.modules.length
|
||||||
|
: tab === 'environments' ? environments.length
|
||||||
|
: tab === 'links' ? links.length
|
||||||
|
: tab === 'kanban' ? project.tasks.length
|
||||||
|
: '' }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<!-- More ▾ dropdown -->
|
|
||||||
<div class="relative">
|
|
||||||
<div v-if="showMoreMenu" class="fixed inset-0 z-10" @click="showMoreMenu = false"></div>
|
|
||||||
<button
|
|
||||||
@click="showMoreMenu = !showMoreMenu"
|
|
||||||
class="px-4 py-2 text-sm border-b-2 transition-colors relative z-20"
|
|
||||||
:class="(MORE_TABS as readonly string[]).includes(activeTab)
|
|
||||||
? 'text-gray-200 border-blue-500'
|
|
||||||
: 'text-gray-500 border-transparent hover:text-gray-300'">
|
|
||||||
More ▾
|
|
||||||
</button>
|
|
||||||
<div v-if="showMoreMenu" class="absolute left-0 top-full z-20 bg-gray-900 border border-gray-700 rounded shadow-lg py-1 min-w-[10rem]">
|
|
||||||
<button v-for="tab in MORE_TABS" :key="tab"
|
|
||||||
@click="activeTab = tab; showMoreMenu = false"
|
|
||||||
class="w-full text-left px-3 py-1.5 text-sm transition-colors flex items-center justify-between"
|
|
||||||
:class="activeTab === tab
|
|
||||||
? 'text-gray-200 bg-gray-800'
|
|
||||||
: 'text-gray-500 hover:text-gray-300 hover:bg-gray-800'">
|
|
||||||
{{ tabLabel(tab) }}
|
|
||||||
<span class="text-xs text-gray-600 ml-2">{{ tabCount(tab) }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tasks Tab -->
|
<!-- Tasks Tab -->
|
||||||
|
|
@ -1176,52 +1130,38 @@ async function addDecision() {
|
||||||
class="px-1.5 py-0.5 text-xs text-gray-600 hover:text-red-400 rounded">✕</button>
|
class="px-1.5 py-0.5 text-xs text-gray-600 hover:text-red-400 rounded">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<!-- ⚙ Mode ▾ dropdown -->
|
<button @click="toggleMode"
|
||||||
<div class="relative">
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
||||||
<div v-if="showModeMenu" class="fixed inset-0 z-10" @click="showModeMenu = false"></div>
|
:class="autoMode
|
||||||
<button
|
? 'bg-yellow-900/30 text-yellow-400 border-yellow-800 hover:bg-yellow-900/50'
|
||||||
data-testid="mode-menu-trigger"
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
||||||
:data-mode="autoMode ? 'auto' : 'review'"
|
|
||||||
@click="showModeMenu = !showModeMenu"
|
|
||||||
class="px-2 py-1 text-xs border rounded relative z-20"
|
|
||||||
:class="anyModeActive ? 'text-yellow-400 border-yellow-800 bg-yellow-900/20' : 'text-gray-400 border-gray-700 bg-gray-800/50'">
|
|
||||||
⚙ Mode ▾
|
|
||||||
</button>
|
|
||||||
<div v-if="showModeMenu" class="absolute right-0 top-full mt-1 z-20 w-52 bg-gray-900 border border-gray-700 rounded shadow-lg py-1">
|
|
||||||
<button
|
|
||||||
data-testid="mode-toggle-auto"
|
|
||||||
@click="toggleMode"
|
|
||||||
class="w-full text-left px-3 py-1.5 text-xs flex items-center justify-between hover:bg-gray-800"
|
|
||||||
:title="autoMode ? 'Auto mode: agents can write files' : 'Review mode: agents read-only'">
|
:title="autoMode ? 'Auto mode: agents can write files' : 'Review mode: agents read-only'">
|
||||||
<span>{{ autoMode ? '🔓 Auto' : '🔒 Review' }}</span>
|
{{ autoMode ? '🔓 Auto' : '🔒 Review' }}
|
||||||
<span :class="autoMode ? 'text-yellow-400' : 'text-gray-600'" class="text-[10px]">{{ autoMode ? 'on' : 'off' }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="toggleAutocommit"
|
||||||
data-testid="mode-toggle-autocommit"
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
||||||
@click="toggleAutocommit"
|
:class="autocommit
|
||||||
class="w-full text-left px-3 py-1.5 text-xs flex items-center justify-between hover:bg-gray-800"
|
? 'bg-green-900/30 text-green-400 border-green-800 hover:bg-green-900/50'
|
||||||
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
||||||
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
:title="autocommit ? 'Autocommit: on — git commit after pipeline' : 'Autocommit: off'">
|
||||||
<span>Autocommit</span>
|
{{ autocommit ? '✓ Autocommit' : 'Autocommit' }}
|
||||||
<span :class="autocommit ? 'text-green-400' : 'text-gray-600'" class="text-[10px]">{{ autocommit ? 'on' : 'off' }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="toggleAutoTest"
|
||||||
data-testid="mode-toggle-autotest"
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
||||||
@click="toggleAutoTest"
|
:class="autoTest
|
||||||
class="w-full text-left px-3 py-1.5 text-xs flex items-center justify-between hover:bg-gray-800"
|
? 'bg-blue-900/30 text-blue-400 border-blue-800 hover:bg-blue-900/50'
|
||||||
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
||||||
:title="autoTest ? 'Auto-test: on — запускать тесты после pipeline' : 'Auto-test: off'">
|
:title="autoTest ? 'Auto-test: on — запускать тесты после pipeline' : 'Auto-test: off'">
|
||||||
<span>{{ t('projectView.auto_test_label') }}</span>
|
{{ autoTest ? '✓ ' + t('projectView.auto_test_label') : t('projectView.auto_test_label') }}
|
||||||
<span :class="autoTest ? 'text-blue-400' : 'text-gray-600'" class="text-[10px]">{{ autoTest ? 'on' : 'off' }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="toggleWorktrees"
|
||||||
data-testid="mode-toggle-worktrees"
|
class="px-2 py-1 text-xs border rounded transition-colors"
|
||||||
@click="toggleWorktrees"
|
:class="worktrees
|
||||||
class="w-full text-left px-3 py-1.5 text-xs flex items-center justify-between hover:bg-gray-800"
|
? 'bg-teal-900/30 text-teal-400 border-teal-800 hover:bg-teal-900/50'
|
||||||
|
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:bg-gray-800'"
|
||||||
:title="worktrees ? 'Worktrees: on' : 'Worktrees: off'">
|
:title="worktrees ? 'Worktrees: on' : 'Worktrees: off'">
|
||||||
<span>{{ worktrees ? t('projectView.worktrees_on') : t('projectView.worktrees_off') }}</span>
|
{{ worktrees ? t('projectView.worktrees_on') : t('projectView.worktrees_off') }}
|
||||||
<span :class="worktrees ? 'text-teal-400' : 'text-gray-600'" class="text-[10px]">{{ worktrees ? 'on' : 'off' }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button @click="runAudit" :disabled="auditLoading"
|
<button @click="runAudit" :disabled="auditLoading"
|
||||||
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
class="px-2 py-1 text-xs bg-purple-900/30 text-purple-400 border border-purple-800 rounded hover:bg-purple-900/50 disabled:opacity-50"
|
||||||
title="Check which pending tasks are already done">
|
title="Check which pending tasks are already done">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue