Klik op een taak in het sprint-scherm opent de edit-dialog nu
client-side via setActiveTask op de sprint-workspace-store.
Geen URL-navigatie, geen volledige server re-render — alleen
GET /api/tasks/{id} voor het detail. SSE propageert server-saves
automatisch terug.
- TaskDialog: optionele onClose/onSaved callbacks (closePath
optional gemaakt — backwards compatible)
- SprintTaskDialogMount: nieuwe client-component die
selectActiveTask consumeert en TaskDialog rendert
- SprintUrlTaskSync: deeplink (?editTask=<id>) → store
- Sprint page: mounts toegevoegd, editTask searchParam +
EditTaskLoader-Suspense verwijderd
- TaskList.openEditDialog roept setActiveTask aan ipv router.push
- Vitest integratie-test voor SprintTaskDialogMount
Out-of-scope (follow-up PBIs): newTask-flow, mobile, product-backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
3.5 KiB
TypeScript
119 lines
3.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
|
|
vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
|
vi.mock('@/actions/tasks', () => ({
|
|
saveTask: vi.fn(),
|
|
deleteTask: vi.fn(),
|
|
}))
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
|
|
|
import { SprintTaskDialogMount } from '@/components/sprint/sprint-task-dialog-mount'
|
|
import { useSprintWorkspaceStore } from '@/stores/sprint-workspace/store'
|
|
import type { SprintWorkspaceTaskDetail } from '@/stores/sprint-workspace/types'
|
|
|
|
const TASK_DETAIL: SprintWorkspaceTaskDetail = {
|
|
id: 't1',
|
|
code: 'T-1',
|
|
title: 'Mijn taak',
|
|
description: 'Beschrijving',
|
|
priority: 2,
|
|
sort_order: 1,
|
|
status: 'in_progress',
|
|
story_id: 'story-1',
|
|
sprint_id: 'sprint-1',
|
|
created_at: new Date('2026-01-15'),
|
|
_detail: true,
|
|
implementation_plan: 'Stap 1\nStap 2',
|
|
}
|
|
|
|
function resetStore() {
|
|
useSprintWorkspaceStore.setState((s) => {
|
|
s.context.activeProduct = null
|
|
s.context.activeSprintId = null
|
|
s.context.activeStoryId = null
|
|
s.context.activeTaskId = null
|
|
s.entities.sprintsById = {}
|
|
s.entities.storiesById = {}
|
|
s.entities.tasksById = {}
|
|
s.relations.sprintIdsByProduct = {}
|
|
s.relations.storyIdsBySprint = {}
|
|
s.relations.taskIdsByStory = {}
|
|
s.loading.loadedProductSprintsIds = {}
|
|
s.loading.loadingProductId = null
|
|
s.loading.loadedSprintIds = {}
|
|
s.loading.loadingSprintId = null
|
|
s.loading.loadedStoryIds = {}
|
|
s.loading.loadedTaskIds = {}
|
|
s.loading.activeRequestId = null
|
|
s.pendingMutations = {}
|
|
})
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetStore()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('SprintTaskDialogMount', () => {
|
|
it('rendert niets wanneer er geen active task is', () => {
|
|
const { container } = render(
|
|
<SprintTaskDialogMount productId="p1" isDemo={false} />,
|
|
)
|
|
expect(container.textContent).toBe('')
|
|
})
|
|
|
|
it('rendert niets wanneer active task geen _detail heeft', () => {
|
|
useSprintWorkspaceStore.setState((s) => {
|
|
s.entities.tasksById['t1'] = {
|
|
id: 't1',
|
|
code: 'T-1',
|
|
title: 'Mijn taak',
|
|
description: null,
|
|
priority: 2,
|
|
sort_order: 1,
|
|
status: 'todo',
|
|
story_id: 'story-1',
|
|
sprint_id: 'sprint-1',
|
|
created_at: new Date(),
|
|
}
|
|
s.context.activeTaskId = 't1'
|
|
})
|
|
|
|
const { container } = render(
|
|
<SprintTaskDialogMount productId="p1" isDemo={false} />,
|
|
)
|
|
expect(container.textContent).toBe('')
|
|
})
|
|
|
|
it('rendert TaskDialog met titel "Taak bewerken" wanneer detail aanwezig is', () => {
|
|
useSprintWorkspaceStore.setState((s) => {
|
|
s.entities.tasksById['t1'] = TASK_DETAIL
|
|
s.context.activeTaskId = 't1'
|
|
})
|
|
|
|
render(<SprintTaskDialogMount productId="p1" isDemo={false} />)
|
|
|
|
expect(screen.getByText('Taak bewerken')).toBeTruthy()
|
|
expect((screen.getByLabelText(/Titel/) as HTMLInputElement).value).toBe('Mijn taak')
|
|
})
|
|
|
|
it('clear activeTaskId wanneer Annuleren wordt geklikt', async () => {
|
|
useSprintWorkspaceStore.setState((s) => {
|
|
s.entities.tasksById['t1'] = TASK_DETAIL
|
|
s.context.activeTaskId = 't1'
|
|
})
|
|
|
|
render(<SprintTaskDialogMount productId="p1" isDemo={false} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Annuleren' }))
|
|
|
|
await waitFor(() => {
|
|
expect(useSprintWorkspaceStore.getState().context.activeTaskId).toBeNull()
|
|
})
|
|
})
|
|
})
|