Compare commits

...

3 commits

Author SHA1 Message Date
Gros Frumos
a22cf738b7 kin: auto-commit after pipeline 2026-03-18 18:24:37 +02:00
Gros Frumos
d532f26f5a Merge branch 'KIN-UI-016-frontend_dev' 2026-03-18 18:22:31 +02:00
Gros Frumos
2a594a8d24 kin: KIN-UI-016-frontend_dev 2026-03-18 18:22:31 +02:00
2 changed files with 226 additions and 0 deletions

View file

@ -1098,6 +1098,7 @@ async function addDecision() {
<input type="date" v-model="dateTo" data-testid="date-to" <input type="date" v-model="dateTo" data-testid="date-to"
class="bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-gray-300 focus:border-gray-500 outline-none" /> class="bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-gray-300 focus:border-gray-500 outline-none" />
<button v-if="dateFrom || dateTo" @click="dateFrom = ''; dateTo = ''" <button v-if="dateFrom || dateTo" @click="dateFrom = ''; dateTo = ''"
data-testid="date-reset-btn"
class="text-gray-600 hover:text-red-400 text-xs px-1"></button> class="text-gray-600 hover:text-red-400 text-xs px-1"></button>
</div> </div>
</div> </div>

View file

@ -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<typeof import('../../api')>()
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<string, string> = {}
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)
})
})