kin: KIN-078 Канбан доска не отображается в в полную ширину экрана. Проверить был ли вызван хук перезагрузки после выполнения задачи.

This commit is contained in:
Gros Frumos 2026-03-16 10:59:09 +02:00
parent c14c0b7832
commit cc592bfbbc
2 changed files with 153 additions and 2 deletions

View file

@ -646,3 +646,153 @@ describe('KIN-075: канбан — кнопки управления', () => {
expect(wrapper.text()).toContain('Add Task') expect(wrapper.text()).toContain('Add Task')
}) })
}) })
// ─────────────────────────────────────────────────────────────
// KIN-078: полная ширина канбан-доски (нет max-w ограничений)
// ─────────────────────────────────────────────────────────────
describe('KIN-078: канбан — flex layout без ограничений ширины', () => {
it('Flex-контейнер колонок имеет класс w-full', async () => {
const wrapper = await mountOnKanban()
const flexContainer = wrapper.find('.flex.gap-3.w-full')
expect(flexContainer.exists(), 'flex gap-3 w-full контейнер должен существовать').toBe(true)
expect(flexContainer.classes()).toContain('w-full')
})
it('Flex-контейнер колонок не содержит inline style min-width: max-content', async () => {
const wrapper = await mountOnKanban()
const flexContainer = wrapper.find('.flex.gap-3.w-full')
const style = flexContainer.element.getAttribute('style')
expect(style ?? '').not.toContain('min-width')
})
it('Каждая из 5 колонок имеет flex-1 (растягивается), а не фиксированный w-64', async () => {
const wrapper = await mountOnKanban()
// KANBAN_COLUMNS — 5 колонок, все должны иметь flex-1
const allFlex1 = wrapper.findAll('div').filter(d => d.classes().includes('flex-1') && d.classes().includes('flex-col'))
expect(allFlex1.length, '5 колонок с flex-1 flex-col должны быть').toBe(5)
for (const col of allFlex1) {
expect(col.classes(), 'Колонка не должна иметь фиксированный w-64').not.toContain('w-64')
}
})
it('Каждая из 5 колонок имеет min-w-[12rem] (минимальная ширина)', async () => {
const wrapper = await mountOnKanban()
const columns = wrapper.findAll('div').filter(d =>
d.classes().includes('flex-1') && d.classes().includes('flex-col')
)
expect(columns.length).toBe(5)
for (const col of columns) {
expect(col.classes(), 'Колонка должна иметь min-w-[12rem]').toContain('min-w-[12rem]')
}
})
})
// ─────────────────────────────────────────────────────────────
// KIN-078: runTask → запуск polling на kanban-вкладке
// ─────────────────────────────────────────────────────────────
describe('KIN-078: runTask → polling на kanban-вкладке', () => {
it('runTask запускает polling если пользователь переключился на kanban пока задача выполнялась', async () => {
vi.useFakeTimers()
// Изначально нет in_progress задач → переключение на kanban не запускает polling
const projectNoPending = {
...MOCK_PROJECT,
tasks: MOCK_PROJECT.tasks.filter(t => t.status !== 'in_progress'),
}
vi.mocked(api.project).mockResolvedValue(projectNoPending as any)
// Откладываем api.runTask — имитируем задержку
let resolveRun!: () => void
vi.mocked(api.runTask).mockReturnValue(new Promise<void>(res => { resolveRun = () => res() }) as any)
vi.spyOn(window, 'confirm').mockReturnValue(true)
const router = makeRouter()
await router.push('/project/KIN')
const wrapper = mount(ProjectView, {
props: { id: 'KIN' },
global: { plugins: [router] },
})
await flushPromises()
// Кликаем ▶ для KIN-001 (pending) на вкладке Tasks — runTask подвисает на await api.runTask
const runBtn = wrapper.find('button[title="Run pipeline"]')
expect(runBtn.exists(), '▶ кнопка должна быть на Tasks вкладке').toBe(true)
await runBtn.trigger('click')
// Пока runTask ждёт — переключаемся на kanban (нет in_progress → polling не стартует)
const kanbanTab = wrapper.findAll('button').find(b =>
b.classes().includes('border-b-2') && b.text().includes('Kanban')
)!
await kanbanTab.trigger('click')
await flushPromises()
// Убеждаемся что polling не запустился (нет in_progress задач)
await vi.advanceTimersByTimeAsync(5000)
await flushPromises()
const callsBefore = vi.mocked(api.project).mock.calls.length
// Настраиваем следующий load() чтобы вернуть проект с in_progress задачами
vi.mocked(api.project).mockResolvedValue(MOCK_PROJECT as any)
// Завершаем api.runTask → runTask продолжает: load() → checkAndPollKanban()
resolveRun()
await flushPromises()
const callsAfterLoad = vi.mocked(api.project).mock.calls.length
expect(callsAfterLoad, 'load() должен вызвать api.project').toBeGreaterThan(callsBefore)
// Продвигаем время на 5с → polling-тик должен сработать
await vi.advanceTimersByTimeAsync(5000)
await flushPromises()
expect(
vi.mocked(api.project).mock.calls.length,
'Polling должен запуститься после runTask когда activeTab === kanban',
).toBeGreaterThan(callsAfterLoad)
})
it('runTask не запускает polling если activeTab !== kanban в момент завершения', async () => {
vi.useFakeTimers()
vi.mocked(api.runTask).mockResolvedValue(undefined as any)
vi.spyOn(window, 'confirm').mockReturnValue(true)
// Нет in_progress задач → ни через watcher, ни через runTask polling не стартует
const projectNoPending = {
...MOCK_PROJECT,
tasks: MOCK_PROJECT.tasks.filter(t => t.status !== 'in_progress'),
}
vi.mocked(api.project).mockResolvedValue(projectNoPending as any)
const router = makeRouter()
await router.push('/project/KIN')
const wrapper = mount(ProjectView, {
props: { id: 'KIN' },
global: { plugins: [router] },
})
await flushPromises()
// Остаёмся на Tasks вкладке (activeTab === 'tasks') и кликаем ▶
const runBtn = wrapper.find('button[title="Run pipeline"]')
await runBtn.trigger('click')
await flushPromises()
const callsAfterRun = vi.mocked(api.project).mock.calls.length
// Продвигаем время — polling не должен запуститься (мы на tasks, нет in_progress)
await vi.advanceTimersByTimeAsync(5000)
await flushPromises()
expect(
vi.mocked(api.project).mock.calls.length,
'Polling не должен запуститься когда activeTab !== kanban',
).toBe(callsAfterRun)
})
})

View file

@ -385,6 +385,7 @@ async function runTask(taskId: string, event: Event) {
try { try {
await api.runTask(taskId) await api.runTask(taskId)
await load() await load()
if (activeTab.value === 'kanban') checkAndPollKanban()
} catch (e: any) { } catch (e: any) {
error.value = e.message error.value = e.message
} }
@ -883,8 +884,8 @@ async function addDecision() {
</div> </div>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<div class="flex gap-3" style="min-width: max-content"> <div class="flex gap-3 w-full">
<div v-for="col in KANBAN_COLUMNS" :key="col.status" class="w-64 flex flex-col gap-2"> <div v-for="col in KANBAN_COLUMNS" :key="col.status" class="flex-1 min-w-[12rem] flex flex-col gap-2">
<!-- Column header --> <!-- Column header -->
<div class="flex items-center gap-2 px-2 py-1.5"> <div class="flex items-center gap-2 px-2 py-1.5">
<span class="text-xs font-semibold uppercase tracking-wide" :class="col.headerClass">{{ col.label }}</span> <span class="text-xs font-semibold uppercase tracking-wide" :class="col.headerClass">{{ col.label }}</span>