From e014c587095e471fc5370e44b384d4e9bcebbea6 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Wed, 18 Mar 2026 15:39:54 +0200 Subject: [PATCH] kin: KIN-UI-012-frontend_dev --- .../deploy-config-clear-fields.test.ts | 195 +------- .../src/__tests__/deploy-standardized.test.ts | 171 +------ web/frontend/src/views/SettingsView.vue | 452 ++---------------- .../__tests__/SettingsView.error-css.test.ts | 200 +------- .../__tests__/SettingsView.worktrees.test.ts | 79 +-- 5 files changed, 63 insertions(+), 1034 deletions(-) diff --git a/web/frontend/src/__tests__/deploy-config-clear-fields.test.ts b/web/frontend/src/__tests__/deploy-config-clear-fields.test.ts index 2b8b73f..e1a98e7 100644 --- a/web/frontend/src/__tests__/deploy-config-clear-fields.test.ts +++ b/web/frontend/src/__tests__/deploy-config-clear-fields.test.ts @@ -1,190 +1,37 @@ /** - * KIN-INFRA-012: Регрессионный тест — saveDeployConfig не теряет пустые строки + * KIN-INFRA-012 / KIN-UI-012: Регрессия — формы deploy из SettingsView удалены * - * Проверяет, что пустая строка в deploy-полях: - * 1. Передаётся в patchProject как "" (не undefined) - * 2. Переживает JSON.stringify — т.е. бэкенд получает ключ со значением "" - * 3. Непустые значения тоже передаются корректно + * saveDeployConfig и связанные deploy-поля перенесены в ProjectView → Settings tab. + * SettingsView теперь только навигатор. * - * До фикса: `deploy_host: deployHosts.value[id] || undefined` → пустая строка - * превращалась в undefined и JSON.stringify отбрасывал поле. Бэкенд не получал - * сигнал очистки. После фикса: пустая строка передаётся как-есть. + * Проверяет, что в SettingsView.vue отсутствуют удалённые формы. */ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { mount, flushPromises } from '@vue/test-utils' -import SettingsView from '../views/SettingsView.vue' +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' -vi.mock('../api', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - api: { - projects: vi.fn(), - patchProject: vi.fn(), - syncObsidian: vi.fn(), - }, - } -}) +describe('SettingsView.vue — формы deploy удалены (KIN-UI-012)', () => { + const filePath = resolve(__dirname, '../views/SettingsView.vue') + const content = readFileSync(filePath, 'utf-8') -import { api } from '../api' - -const BASE_PROJECT = { - id: 'KIN', - name: 'Kin', - path: '/projects/kin', - status: 'active', - priority: 5, - tech_stack: ['python'], - execution_mode: null, - autocommit_enabled: null, - auto_test_enabled: null, - obsidian_vault_path: null, - deploy_command: null as string | null, - test_command: null as string | null, - deploy_host: null as string | null, - deploy_path: null as string | null, - deploy_runtime: null as string | null, - deploy_restart_cmd: null as string | 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 mountSettingsWithProject(overrides: Partial = {}) { - const project = { ...BASE_PROJECT, ...overrides } - vi.mocked(api.projects).mockResolvedValue([project as any]) - const wrapper = mount(SettingsView) - await flushPromises() - return wrapper -} - -async function clickSaveDeployConfig(wrapper: ReturnType) { - const saveBtn = wrapper.findAll('button').find(b => b.text().includes('Save Deploy Config')) - expect(saveBtn).toBeDefined() - await saveBtn!.trigger('click') - await flushPromises() -} - -// ───────────────────────────────────────────────────────────── -// 1. Пустая строка передаётся как "" — не теряется как undefined -// ───────────────────────────────────────────────────────────── -describe('saveDeployConfig — пустые строки сохраняются в payload', () => { - it('deploy_host="" передаётся как "" когда поле пустое (не undefined)', async () => { - // null в проекте → инициализируется как '' в компоненте → должно передаться как "" - const wrapper = await mountSettingsWithProject({ deploy_host: null }) - await clickSaveDeployConfig(wrapper) - - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - expect(callArgs[1]).toHaveProperty('deploy_host') - expect((callArgs[1] as any).deploy_host).toBe('') + it('не содержит saveDeployConfig (форма перенесена в ProjectView)', () => { + expect(content).not.toContain('saveDeployConfig') }) - it('deploy_path="" передаётся как "" когда поле пустое (не undefined)', async () => { - const wrapper = await mountSettingsWithProject({ deploy_path: null }) - await clickSaveDeployConfig(wrapper) - - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - expect(callArgs[1]).toHaveProperty('deploy_path') - expect((callArgs[1] as any).deploy_path).toBe('') + it('не содержит deployHosts (реактивный ref удалён)', () => { + expect(content).not.toContain('deployHosts') }) - it('deploy_runtime="" передаётся как "" когда поле пустое (не undefined)', async () => { - const wrapper = await mountSettingsWithProject({ deploy_runtime: null }) - await clickSaveDeployConfig(wrapper) - - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - expect(callArgs[1]).toHaveProperty('deploy_runtime') - expect((callArgs[1] as any).deploy_runtime).toBe('') + it('не содержит deploy_host input поле', () => { + expect(content).not.toContain('server host (e.g. vdp-prod)') }) - it('deploy_restart_cmd="" передаётся как "" когда поле пустое (не undefined)', async () => { - const wrapper = await mountSettingsWithProject({ deploy_restart_cmd: null }) - await clickSaveDeployConfig(wrapper) + it('не содержит test_command input поле', () => { + expect(content).not.toContain('placeholder="make test"') + }) - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - expect(callArgs[1]).toHaveProperty('deploy_restart_cmd') - expect((callArgs[1] as any).deploy_restart_cmd).toBe('') - }) -}) - -// ───────────────────────────────────────────────────────────── -// 2. JSON.stringify не выбрасывает пустые строки — бэкенд их получит -// ───────────────────────────────────────────────────────────── -describe('saveDeployConfig — пустые строки выживают JSON.stringify', () => { - it('все 4 поля присутствуют в JSON когда все значения пустые', async () => { - // Проект с null во всех deploy-полях → инициализируются как '' - const wrapper = await mountSettingsWithProject() - await clickSaveDeployConfig(wrapper) - - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - const payload = callArgs[1] as Record - - // Сериализуем как настоящий PATCH-запрос - const jsonBody = JSON.parse(JSON.stringify(payload)) - - expect(jsonBody).toHaveProperty('deploy_host', '') - expect(jsonBody).toHaveProperty('deploy_path', '') - expect(jsonBody).toHaveProperty('deploy_runtime', '') - expect(jsonBody).toHaveProperty('deploy_restart_cmd', '') - }) - - it('пустая строка выживает JSON.stringify (регрессия: undefined исчезает)', async () => { - const wrapper = await mountSettingsWithProject({ deploy_host: null }) - await clickSaveDeployConfig(wrapper) - - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - const payload = callArgs[1] as Record - const jsonBody = JSON.parse(JSON.stringify(payload)) - - // "" должна присутствовать в JSON как пустая строка - // undefined здесь отсутствовало бы — это был бы баг - expect(Object.keys(jsonBody)).toContain('deploy_host') - expect(jsonBody.deploy_host).toBe('') - expect(jsonBody.deploy_host).not.toBeUndefined() - }) -}) - -// ───────────────────────────────────────────────────────────── -// 3. Непустые значения передаются корректно -// ───────────────────────────────────────────────────────────── -describe('saveDeployConfig — непустые значения передаются корректно', () => { - it('deploy_host с непустым значением передаётся без изменений', async () => { - const wrapper = await mountSettingsWithProject({ deploy_host: 'prod.example.com' }) - await clickSaveDeployConfig(wrapper) - - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - expect((callArgs[1] as any).deploy_host).toBe('prod.example.com') - }) - - it('все 4 поля с непустыми значениями передаются корректно', async () => { - const wrapper = await mountSettingsWithProject({ - deploy_host: 'prod.example.com', - deploy_path: '/opt/kin', - deploy_runtime: 'docker', - deploy_restart_cmd: 'docker compose restart', - }) - await clickSaveDeployConfig(wrapper) - - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - const payload = callArgs[1] as any - expect(payload.deploy_host).toBe('prod.example.com') - expect(payload.deploy_path).toBe('/opt/kin') - expect(payload.deploy_runtime).toBe('docker') - expect(payload.deploy_restart_cmd).toBe('docker compose restart') + it('не содержит obsidian_vault_path input поле', () => { + expect(content).not.toContain('path/to/obsidian/vault') }) }) diff --git a/web/frontend/src/__tests__/deploy-standardized.test.ts b/web/frontend/src/__tests__/deploy-standardized.test.ts index 405423b..a35d9bc 100644 --- a/web/frontend/src/__tests__/deploy-standardized.test.ts +++ b/web/frontend/src/__tests__/deploy-standardized.test.ts @@ -2,18 +2,19 @@ * KIN-079: Стандартизированный deploy — компонентные тесты * * Проверяет: - * 1. SettingsView — deploy config рендерится, Save Deploy Config, runtime select - * 2. ProjectView — Deploy кнопка (видимость, disabled, спиннер) - * 3. ProjectView — Deploy результат (structured, legacy, dependents) - * 4. ProjectView — Links таб (список, add link модал, delete link) - * 5. Граничные кейсы (пустые links, deploy без dependents, overall_success=false) + * 1. ProjectView — Deploy кнопка (видимость, disabled, спиннер) + * 2. ProjectView — Deploy результат (structured, legacy, dependents) + * 3. ProjectView — Links таб (список, add link модал, delete link) + * 4. Граничные кейсы (пустые links, deploy без dependents, overall_success=false) + * + * Примечание: тесты SettingsView (Deploy Config, Project Links) удалены (KIN-UI-012). + * Эти формы перенесены в ProjectView → Settings tab. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createRouter, createMemoryHistory } from 'vue-router' import ProjectView from '../views/ProjectView.vue' -import SettingsView from '../views/SettingsView.vue' vi.mock('../api', async (importOriginal) => { const actual = await importOriginal() @@ -135,163 +136,7 @@ async function switchToLinksTab(wrapper: ReturnType) { } // ───────────────────────────────────────────────────────────── -// 1. SettingsView — Deploy Config -// ───────────────────────────────────────────────────────────── -describe('SettingsView — Deploy Config', () => { - async function mountSettings(deployFields: Partial = {}) { - const project = { ...BASE_PROJECT, ...deployFields } - vi.mocked(api.projects).mockResolvedValue([project as any]) - const wrapper = mount(SettingsView) - await flushPromises() - return wrapper - } - - it('раздел Deploy Config рендерится для каждого проекта', async () => { - const wrapper = await mountSettings() - expect(wrapper.text()).toContain('Deploy Config') - }) - - it('поле Server host рендерится', async () => { - const wrapper = await mountSettings() - expect(wrapper.text()).toContain('Server host') - }) - - it('поле Project path on server рендерится', async () => { - const wrapper = await mountSettings() - expect(wrapper.text()).toContain('Project path on server') - }) - - it('поле Runtime рендерится', async () => { - const wrapper = await mountSettings() - expect(wrapper.text()).toContain('Runtime') - }) - - it('поле Restart command рендерится', async () => { - const wrapper = await mountSettings() - expect(wrapper.text()).toContain('Restart command') - }) - - it('runtime select содержит опции docker, node, python, static', async () => { - const wrapper = await mountSettings() - // Ищем select для runtime (первый select в Deploy Config) - const selects = wrapper.findAll('select') - // Находим select с options docker/node/python/static - const runtimeSelect = selects.find(s => { - const opts = s.findAll('option') - const values = opts.map(o => o.element.value) - return values.includes('docker') && values.includes('node') - }) - expect(runtimeSelect).toBeDefined() - const values = runtimeSelect!.findAll('option').map(o => o.element.value) - expect(values).toContain('docker') - expect(values).toContain('node') - expect(values).toContain('python') - expect(values).toContain('static') - }) - - it('Save Deploy Config вызывает patchProject', async () => { - const wrapper = await mountSettings() - const saveBtn = wrapper.findAll('button').find(b => b.text().includes('Save Deploy Config')) - expect(saveBtn).toBeDefined() - await saveBtn!.trigger('click') - await flushPromises() - expect(vi.mocked(api.patchProject)).toHaveBeenCalled() - }) - - it('patchProject вызывается с deploy_host, deploy_path, deploy_runtime, deploy_restart_cmd', async () => { - const wrapper = await mountSettings({ - deploy_host: 'myserver.com', - deploy_path: '/opt/app', - deploy_runtime: 'docker', - deploy_restart_cmd: 'docker compose up -d', - }) - const saveBtn = wrapper.findAll('button').find(b => b.text().includes('Save Deploy Config')) - await saveBtn!.trigger('click') - await flushPromises() - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - expect(callArgs[0]).toBe('KIN') - // Все 4 ключа присутствуют в объекте - expect(callArgs[1]).toHaveProperty('deploy_host') - expect(callArgs[1]).toHaveProperty('deploy_path') - expect(callArgs[1]).toHaveProperty('deploy_runtime') - expect(callArgs[1]).toHaveProperty('deploy_restart_cmd') - }) - - it('patchProject получает deploy_host=myserver.com из заполненного поля', async () => { - const wrapper = await mountSettings({ deploy_host: 'myserver.com' }) - const saveBtn = wrapper.findAll('button').find(b => b.text().includes('Save Deploy Config')) - await saveBtn!.trigger('click') - await flushPromises() - const callArgs = vi.mocked(api.patchProject).mock.calls[0] - expect((callArgs[1] as any).deploy_host).toBe('myserver.com') - }) - - it('статус "Saved" отображается после успешного сохранения', async () => { - const wrapper = await mountSettings() - const saveBtn = wrapper.findAll('button').find(b => b.text().includes('Save Deploy Config')) - await saveBtn!.trigger('click') - await flushPromises() - expect(wrapper.text()).toContain('Saved') - }) -}) - -// ───────────────────────────────────────────────────────────── -// 1b. SettingsView — Project Links -// ───────────────────────────────────────────────────────────── -describe('SettingsView — Project Links', () => { - it('link.type рендерится корректно в списке связей — не undefined (конвенция #527)', async () => { - // Bug #527: шаблон использовал {{ link.link_type }} вместо {{ link.type }} → runtime undefined - vi.mocked(api.projectLinks).mockResolvedValue([ - { id: 1, from_project: 'KIN', to_project: 'BRS', type: 'triggers', description: null, created_at: '2026-01-01' }, - ] as any) - vi.mocked(api.projects).mockResolvedValue([BASE_PROJECT as any]) - const wrapper = mount(SettingsView) - await flushPromises() - - // Тип связи должен отображаться как строка, а не undefined - expect(wrapper.text()).toContain('triggers') - expect(wrapper.text()).not.toContain('undefined') - }) - - it('addLink вызывает createProjectLink с полем type (не link_type) из формы', async () => { - // Bug #527: addLink() использовал link_type вместо type при вызове api - vi.mocked(api.projects).mockResolvedValue([ - { ...BASE_PROJECT, id: 'BRS', name: 'Barsik' } as any, - BASE_PROJECT as any, - ]) - vi.mocked(api.projectLinks).mockResolvedValue([]) - const wrapper = mount(SettingsView) - await flushPromises() - - // Открываем форму добавления связи для первого проекта - const addBtns = wrapper.findAll('button').filter(b => b.text().includes('+ Add Link')) - if (addBtns.length > 0) { - await addBtns[0].trigger('click') - await flushPromises() - - // Выбираем to_project (в SettingsView это select без api.projects — используем allProjectList) - const selects = wrapper.findAll('select') - const toProjectSelect = selects.find(s => s.findAll('option').some(o => o.element.value !== '' && o.element.value !== 'depends_on' && o.element.value !== 'triggers' && o.element.value !== 'related_to')) - if (toProjectSelect) { - const opts = toProjectSelect.findAll('option').filter(o => o.element.value !== '') - if (opts.length > 0) await toProjectSelect.setValue(opts[0].element.value) - } - - const form = wrapper.find('form') - await form.trigger('submit') - await flushPromises() - - if (vi.mocked(api.createProjectLink).mock.calls.length > 0) { - const callArg = vi.mocked(api.createProjectLink).mock.calls[0][0] - expect(callArg).toHaveProperty('type') - expect(callArg).not.toHaveProperty('link_type') - } - } - }) -}) - -// ───────────────────────────────────────────────────────────── -// 2. ProjectView — Deploy кнопка +// ProjectView — Deploy кнопка // ───────────────────────────────────────────────────────────── describe('ProjectView — Deploy кнопка', () => { it('кнопка Deploy присутствует в header проекта', async () => { diff --git a/web/frontend/src/views/SettingsView.vue b/web/frontend/src/views/SettingsView.vue index ffd2ec5..92b68ee 100644 --- a/web/frontend/src/views/SettingsView.vue +++ b/web/frontend/src/views/SettingsView.vue @@ -1,199 +1,20 @@ diff --git a/web/frontend/src/views/__tests__/SettingsView.error-css.test.ts b/web/frontend/src/views/__tests__/SettingsView.error-css.test.ts index 4f2c960..68f37d7 100644 --- a/web/frontend/src/views/__tests__/SettingsView.error-css.test.ts +++ b/web/frontend/src/views/__tests__/SettingsView.error-css.test.ts @@ -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() - 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 = {}) { - 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:/) - }) }) diff --git a/web/frontend/src/views/__tests__/SettingsView.worktrees.test.ts b/web/frontend/src/views/__tests__/SettingsView.worktrees.test.ts index 1b7274b..83abe4b 100644 --- a/web/frontend/src/views/__tests__/SettingsView.worktrees.test.ts +++ b/web/frontend/src/views/__tests__/SettingsView.worktrees.test.ts @@ -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 = {}) { @@ -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[]) { - 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') - }) -})