/** * 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() 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 = {}) { 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) { 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: "', 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') }) })