2026-03-17 20:32:49 +02:00
|
|
|
|
/**
|
2026-03-18 14:30:36 +02:00
|
|
|
|
* KIN-120: Тесты SettingsView — навигатор по настройкам проектов
|
|
|
|
|
|
*
|
|
|
|
|
|
* После рефакторинга SettingsView стал навигатором:
|
|
|
|
|
|
* показывает список проектов и ссылки на /project/{id}?tab=settings.
|
|
|
|
|
|
* Детальные настройки каждого проекта переехали в ProjectView → вкладка Settings.
|
2026-03-17 20:32:49 +02:00
|
|
|
|
*
|
|
|
|
|
|
* Проверяет:
|
2026-03-18 14:30:36 +02:00
|
|
|
|
* 1. Загрузка и отображение списка проектов
|
|
|
|
|
|
* 2. Имя и id проекта видны
|
|
|
|
|
|
* 3. Ссылки ведут на /project/{id}?tab=settings
|
|
|
|
|
|
* 4. execution_mode отображается если задан
|
2026-03-17 20:32:49 +02:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
2026-03-18 14:30:36 +02:00
|
|
|
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
2026-03-17 20:32:49 +02:00
|
|
|
|
import SettingsView from '../SettingsView.vue'
|
|
|
|
|
|
|
|
|
|
|
|
vi.mock('../../api', async (importOriginal) => {
|
|
|
|
|
|
const actual = await importOriginal<typeof import('../../api')>()
|
|
|
|
|
|
return {
|
|
|
|
|
|
...actual,
|
|
|
|
|
|
api: {
|
|
|
|
|
|
projects: vi.fn(),
|
2026-03-18 15:22:17 +02:00
|
|
|
|
projectLinks: vi.fn(),
|
|
|
|
|
|
patchProject: vi.fn(),
|
2026-03-17 20:32:49 +02:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
import { api } from '../../api'
|
|
|
|
|
|
|
|
|
|
|
|
const BASE_PROJECT = {
|
|
|
|
|
|
id: 'proj-1',
|
|
|
|
|
|
name: 'Test Project',
|
|
|
|
|
|
path: '/projects/test',
|
|
|
|
|
|
status: 'active',
|
|
|
|
|
|
priority: 5,
|
|
|
|
|
|
tech_stack: ['python'],
|
2026-03-18 14:30:36 +02:00
|
|
|
|
execution_mode: null as string | null,
|
2026-03-17 20:32:49 +02:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
function makeRouter() {
|
|
|
|
|
|
return createRouter({
|
|
|
|
|
|
history: createMemoryHistory(),
|
|
|
|
|
|
routes: [
|
|
|
|
|
|
{ path: '/settings', component: SettingsView },
|
|
|
|
|
|
{ path: '/project/:id', component: { template: '<div />' } },
|
|
|
|
|
|
],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 20:32:49 +02:00
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.clearAllMocks()
|
2026-03-18 15:22:17 +02:00
|
|
|
|
vi.mocked(api.projectLinks).mockResolvedValue([])
|
|
|
|
|
|
vi.mocked(api.patchProject).mockResolvedValue({} as any)
|
2026-03-17 20:32:49 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async function mountSettings(overrides: Partial<typeof BASE_PROJECT> = {}) {
|
|
|
|
|
|
const project = { ...BASE_PROJECT, ...overrides }
|
|
|
|
|
|
vi.mocked(api.projects).mockResolvedValue([project as any])
|
2026-03-18 14:30:36 +02:00
|
|
|
|
const router = makeRouter()
|
|
|
|
|
|
await router.push('/settings')
|
|
|
|
|
|
const wrapper = mount(SettingsView, { global: { plugins: [router] } })
|
2026-03-17 20:32:49 +02:00
|
|
|
|
await flushPromises()
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
describe('SettingsView — навигатор', () => {
|
|
|
|
|
|
it('показывает имя проекта', async () => {
|
|
|
|
|
|
const wrapper = await mountSettings()
|
|
|
|
|
|
expect(wrapper.text()).toContain('Test Project')
|
2026-03-17 20:32:49 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
it('показывает id проекта', async () => {
|
|
|
|
|
|
const wrapper = await mountSettings()
|
|
|
|
|
|
expect(wrapper.text()).toContain('proj-1')
|
2026-03-17 20:32:49 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
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')
|
2026-03-17 20:32:49 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
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')
|
2026-03-17 20:32:49 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
it('показывает execution_mode если задан', async () => {
|
|
|
|
|
|
const wrapper = await mountSettings({ execution_mode: 'auto_complete' })
|
|
|
|
|
|
expect(wrapper.text()).toContain('auto_complete')
|
2026-03-17 20:32:49 +02:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-18 14:30:36 +02:00
|
|
|
|
it('не показывает execution_mode если null', async () => {
|
|
|
|
|
|
const wrapper = await mountSettings({ execution_mode: null })
|
|
|
|
|
|
expect(wrapper.text()).not.toContain('auto_complete')
|
2026-03-17 20:32:49 +02:00
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-03-18 15:22:17 +02:00
|
|
|
|
|
|
|
|
|
|
// --- KIN-120: Isolation and field presence tests ---
|
|
|
|
|
|
|
|
|
|
|
|
async function mountSettingsMultiple(projects: Partial<typeof BASE_PROJECT>[]) {
|
|
|
|
|
|
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')
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|