kin: KIN-UI-012-frontend_dev
This commit is contained in:
parent
d64f5105f4
commit
e014c58709
5 changed files with 63 additions and 1034 deletions
|
|
@ -1,87 +1,14 @@
|
|||
/**
|
||||
* KIN-UI-013: Регрессионный тест — CSS-класс ошибки в SettingsView
|
||||
* KIN-UI-013: Регрессионный тест — статический анализ SettingsView
|
||||
*
|
||||
* До фикса: .startsWith('Error') — хардкод английской строки, ломался при смене локали.
|
||||
* После фикса: .startsWith(t('common.error')) — использует i18n-ключ.
|
||||
*
|
||||
* Проверяет:
|
||||
* 1. Литеральный .startsWith('Error') отсутствует в SettingsView.vue
|
||||
* 2. При ошибке API — статусный span получает CSS-класс text-red-400
|
||||
* 3. При успехе API — статусный span получает CSS-класс text-green-400
|
||||
* 4. Покрывает все 5 статусных полей:
|
||||
* saveStatus, saveTestStatus, saveDeployConfigStatus,
|
||||
* saveAutoTestStatus, saveWorktreesStatus
|
||||
* Проверяет, что хардкод английской строки 'Error' в .startsWith() не используется.
|
||||
* Поведенческие тесты кнопок сохранения удалены вместе с самими формами (KIN-UI-012).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import SettingsView from '../SettingsView.vue'
|
||||
|
||||
vi.mock('../../api', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../api')>()
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
projects: vi.fn(),
|
||||
projectLinks: vi.fn(),
|
||||
patchProject: vi.fn(),
|
||||
syncObsidian: 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,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(api.projectLinks).mockResolvedValue([])
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 1. Статический анализ: .startsWith('Error') не используется
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('SettingsView.vue — статический анализ', () => {
|
||||
it("не содержит .startsWith('Error') (одинарные кавычки)", () => {
|
||||
const filePath = resolve(__dirname, '../SettingsView.vue')
|
||||
|
|
@ -94,123 +21,4 @@ describe('SettingsView.vue — статический анализ', () => {
|
|||
const content = readFileSync(filePath, 'utf-8')
|
||||
expect(content).not.toContain('.startsWith("Error")')
|
||||
})
|
||||
|
||||
it('содержит .startsWith(t(\'common.error\')) — i18n-версию', () => {
|
||||
const filePath = resolve(__dirname, '../SettingsView.vue')
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
expect(content).toContain("startsWith(t('common.error'))")
|
||||
})
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 2. saveVaultPath — CSS-классы при ошибке и успехе
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('SettingsView — saveVaultPath CSS-классы', () => {
|
||||
it('saveStatus: text-red-400 при ошибке API', async () => {
|
||||
vi.mocked(api.patchProject).mockRejectedValue(new Error('network error'))
|
||||
const wrapper = await mountSettings()
|
||||
|
||||
const saveBtn = wrapper.findAll('button').find(b => b.text() === 'Save Vault')
|
||||
expect(saveBtn?.exists()).toBe(true)
|
||||
await saveBtn!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const errorSpan = wrapper.find('.text-red-400')
|
||||
expect(errorSpan.exists()).toBe(true)
|
||||
expect(errorSpan.text()).toMatch(/^Error:/)
|
||||
})
|
||||
|
||||
it('saveStatus: text-green-400 при успехе API', async () => {
|
||||
vi.mocked(api.patchProject).mockResolvedValue({} as any)
|
||||
const wrapper = await mountSettings()
|
||||
|
||||
const saveBtn = wrapper.findAll('button').find(b => b.text() === 'Save Vault')
|
||||
await saveBtn!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const successSpan = wrapper.find('.text-green-400')
|
||||
expect(successSpan.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 3. saveTestCommand — CSS-класс при ошибке
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('SettingsView — saveTestCommand CSS-классы', () => {
|
||||
it('saveTestStatus: text-red-400 при ошибке API', async () => {
|
||||
vi.mocked(api.patchProject).mockRejectedValue(new Error('test save failed'))
|
||||
const wrapper = await mountSettings()
|
||||
|
||||
const saveBtn = wrapper.findAll('button').find(b => b.text() === 'Save Test')
|
||||
expect(saveBtn?.exists()).toBe(true)
|
||||
await saveBtn!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const errorSpan = wrapper.find('.text-red-400')
|
||||
expect(errorSpan.exists()).toBe(true)
|
||||
expect(errorSpan.text()).toMatch(/^Error:/)
|
||||
})
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 4. saveDeployConfig — CSS-класс при ошибке
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('SettingsView — saveDeployConfig CSS-классы', () => {
|
||||
it('saveDeployConfigStatus: text-red-400 при ошибке API', async () => {
|
||||
vi.mocked(api.patchProject).mockRejectedValue(new Error('deploy save failed'))
|
||||
const wrapper = await mountSettings()
|
||||
|
||||
const saveBtn = wrapper.findAll('button').find(b => b.text() === 'Save Deploy Config')
|
||||
expect(saveBtn?.exists()).toBe(true)
|
||||
await saveBtn!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const errorSpan = wrapper.find('.text-red-400')
|
||||
expect(errorSpan.exists()).toBe(true)
|
||||
expect(errorSpan.text()).toMatch(/^Error:/)
|
||||
})
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 5. toggleAutoTest — CSS-класс при ошибке
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('SettingsView — toggleAutoTest CSS-классы', () => {
|
||||
it('saveAutoTestStatus: text-red-400 при ошибке API', async () => {
|
||||
vi.mocked(api.patchProject).mockRejectedValue(new Error('auto-test toggle failed'))
|
||||
const wrapper = await mountSettings()
|
||||
|
||||
const checkbox = wrapper.findAll('input[type="checkbox"]').find((el) => {
|
||||
const label = el.element.closest('label')
|
||||
return label?.textContent?.includes('Auto-test')
|
||||
})
|
||||
expect(checkbox?.exists()).toBe(true)
|
||||
await checkbox!.trigger('change')
|
||||
await flushPromises()
|
||||
|
||||
const errorSpan = wrapper.find('.text-red-400')
|
||||
expect(errorSpan.exists()).toBe(true)
|
||||
expect(errorSpan.text()).toMatch(/^Error:/)
|
||||
})
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 6. toggleWorktrees — CSS-класс при ошибке
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('SettingsView — toggleWorktrees CSS-классы', () => {
|
||||
it('saveWorktreesStatus: text-red-400 при ошибке API', async () => {
|
||||
vi.mocked(api.patchProject).mockRejectedValue(new Error('worktrees toggle failed'))
|
||||
const wrapper = await mountSettings()
|
||||
|
||||
const checkbox = wrapper.findAll('input[type="checkbox"]').find((el) => {
|
||||
const label = el.element.closest('label')
|
||||
return label?.textContent?.includes('Worktrees')
|
||||
})
|
||||
expect(checkbox?.exists()).toBe(true)
|
||||
await checkbox!.trigger('change')
|
||||
await flushPromises()
|
||||
|
||||
const errorSpan = wrapper.find('.text-red-400')
|
||||
expect(errorSpan.exists()).toBe(true)
|
||||
expect(errorSpan.text()).toMatch(/^Error:/)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* KIN-120: Тесты SettingsView — навигатор по настройкам проектов
|
||||
* KIN-UI-012: Тесты SettingsView — навигатор по настройкам проектов
|
||||
*
|
||||
* После рефакторинга SettingsView стал навигатором:
|
||||
* показывает список проектов и ссылки на /project/{id}?tab=settings.
|
||||
|
|
@ -23,8 +23,6 @@ vi.mock('../../api', async (importOriginal) => {
|
|||
...actual,
|
||||
api: {
|
||||
projects: vi.fn(),
|
||||
projectLinks: vi.fn(),
|
||||
patchProject: vi.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -75,8 +73,6 @@ function makeRouter() {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(api.projectLinks).mockResolvedValue([])
|
||||
vi.mocked(api.patchProject).mockResolvedValue({} as any)
|
||||
})
|
||||
|
||||
async function mountSettings(overrides: Partial<typeof BASE_PROJECT> = {}) {
|
||||
|
|
@ -127,76 +123,3 @@ describe('SettingsView — навигатор', () => {
|
|||
expect(wrapper.text()).not.toContain('auto_complete')
|
||||
})
|
||||
})
|
||||
|
||||
// --- 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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue