diff --git a/web/frontend/src/views/ProjectView.vue b/web/frontend/src/views/ProjectView.vue
index 5f13d48..3613496 100644
--- a/web/frontend/src/views/ProjectView.vue
+++ b/web/frontend/src/views/ProjectView.vue
@@ -1098,6 +1098,7 @@ async function addDecision() {
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)
+ })
+})