Scrum4Me/__tests__/components/solo/batch-enqueue-blocker-dialog.test.tsx
Janpeter Visser 0ce6076a5c
Solo batch-enqueue: per-PBI volgorde + blocker-dialog (#65)
* feat(solo): orderBy taken per PBI-hiërarchie

Voeg pbi.priority en pbi.sort_order toe aan de task.findMany orderBy in de solo-page query zodat taken per PBI gegroepeerd worden vóór story- en task-volgorde.

* feat(solo): previewEnqueueAllAction met blocker-detectie

Voeg previewEnqueueAllAction toe aan actions/claude-jobs.ts: haalt taken op in PBI-volgorde, filtert actieve jobs, detecteert eerste blocker (REVIEW taak of BLOCKED PBI). Retourneert tasks[], blockerIndex en blockerReason. Tests: 7 nieuwe cases voor alle blocker-scenario's en demo-blokkering.

* feat(solo): enqueueClaudeJobsBatchAction met IDOR-check

Voeg enqueueClaudeJobsBatchAction toe: accepteert expliciete taskIds[], verifieert dat alle IDs bij de ingelogde gebruiker horen (IDOR-preventie), slaat taken met actieve jobs over (idempotent), en maakt jobs aan in transactie in opgegeven volgorde. 6 nieuwe tests.

* feat(solo): BatchEnqueueBlockerDialog component

Nieuw dialoogvenster dat gebruiker waarschuwt bij gedetecteerde blocker: toont blockerReason in NL, prefixCount taken vóór blokkade, confirm-knop (disabled met tooltip bij count=0) en annuleer-knop. 7 tests voor rendering, click-handlers en disabled-state.

* feat(solo): preview-then-confirm flow in SoloBoard Voer-alle-uit

Vervang directe enqueueAllTodoJobsAction door previewEnqueueAllAction + BatchEnqueueBlockerDialog. Geen blocker → enqueueClaudeJobsBatchAction direct. Wel blocker → dialog met prefix-enqueue of annuleer. Loading-state op knop tijdens preview en confirm. 5 integratie-tests.

* test(solo): uitgebreide batch-preflight tests met 2 PBI's en 4 taken

Nieuw claude-jobs-batch.test.ts: 10 gevallen voor previewEnqueueAllAction (PBI-volgorde, REVIEW/BLOCKED-detectie, active-job-skip met blockerIndex-shift) en enqueueClaudeJobsBatchAction (happy path, IDOR, active-job-skip, demo).
2026-05-03 13:55:13 +02:00

114 lines
3.9 KiB
TypeScript

// @vitest-environment jsdom
import '@testing-library/jest-dom'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
vi.mock('@/components/ui/dialog', () => ({
Dialog: ({ open, children }: { open: boolean; onOpenChange?: (v: boolean) => void; children: React.ReactNode }) =>
open ? <div data-testid="dialog">{children}</div> : null,
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <h2>{children}</h2>,
}))
vi.mock('@/components/ui/button', () => ({
Button: ({
children,
onClick,
disabled,
variant,
}: {
children?: React.ReactNode
onClick?: () => void
disabled?: boolean
variant?: string
}) => (
<button onClick={onClick} disabled={disabled} data-variant={variant}>
{children}
</button>
),
}))
vi.mock('@/components/ui/tooltip', () => ({
TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipTrigger: ({ render: r, children }: { render?: React.ReactElement; children?: React.ReactNode }) =>
r ? <>{r}</> : <>{children}</>,
TooltipContent: ({ children }: { children: React.ReactNode }) => (
<span data-testid="tooltip-content">{children}</span>
),
}))
import { BatchEnqueueBlockerDialog } from '@/components/solo/batch-enqueue-blocker-dialog'
const DEFAULT_PROPS = {
open: true,
onOpenChange: vi.fn(),
prefixCount: 3,
blockerReason: 'task-review' as const,
blockerLabel: 'Story X — Task Y (in review)',
onConfirm: vi.fn(),
onCancel: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('BatchEnqueueBlockerDialog', () => {
it('renders title and blocker info for task-review', () => {
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} />)
expect(screen.getByRole('heading')).toHaveTextContent('Blokkade gedetecteerd')
expect(screen.getByText(/Een taak staat op 'review'/)).toBeInTheDocument()
expect(screen.getByText(/Story X — Task Y/)).toBeInTheDocument()
})
it('renders correct blocker label for pbi-blocked', () => {
render(
<BatchEnqueueBlockerDialog
{...DEFAULT_PROPS}
blockerReason="pbi-blocked"
blockerLabel="PBI Z — geblokkeerd"
/>
)
expect(screen.getByText(/De PBI is geblokkeerd/)).toBeInTheDocument()
expect(screen.getByText(/PBI Z/)).toBeInTheDocument()
})
it('calls onConfirm when primary button is clicked', () => {
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} />)
fireEvent.click(screen.getByText(/Stuur 3 taken tot aan blokkade/))
expect(DEFAULT_PROPS.onConfirm).toHaveBeenCalledTimes(1)
})
it('calls onCancel when cancel button is clicked', () => {
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} />)
fireEvent.click(screen.getByText('Annuleer'))
expect(DEFAULT_PROPS.onCancel).toHaveBeenCalledTimes(1)
})
it('disables confirm button and shows tooltip when prefixCount is 0', () => {
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} prefixCount={0} />)
const confirmBtn = screen.getByText(/Stuur 0/).closest('button')
expect(confirmBtn).toBeDisabled()
expect(screen.getByTestId('tooltip-content')).toHaveTextContent('Geen taken vóór blokkade')
})
it('does not render when open is false', () => {
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} open={false} />)
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument()
})
it('uses singular taak when prefixCount is 1', () => {
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} prefixCount={1} />)
expect(screen.getByText(/Stuur 1 taak tot aan blokkade/)).toBeInTheDocument()
expect(screen.getByText(/1 taak vóór de blokkade/)).toBeInTheDocument()
})
})