/** * KIN-120: Тесты SettingsView — навигатор по настройкам проектов * * После рефакторинга SettingsView стал навигатором: * показывает список проектов и ссылки на /project/{id}?tab=settings. * Детальные настройки каждого проекта переехали в ProjectView → вкладка Settings. * * Проверяет: * 1. Загрузка и отображение списка проектов * 2. Имя и id проекта видны * 3. Ссылки ведут на /project/{id}?tab=settings * 4. execution_mode отображается если задан */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createRouter, createMemoryHistory } from 'vue-router' import SettingsView from '../SettingsView.vue' vi.mock('../../api', async (importOriginal) => { const actual = await importOriginal() return { ...actual, api: { projects: vi.fn(), projectLinks: vi.fn(), patchProject: vi.fn(), }, } }) 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 as string | 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, } function makeRouter() { return createRouter({ history: createMemoryHistory(), routes: [ { path: '/settings', component: SettingsView }, { path: '/project/:id', component: { template: '
' } }, ], }) } beforeEach(() => { vi.clearAllMocks() vi.mocked(api.projectLinks).mockResolvedValue([]) vi.mocked(api.patchProject).mockResolvedValue({} as any) }) async function mountSettings(overrides: Partial = {}) { const project = { ...BASE_PROJECT, ...overrides } vi.mocked(api.projects).mockResolvedValue([project as any]) const router = makeRouter() await router.push('/settings') const wrapper = mount(SettingsView, { global: { plugins: [router] } }) await flushPromises() return wrapper } describe('SettingsView — навигатор', () => { it('показывает имя проекта', async () => { const wrapper = await mountSettings() expect(wrapper.text()).toContain('Test Project') }) it('показывает id проекта', async () => { const wrapper = await mountSettings() expect(wrapper.text()).toContain('proj-1') }) it('содержит ссылку на страницу настроек проекта', async () => { const wrapper = await mountSettings() const links = wrapper.findAll('a') expect(links.length).toBeGreaterThan(0) const settingsLink = links.find(l => l.attributes('href')?.includes('proj-1')) expect(settingsLink?.exists()).toBe(true) expect(settingsLink?.attributes('href')).toContain('settings') }) it('ссылка ведёт на /project/{id} с tab=settings', async () => { const wrapper = await mountSettings() const link = wrapper.find('a[href*="proj-1"]') expect(link.exists()).toBe(true) expect(link.attributes('href')).toMatch(/\/project\/proj-1/) expect(link.attributes('href')).toContain('settings') }) it('показывает execution_mode если задан', async () => { const wrapper = await mountSettings({ execution_mode: 'auto_complete' }) expect(wrapper.text()).toContain('auto_complete') }) it('не показывает execution_mode если null', async () => { const wrapper = await mountSettings({ execution_mode: null }) expect(wrapper.text()).not.toContain('auto_complete') }) }) // --- KIN-120: Isolation and field presence tests --- async function mountSettingsMultiple(projects: Partial[]) { vi.mocked(api.projects).mockResolvedValue(projects as any[]) const router = makeRouter() await router.push('/settings') const wrapper = mount(SettingsView, { global: { plugins: [router] } }) await flushPromises() return wrapper } describe('SettingsView — изоляция настроек проектов', () => { it('obsidian_vault_path proj-1 и proj-2 независимы', async () => { const proj1 = { ...BASE_PROJECT, id: 'proj-1', obsidian_vault_path: '/vault/proj1' } const proj2 = { ...BASE_PROJECT, id: 'proj-2', name: 'Second Project', obsidian_vault_path: '/vault/proj2' } const wrapper = await mountSettingsMultiple([proj1, proj2]) const inputs = wrapper.findAll('input[placeholder="/path/to/obsidian/vault"]') expect(inputs).toHaveLength(2) expect((inputs[0].element as HTMLInputElement).value).toBe('/vault/proj1') expect((inputs[1].element as HTMLInputElement).value).toBe('/vault/proj2') }) it('test_command proj-1 не перекрывает test_command proj-2', async () => { const proj1 = { ...BASE_PROJECT, id: 'proj-1', test_command: 'make test' } const proj2 = { ...BASE_PROJECT, id: 'proj-2', name: 'Second Project', test_command: 'npm test' } const wrapper = await mountSettingsMultiple([proj1, proj2]) const inputs = wrapper.findAll('input[placeholder="make test"]') expect(inputs).toHaveLength(2) expect((inputs[0].element as HTMLInputElement).value).toBe('make test') expect((inputs[1].element as HTMLInputElement).value).toBe('npm test') }) it('deploy_host proj-1 не перекрывает deploy_host proj-2', async () => { const proj1 = { ...BASE_PROJECT, id: 'proj-1', deploy_host: 'server-a' } const proj2 = { ...BASE_PROJECT, id: 'proj-2', name: 'Second Project', deploy_host: 'server-b' } const wrapper = await mountSettingsMultiple([proj1, proj2]) const inputs = wrapper.findAll('input[placeholder="server host (e.g. vdp-prod)"]') expect(inputs).toHaveLength(2) expect((inputs[0].element as HTMLInputElement).value).toBe('server-a') expect((inputs[1].element as HTMLInputElement).value).toBe('server-b') }) }) describe('SettingsView — наличие полей настроек', () => { it('показывает поле obsidian_vault_path', async () => { const wrapper = await mountSettings({ obsidian_vault_path: '/vault/test' }) const input = wrapper.find('input[placeholder="/path/to/obsidian/vault"]') expect(input.exists()).toBe(true) expect((input.element as HTMLInputElement).value).toBe('/vault/test') }) it('показывает поле test_command с корректным значением', async () => { const wrapper = await mountSettings({ test_command: 'pytest tests/' }) const input = wrapper.find('input[placeholder="make test"]') expect(input.exists()).toBe(true) expect((input.element as HTMLInputElement).value).toBe('pytest tests/') }) it('показывает поле deploy_host', async () => { const wrapper = await mountSettings({ deploy_host: 'my-server' }) const input = wrapper.find('input[placeholder="server host (e.g. vdp-prod)"]') expect(input.exists()).toBe(true) expect((input.element as HTMLInputElement).value).toBe('my-server') }) it('показывает поле deploy_path', async () => { const wrapper = await mountSettings({ deploy_path: '/srv/app' }) const input = wrapper.find('input[placeholder="/srv/myproject"]') expect(input.exists()).toBe(true) expect((input.element as HTMLInputElement).value).toBe('/srv/app') }) })