* feat(user-settings): voeg IdeasListPrefs schema toe met filterStatuses Nieuw IdeasListPrefs-subschema met filterStatuses (array van IdeaStatusApi-waarden), ingehangen als views.ideasList in ViewsPrefs. Testdekking voor geldig, ongeldig en leeg filterStatuses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: extraheer MultiFilterPills naar backlog-filter-popover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ideas): voeg IdeasFilterPopover component toe Nieuwe client-component met multi-select statusfilter popover voor het Ideeënscherm; hergebruikt MultiFilterPills uit backlog-filter-popover. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ideas): vervang inline statuschips door IdeasFilterPopover met user-settings persistentie Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(ideas): voeg componenttests toe voor IdeasFilterPopover en persistentie Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ideas): voeg activeProductId-prop toe aan IdeaList IdeaListProps uitgebreid met activeProductId: string | null. Create-form initialiseert en reset naar het actieve product na aanmaken. Tests en page.tsx bijgewerkt (page.tsx krijgt echte waarde in volgende taak). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ideas): resolve active_product_id en geef door aan IdeaList Haalt active_product_id op via prisma.user.findUnique en resolveert het tegen de al opgehaalde toegankelijke productenlijst (AC4). Geeft het resultaat als prop door aan IdeaList in plaats van hardcoded null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ideas): stuur activeProductId mee bij snel idee aanmaken Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(ideas): voeg component-tests toe voor activeProductId-voorvulling AC1: "Nieuw idee"-select voorgevuld met activeProductId AC2: "Nieuw idee"-select leeg bij activeProductId=null AC3: "Snel idee" stuurt product_id=activeProductId mee bij createIdeaAction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(notifications): toon textarea altijd in answer-modal naast opties Vervang opties-XOR-textarea door twee onafhankelijke blokken: opties alleen wanneer aanwezig, vrij tekstveld altijd zichtbaar. Bij opties een visuele scheiding (border-t) en label 'Of typ een eigen antwoord'. Verstuur-knop nu altijd in footer zichtbaar (was verborgen bij opties). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(notifications): gebruik question.options?.length als conditie Gebruik de kortere optional chaining variant consistent, conform plan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(notifications): voeg component-tests toe voor AnswerModal Dekt: optieknoppen + textarea + Verstuur zichtbaar met opties, submit via optieknop, submit via vrij tekstveld, disabled Verstuur bij leeg veld, en demo-modus (textarea + Verstuur disabled). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(notifications): werk answer-modal spec bij voor vrije tekstveld naast opties Beschrijft dat textarea + Verstuur altijd zichtbaar zijn in multiple-choice mode. Corrigeert de Cmd/Ctrl+Enter-bullet: shortcut werkt nu ook daar. Bijgewerkt naar 2026-05-15. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
import React from 'react'
|
|
|
|
vi.mock('@/actions/questions', () => ({
|
|
answerQuestion: vi.fn(),
|
|
}))
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
|
vi.mock('@/stores/notifications-store', () => ({
|
|
useNotificationsStore: {
|
|
getState: () => ({ remove: vi.fn() }),
|
|
},
|
|
}))
|
|
vi.mock('next/link', () => ({
|
|
default: ({ href, children }: { href: string; children: React.ReactNode }) => (
|
|
<a href={href}>{children}</a>
|
|
),
|
|
}))
|
|
|
|
import { AnswerModal } from '@/components/notifications/answer-modal'
|
|
import { answerQuestion } from '@/actions/questions'
|
|
import { toast } from 'sonner'
|
|
import type { NotificationQuestion } from '@/stores/notifications-store'
|
|
|
|
const mockAnswerQuestion = answerQuestion as ReturnType<typeof vi.fn>
|
|
const mockToast = toast as unknown as {
|
|
success: ReturnType<typeof vi.fn>
|
|
error: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
const QUESTION: NotificationQuestion = {
|
|
kind: 'idea',
|
|
id: 'q-1',
|
|
product_id: 'prod-1',
|
|
idea_id: 'idea-1',
|
|
idea_code: 'IDEA-42',
|
|
idea_title: 'Mijn Idee',
|
|
question: 'Wat denk jij?',
|
|
options: ['Optie A', 'Optie B'],
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
expires_at: '2026-12-31T00:00:00Z',
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('AnswerModal — met opties', () => {
|
|
it('toont optieknoppen, textarea en Verstuur-knop', () => {
|
|
render(<AnswerModal question={QUESTION} isDemo={false} onClose={vi.fn()} />)
|
|
expect(screen.getByRole('button', { name: 'Optie A' })).toBeTruthy()
|
|
expect(screen.getByRole('button', { name: 'Optie B' })).toBeTruthy()
|
|
expect(screen.getByLabelText(/Antwoord op Claude/)).toBeTruthy()
|
|
expect(screen.getByRole('button', { name: 'Verstuur' })).toBeTruthy()
|
|
})
|
|
|
|
it('roept answerQuestion aan met optiewaarde bij klik op optieknop', async () => {
|
|
mockAnswerQuestion.mockResolvedValue({ ok: true })
|
|
render(<AnswerModal question={QUESTION} isDemo={false} onClose={vi.fn()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Optie A' }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockAnswerQuestion).toHaveBeenCalledWith('q-1', 'Optie A')
|
|
})
|
|
})
|
|
|
|
it('roept answerQuestion aan met getypte tekst bij klik op Verstuur', async () => {
|
|
mockAnswerQuestion.mockResolvedValue({ ok: true })
|
|
render(<AnswerModal question={QUESTION} isDemo={false} onClose={vi.fn()} />)
|
|
|
|
fireEvent.change(screen.getByLabelText(/Antwoord op Claude/), {
|
|
target: { value: 'Mijn eigen antwoord' },
|
|
})
|
|
fireEvent.click(screen.getByRole('button', { name: 'Verstuur' }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockAnswerQuestion).toHaveBeenCalledWith('q-1', 'Mijn eigen antwoord')
|
|
})
|
|
})
|
|
|
|
it('Verstuur-knop is disabled zolang het tekstveld leeg is', () => {
|
|
render(<AnswerModal question={QUESTION} isDemo={false} onClose={vi.fn()} />)
|
|
expect(screen.getByRole('button', { name: 'Verstuur' })).toHaveProperty('disabled', true)
|
|
})
|
|
})
|
|
|
|
describe('AnswerModal — demo-modus', () => {
|
|
it('textarea is disabled en Verstuur is disabled bij isDemo=true', () => {
|
|
render(<AnswerModal question={QUESTION} isDemo={true} onClose={vi.fn()} />)
|
|
expect(screen.getByLabelText(/Antwoord op Claude/)).toHaveProperty('disabled', true)
|
|
expect(screen.getByRole('button', { name: 'Verstuur' })).toHaveProperty('disabled', true)
|
|
})
|
|
})
|
|
|
|
describe('AnswerModal — geen vraag', () => {
|
|
it('rendert niets wanneer question null is', () => {
|
|
const { container } = render(
|
|
<AnswerModal question={null} isDemo={false} onClose={vi.fn()} />,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
})
|