kin: auto-commit after pipeline
This commit is contained in:
parent
79ff03ab3d
commit
477fc68cd3
1 changed files with 455 additions and 0 deletions
|
|
@ -0,0 +1,455 @@
|
||||||
|
/**
|
||||||
|
* 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue