kin: KIN-076 Реализовать поле поиска по задачам.
This commit is contained in:
parent
394301c7a7
commit
c14c0b7832
1 changed files with 288 additions and 0 deletions
288
web/frontend/src/__tests__/task-search.test.ts
Normal file
288
web/frontend/src/__tests__/task-search.test.ts
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
/**
|
||||||
|
* KIN-076: Тесты поля поиска по задачам в ProjectView
|
||||||
|
*
|
||||||
|
* Проверяет:
|
||||||
|
* 1. Поиск по слову из title — задача видна, остальные скрыты
|
||||||
|
* 2. Поиск по несуществующему слову — список задач пустой
|
||||||
|
* 3. Очистка поля поиска — все задачи снова видны
|
||||||
|
* 4. Поиск регистронезависим
|
||||||
|
* 5. Поиск по содержимому brief
|
||||||
|
* 6. Кнопка ✕ очищает поиск
|
||||||
|
* 7. При смене props.id поисковый запрос сбрасывается
|
||||||
|
* 8. Поле поиска присутствует на Kanban вкладке
|
||||||
|
* 9. Поиск на Kanban фильтрует задачи в колонках
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { mount, flushPromises } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import ProjectView from '../views/ProjectView.vue'
|
||||||
|
|
||||||
|
vi.mock('../api', () => ({
|
||||||
|
api: {
|
||||||
|
project: vi.fn(),
|
||||||
|
taskFull: vi.fn(),
|
||||||
|
runTask: vi.fn(),
|
||||||
|
auditProject: vi.fn(),
|
||||||
|
createTask: vi.fn(),
|
||||||
|
patchTask: vi.fn(),
|
||||||
|
patchProject: vi.fn(),
|
||||||
|
deployProject: vi.fn(),
|
||||||
|
getPhases: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
const Stub = { template: '<div />' }
|
||||||
|
|
||||||
|
function makeTask(id: string, title: string, brief: unknown = null) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
project_id: 'KIN',
|
||||||
|
title,
|
||||||
|
status: 'pending',
|
||||||
|
priority: 5,
|
||||||
|
assigned_role: null,
|
||||||
|
parent_task_id: null,
|
||||||
|
brief,
|
||||||
|
spec: null,
|
||||||
|
execution_mode: null,
|
||||||
|
blocked_reason: null,
|
||||||
|
dangerously_skipped: null,
|
||||||
|
category: null,
|
||||||
|
acceptance_criteria: null,
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
updated_at: '2024-01-01',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOCK_PROJECT = {
|
||||||
|
id: 'KIN',
|
||||||
|
name: 'Kin',
|
||||||
|
path: '/projects/kin',
|
||||||
|
status: 'active',
|
||||||
|
priority: 5,
|
||||||
|
tech_stack: ['python', 'vue'],
|
||||||
|
execution_mode: 'review',
|
||||||
|
autocommit_enabled: 0,
|
||||||
|
obsidian_vault_path: null,
|
||||||
|
deploy_command: null,
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
total_tasks: 3,
|
||||||
|
done_tasks: 0,
|
||||||
|
active_tasks: 1,
|
||||||
|
blocked_tasks: 0,
|
||||||
|
review_tasks: 0,
|
||||||
|
project_type: 'development',
|
||||||
|
ssh_host: null,
|
||||||
|
ssh_user: null,
|
||||||
|
ssh_key_path: null,
|
||||||
|
ssh_proxy_jump: null,
|
||||||
|
description: null,
|
||||||
|
tasks: [
|
||||||
|
makeTask('KIN-001', 'Реализовать поле поиска'),
|
||||||
|
makeTask('KIN-002', 'Добавить аутентификацию'),
|
||||||
|
makeTask('KIN-003', 'Исправить баг в отчётах', { text: 'текст с ключевым словом oauth' }),
|
||||||
|
],
|
||||||
|
decisions: [],
|
||||||
|
modules: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// localStorage mock
|
||||||
|
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 })
|
||||||
|
|
||||||
|
function makeRouter() {
|
||||||
|
return createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{ path: '/', component: Stub },
|
||||||
|
{ path: '/project/:id', component: ProjectView, props: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convention #162: beforeEach с vi.clearAllMocks() + deep clone mock-объектов
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageMock.clear()
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(api.project).mockResolvedValue(JSON.parse(JSON.stringify(MOCK_PROJECT)) as any)
|
||||||
|
vi.mocked(api.getPhases).mockResolvedValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function mountOnTasks() {
|
||||||
|
const router = makeRouter()
|
||||||
|
await router.push('/project/KIN')
|
||||||
|
const wrapper = mount(ProjectView, {
|
||||||
|
props: { id: 'KIN' },
|
||||||
|
global: { plugins: [router] },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Tasks tab: acceptance criteria (обязательные тесты KIN-076)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('KIN-076: поиск задач — Tasks вкладка (acceptance criteria)', () => {
|
||||||
|
it('1. Поиск по слову из title — задача видна, остальные скрыты', async () => {
|
||||||
|
const wrapper = await mountOnTasks()
|
||||||
|
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск по задачам..."]')
|
||||||
|
expect(searchInput.exists(), 'Поле поиска должно быть в DOM').toBe(true)
|
||||||
|
|
||||||
|
await searchInput.setValue('поиска')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const hrefs = wrapper.findAll('a[href^="/task/"]').map(l => l.attributes('href'))
|
||||||
|
expect(hrefs, 'KIN-001 должен быть виден').toContain('/task/KIN-001')
|
||||||
|
expect(hrefs, 'KIN-002 не должен быть виден').not.toContain('/task/KIN-002')
|
||||||
|
expect(hrefs, 'KIN-003 не должен быть виден').not.toContain('/task/KIN-003')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('2. Поиск по несуществующему слову — список задач пустой', async () => {
|
||||||
|
const wrapper = await mountOnTasks()
|
||||||
|
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск по задачам..."]')
|
||||||
|
await searchInput.setValue('несуществующеесловоxyz123')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const links = wrapper.findAll('a[href^="/task/"]')
|
||||||
|
expect(links, 'При несуществующем слове список задач должен быть пустым').toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('3. Очистка поля поиска — все задачи снова видны', async () => {
|
||||||
|
const wrapper = await mountOnTasks()
|
||||||
|
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск по задачам..."]')
|
||||||
|
|
||||||
|
// Фильтруем задачи поиском
|
||||||
|
await searchInput.setValue('аутентификацию')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('a[href^="/task/"]'), 'Фильтрация должна работать').toHaveLength(1)
|
||||||
|
|
||||||
|
// Очищаем поле — возвращаем все задачи
|
||||||
|
await searchInput.setValue('')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('a[href^="/task/"]'), 'После очистки должны быть все 3 задачи').toHaveLength(3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Tasks tab: дополнительные сценарии
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('KIN-076: поиск задач — дополнительные сценарии', () => {
|
||||||
|
it('4. Поиск регистронезависим — заглавные буквы находят строчные', async () => {
|
||||||
|
const wrapper = await mountOnTasks()
|
||||||
|
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск по задачам..."]')
|
||||||
|
await searchInput.setValue('ПОИСКА')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const hrefs = wrapper.findAll('a[href^="/task/"]').map(l => l.attributes('href'))
|
||||||
|
expect(hrefs, 'KIN-001 должен находиться независимо от регистра').toContain('/task/KIN-001')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('5. Поиск по содержимому brief — задача с совпадением в brief видна', async () => {
|
||||||
|
const wrapper = await mountOnTasks()
|
||||||
|
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск по задачам..."]')
|
||||||
|
await searchInput.setValue('oauth')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const hrefs = wrapper.findAll('a[href^="/task/"]').map(l => l.attributes('href'))
|
||||||
|
expect(hrefs, 'KIN-003 должна быть найдена по brief').toContain('/task/KIN-003')
|
||||||
|
expect(hrefs, 'KIN-001 не совпадает — не должен быть виден').not.toContain('/task/KIN-001')
|
||||||
|
expect(hrefs, 'KIN-002 не совпадает — не должен быть виден').not.toContain('/task/KIN-002')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('6. Кнопка ✕ очищает поиск и показывает все задачи', async () => {
|
||||||
|
const wrapper = await mountOnTasks()
|
||||||
|
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск по задачам..."]')
|
||||||
|
await searchInput.setValue('поиска')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
// Кнопка ✕ появляется при непустом поиске
|
||||||
|
const clearBtn = wrapper.findAll('button').find(b => b.text().trim() === '✕')
|
||||||
|
expect(clearBtn?.exists(), 'Кнопка ✕ должна быть видна при непустом поиске').toBe(true)
|
||||||
|
|
||||||
|
await clearBtn!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.findAll('a[href^="/task/"]'), 'После ✕ — все задачи снова видны').toHaveLength(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('7. При смене props.id поисковый запрос сбрасывается', async () => {
|
||||||
|
const router = makeRouter()
|
||||||
|
await router.push('/project/KIN')
|
||||||
|
const wrapper = mount(ProjectView, {
|
||||||
|
props: { id: 'KIN' },
|
||||||
|
global: { plugins: [router] },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск по задачам..."]')
|
||||||
|
await searchInput.setValue('поиска')
|
||||||
|
await flushPromises()
|
||||||
|
expect((searchInput.element as HTMLInputElement).value).toBe('поиска')
|
||||||
|
|
||||||
|
// Меняем id проекта → watch сбрасывает taskSearch
|
||||||
|
await wrapper.setProps({ id: 'OTHER' })
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(searchInput.element as HTMLInputElement).value,
|
||||||
|
'Поиск должен сброситься при смене проекта',
|
||||||
|
).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Kanban tab: поиск
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('KIN-076: поиск задач — Kanban вкладка', () => {
|
||||||
|
async function mountOnKanban() {
|
||||||
|
const wrapper = await mountOnTasks()
|
||||||
|
const kanbanTab = wrapper.findAll('button').find(b =>
|
||||||
|
b.classes().includes('border-b-2') && b.text().includes('Kanban')
|
||||||
|
)!
|
||||||
|
await kanbanTab.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
it('8. Поле поиска присутствует на Kanban вкладке', async () => {
|
||||||
|
const wrapper = await mountOnKanban()
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск..."]')
|
||||||
|
expect(searchInput.exists(), 'Поле поиска должно быть на канбан-вкладке').toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('9. Поиск на Kanban фильтрует задачи в колонках', async () => {
|
||||||
|
const wrapper = await mountOnKanban()
|
||||||
|
const searchInput = wrapper.find('input[placeholder="Поиск..."]')
|
||||||
|
|
||||||
|
await searchInput.setValue('поиска')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.find('a[href="/task/KIN-001"]').exists(), 'KIN-001 должен быть виден').toBe(true)
|
||||||
|
expect(wrapper.find('a[href="/task/KIN-002"]').exists(), 'KIN-002 не должен быть виден').toBe(false)
|
||||||
|
expect(wrapper.find('a[href="/task/KIN-003"]').exists(), 'KIN-003 не должен быть виден').toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue