204 lines
8.8 KiB
TypeScript
204 lines
8.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* KIN-103: Тесты worktrees_enabled toggle в SettingsView
|
|||
|
|
*
|
|||
|
|
* Проверяет:
|
|||
|
|
* 1. Интерфейс Project содержит worktrees_enabled: number | null
|
|||
|
|
* 2. patchProject принимает worktrees_enabled?: boolean
|
|||
|
|
* 3. Инициализацию из числовых значений (0, 1, null) → !!()
|
|||
|
|
* 4. Toggle вызывает PATCH с true/false
|
|||
|
|
* 5. Откат при ошибке PATCH
|
|||
|
|
* 6. Checkbox disabled пока идёт сохранение
|
|||
|
|
* 7. Статусные сообщения (Saved / Error)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|||
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|||
|
|
import SettingsView from '../SettingsView.vue'
|
|||
|
|
|
|||
|
|
vi.mock('../../api', async (importOriginal) => {
|
|||
|
|
const actual = await importOriginal<typeof import('../../api')>()
|
|||
|
|
return {
|
|||
|
|
...actual,
|
|||
|
|
api: {
|
|||
|
|
projects: vi.fn(),
|
|||
|
|
patchProject: vi.fn(),
|
|||
|
|
syncObsidian: vi.fn(),
|
|||
|
|
projectLinks: vi.fn().mockResolvedValue([]),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
import { api } from '../../api'
|
|||
|
|
|
|||
|
|
const BASE_PROJECT = {
|
|||
|
|
id: 'proj-1',
|
|||
|
|
name: 'Test Project',
|
|||
|
|
path: '/projects/test',
|
|||
|
|
status: 'active',
|
|||
|
|
priority: 5,
|
|||
|
|
tech_stack: ['python'],
|
|||
|
|
execution_mode: null,
|
|||
|
|
autocommit_enabled: null,
|
|||
|
|
auto_test_enabled: null,
|
|||
|
|
worktrees_enabled: null as number | null,
|
|||
|
|
obsidian_vault_path: null,
|
|||
|
|
deploy_command: null,
|
|||
|
|
test_command: null,
|
|||
|
|
deploy_host: null,
|
|||
|
|
deploy_path: null,
|
|||
|
|
deploy_runtime: null,
|
|||
|
|
deploy_restart_cmd: null,
|
|||
|
|
created_at: '2024-01-01',
|
|||
|
|
total_tasks: 0,
|
|||
|
|
done_tasks: 0,
|
|||
|
|
active_tasks: 0,
|
|||
|
|
blocked_tasks: 0,
|
|||
|
|
review_tasks: 0,
|
|||
|
|
project_type: null,
|
|||
|
|
ssh_host: null,
|
|||
|
|
ssh_user: null,
|
|||
|
|
ssh_key_path: null,
|
|||
|
|
ssh_proxy_jump: null,
|
|||
|
|
description: null,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
vi.clearAllMocks()
|
|||
|
|
vi.mocked(api.patchProject).mockResolvedValue(BASE_PROJECT as any)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async function mountSettings(overrides: Partial<typeof BASE_PROJECT> = {}) {
|
|||
|
|
const project = { ...BASE_PROJECT, ...overrides }
|
|||
|
|
vi.mocked(api.projects).mockResolvedValue([project as any])
|
|||
|
|
const wrapper = mount(SettingsView)
|
|||
|
|
await flushPromises()
|
|||
|
|
return wrapper
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findWorktreesCheckbox(wrapper: ReturnType<typeof mount>) {
|
|||
|
|
const labels = wrapper.findAll('label')
|
|||
|
|
const worktreesLabel = labels.find(l => l.text().includes('Worktrees'))
|
|||
|
|
const checkbox = worktreesLabel?.find('input[type="checkbox"]')
|
|||
|
|
// decision #544: assertion безусловная — ложный зелёный недопустим
|
|||
|
|
expect(checkbox?.exists()).toBe(true)
|
|||
|
|
return checkbox!
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 1. Инициализация из числовых значений
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('worktreesEnabled — инициализация', () => {
|
|||
|
|
it('worktrees_enabled=1 → checkbox checked', async () => {
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 1 })
|
|||
|
|
const checkbox = findWorktreesCheckbox(wrapper)
|
|||
|
|
expect((checkbox.element as HTMLInputElement).checked).toBe(true)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('worktrees_enabled=0 → checkbox unchecked', async () => {
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 0 })
|
|||
|
|
const checkbox = findWorktreesCheckbox(wrapper)
|
|||
|
|
expect((checkbox.element as HTMLInputElement).checked).toBe(false)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('worktrees_enabled=null → checkbox unchecked', async () => {
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: null })
|
|||
|
|
const checkbox = findWorktreesCheckbox(wrapper)
|
|||
|
|
expect((checkbox.element as HTMLInputElement).checked).toBe(false)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 2. Toggle → patchProject вызывается с корректным значением
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('toggleWorktrees — вызов patchProject', () => {
|
|||
|
|
it('toggle с unchecked → patchProject({ worktrees_enabled: true })', async () => {
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 0 })
|
|||
|
|
await findWorktreesCheckbox(wrapper).trigger('change')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(vi.mocked(api.patchProject)).toHaveBeenCalledWith(
|
|||
|
|
'proj-1',
|
|||
|
|
expect.objectContaining({ worktrees_enabled: true }),
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('toggle с checked → patchProject({ worktrees_enabled: false }), не undefined (decision #524)', async () => {
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 1 })
|
|||
|
|
await findWorktreesCheckbox(wrapper).trigger('change')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
const payload = vi.mocked(api.patchProject).mock.calls[0][1] as any
|
|||
|
|
expect(payload.worktrees_enabled).toBe(false)
|
|||
|
|
expect(payload.worktrees_enabled).not.toBeUndefined()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 3. Откат при ошибке PATCH
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('toggleWorktrees — откат при ошибке PATCH', () => {
|
|||
|
|
it('ошибка при включении: checkbox откатывается обратно к unchecked', async () => {
|
|||
|
|
vi.mocked(api.patchProject).mockRejectedValueOnce(new Error('network error'))
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 0 })
|
|||
|
|
await findWorktreesCheckbox(wrapper).trigger('change')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect((findWorktreesCheckbox(wrapper).element as HTMLInputElement).checked).toBe(false)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('ошибка при выключении: checkbox откатывается обратно к checked', async () => {
|
|||
|
|
vi.mocked(api.patchProject).mockRejectedValueOnce(new Error('server error'))
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 1 })
|
|||
|
|
await findWorktreesCheckbox(wrapper).trigger('change')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect((findWorktreesCheckbox(wrapper).element as HTMLInputElement).checked).toBe(true)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 4. Disabled во время сохранения
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('toggleWorktrees — disabled во время сохранения', () => {
|
|||
|
|
it('checkbox disabled пока идёт PATCH, enabled после завершения', async () => {
|
|||
|
|
let resolveRequest!: (v: any) => void
|
|||
|
|
vi.mocked(api.patchProject).mockImplementationOnce(
|
|||
|
|
() => new Promise(resolve => { resolveRequest = resolve }),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 0 })
|
|||
|
|
const checkbox = findWorktreesCheckbox(wrapper)
|
|||
|
|
await checkbox.trigger('change')
|
|||
|
|
|
|||
|
|
// savingWorktrees = true → checkbox должен быть disabled
|
|||
|
|
expect((checkbox.element as HTMLInputElement).disabled).toBe(true)
|
|||
|
|
|
|||
|
|
resolveRequest(BASE_PROJECT)
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect((checkbox.element as HTMLInputElement).disabled).toBe(false)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 5. Статусные сообщения
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('toggleWorktrees — статусное сообщение', () => {
|
|||
|
|
it('успех → показывает "Saved"', async () => {
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 0 })
|
|||
|
|
await findWorktreesCheckbox(wrapper).trigger('change')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(wrapper.text()).toContain('Saved')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('ошибка → показывает "Error: <message>"', async () => {
|
|||
|
|
vi.mocked(api.patchProject).mockRejectedValueOnce(new Error('connection timeout'))
|
|||
|
|
const wrapper = await mountSettings({ worktrees_enabled: 0 })
|
|||
|
|
await findWorktreesCheckbox(wrapper).trigger('change')
|
|||
|
|
await flushPromises()
|
|||
|
|
|
|||
|
|
expect(wrapper.text()).toContain('Error: connection timeout')
|
|||
|
|
})
|
|||
|
|
})
|