From a22cf738b7e136e11fec0b60789d38d8f8457e7e Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Wed, 18 Mar 2026 18:24:37 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- .../src/views/__tests__/date-filter.test.ts | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 web/frontend/src/views/__tests__/date-filter.test.ts diff --git a/web/frontend/src/views/__tests__/date-filter.test.ts b/web/frontend/src/views/__tests__/date-filter.test.ts new file mode 100644 index 0000000..201d875 --- /dev/null +++ b/web/frontend/src/views/__tests__/date-filter.test.ts @@ -0,0 +1,225 @@ +/** + * KIN-UI-016: Тесты фильтра дат для завершённых задач + * + * Проверяет: + * 1. Блок фильтра дат виден при статусе 'done' в selectedStatuses + * 2. Блок фильтра дат скрыт при отсутствии 'done' в selectedStatuses + * 3. Инпуты date-from и date-to присутствуют в блоке фильтра + * 4. Кнопка сброса скрыта, если dateFrom и dateTo не заданы + * 5. Кнопка сброса появляется при заполнении dateFrom + * 6. Кнопка сброса появляется при заполнении dateTo + * 7. Кнопка сброса имеет data-testid='date-reset-btn' (не хрупкий текстовый селектор) + * 8. Клик по кнопке сброса очищает оба поля dateFrom и dateTo + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import ProjectView from '../ProjectView.vue' + +vi.mock('../../api', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + api: { + project: vi.fn(), + projects: vi.fn(), + getPhases: vi.fn(), + environments: vi.fn(), + projectLinks: vi.fn(), + patchProject: vi.fn(), + syncObsidian: vi.fn(), + }, + } +}) + +import { api } from '../../api' + +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: (k: string) => store[k] ?? null, + setItem: (k: string, v: string) => { store[k] = v }, + removeItem: (k: string) => { delete store[k] }, + clear: () => { store = {} }, + } +})() +Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, configurable: true }) + +const BASE_PROJECT_DETAIL = { + id: 'proj-1', + name: 'Test Project', + path: '/projects/test', + status: 'active', + priority: 5, + tech_stack: ['python'], + execution_mode: 'review', + autocommit_enabled: 0, + auto_test_enabled: 0, + worktrees_enabled: 0, + obsidian_vault_path: '', + deploy_command: '', + test_command: '', + deploy_host: '', + deploy_path: '', + deploy_runtime: '', + deploy_restart_cmd: '', + created_at: '2024-01-01', + total_tasks: 0, + done_tasks: 0, + active_tasks: 0, + blocked_tasks: 0, + review_tasks: 0, + project_type: 'development', + ssh_host: '', + ssh_user: '', + ssh_key_path: '', + ssh_proxy_jump: '', + description: null, + tasks: [], + modules: [], + decisions: [], +} + +function makeRouter() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/project/:id', component: ProjectView, props: true }, + ], + }) +} + +beforeEach(() => { + localStorageMock.clear() + vi.clearAllMocks() + vi.mocked(api.project).mockResolvedValue(BASE_PROJECT_DETAIL as any) + vi.mocked(api.projects).mockResolvedValue([]) + vi.mocked(api.getPhases).mockResolvedValue([]) + vi.mocked(api.environments).mockResolvedValue([]) + vi.mocked(api.projectLinks).mockResolvedValue([]) + vi.mocked(api.patchProject).mockResolvedValue(BASE_PROJECT_DETAIL as any) +}) + +describe('ProjectView — фильтр дат завершённых задач', () => { + it('блок фильтра дат виден когда выбран статус done', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=done') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const dateFromInput = wrapper.find('[data-testid="date-from"]') + expect(dateFromInput.exists()).toBe(true) + }) + + it('блок фильтра дат скрыт когда done не выбран', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=pending') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const dateFromInput = wrapper.find('[data-testid="date-from"]') + expect(dateFromInput.exists()).toBe(false) + }) + + it('инпут date-from присутствует в блоке фильтра при статусе done', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=done') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + expect(wrapper.find('[data-testid="date-from"]').exists()).toBe(true) + }) + + it('инпут date-to присутствует в блоке фильтра при статусе done', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=done') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + expect(wrapper.find('[data-testid="date-to"]').exists()).toBe(true) + }) + + it('кнопка сброса скрыта когда dateFrom и dateTo не заданы', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=done') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + expect(wrapper.find('[data-testid="date-reset-btn"]').exists()).toBe(false) + }) + + it('кнопка сброса появляется после ввода значения в dateFrom', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=done') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const dateFrom = wrapper.find('[data-testid="date-from"]') + await dateFrom.setValue('2024-01-01') + await wrapper.vm.$nextTick() + + expect(wrapper.find('[data-testid="date-reset-btn"]').exists()).toBe(true) + }) + + it('кнопка сброса имеет data-testid="date-reset-btn" (не хрупкий текстовый селектор)', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=done') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const dateTo = wrapper.find('[data-testid="date-to"]') + await dateTo.setValue('2024-12-31') + await wrapper.vm.$nextTick() + + const resetBtn = wrapper.find('[data-testid="date-reset-btn"]') + expect(resetBtn.exists()).toBe(true) + expect(resetBtn.attributes('data-testid')).toBe('date-reset-btn') + }) + + it('клик по кнопке сброса очищает dateFrom и dateTo', async () => { + const router = makeRouter() + await router.push('/project/proj-1?status=done') + const wrapper = mount(ProjectView, { + props: { id: 'proj-1' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const dateFrom = wrapper.find('[data-testid="date-from"]') + const dateTo = wrapper.find('[data-testid="date-to"]') + await dateFrom.setValue('2024-01-01') + await dateTo.setValue('2024-12-31') + await wrapper.vm.$nextTick() + + const resetBtn = wrapper.find('[data-testid="date-reset-btn"]') + expect(resetBtn.exists()).toBe(true) + await resetBtn.trigger('click') + await wrapper.vm.$nextTick() + + expect((dateFrom.element as HTMLInputElement).value).toBe('') + expect((dateTo.element as HTMLInputElement).value).toBe('') + expect(wrapper.find('[data-testid="date-reset-btn"]').exists()).toBe(false) + }) +})