test(ideas): voeg componenttests toe voor IdeasFilterPopover en persistentie
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e398b84930
commit
3f40bc2f0f
1 changed files with 201 additions and 0 deletions
201
__tests__/components/ideas/idea-list.test.tsx
Normal file
201
__tests__/components/ideas/idea-list.test.tsx
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import React from 'react'
|
||||
|
||||
// --- Navigation mock ---
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
|
||||
}))
|
||||
|
||||
// --- Actions mocks ---
|
||||
vi.mock('@/actions/ideas', () => ({
|
||||
createIdeaAction: vi.fn(),
|
||||
archiveIdeaAction: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/actions/user-settings', () => ({
|
||||
updateUserSettingsAction: vi.fn().mockResolvedValue({ success: true, settings: {} }),
|
||||
}))
|
||||
|
||||
// --- Sonner mock ---
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { error: vi.fn(), success: vi.fn() },
|
||||
}))
|
||||
|
||||
// --- IdeaRowActions mock (complex component with many deps) ---
|
||||
vi.mock('@/components/ideas/idea-row-actions', () => ({
|
||||
IdeaRowActions: () => <div data-testid="idea-row-actions" />,
|
||||
}))
|
||||
|
||||
// --- DemoTooltip mock ---
|
||||
vi.mock('@/components/shared/demo-tooltip', () => ({
|
||||
DemoTooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
// --- Popover mock — controlled via open prop ---
|
||||
vi.mock('@/components/ui/popover', () => {
|
||||
const PopoverCtx = React.createContext<{
|
||||
open: boolean
|
||||
onOpenChange: (v: boolean) => void
|
||||
}>({ open: false, onOpenChange: () => {} })
|
||||
|
||||
return {
|
||||
Popover: ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (v: boolean) => void
|
||||
}) => (
|
||||
<PopoverCtx.Provider value={{ open: open ?? false, onOpenChange: onOpenChange ?? (() => {}) }}>
|
||||
{children}
|
||||
</PopoverCtx.Provider>
|
||||
),
|
||||
PopoverTrigger: ({ render: renderEl }: { render: React.ReactElement<{ onClick?: (e: React.MouseEvent) => void }> }) => {
|
||||
const { open, onOpenChange } = React.useContext(PopoverCtx)
|
||||
return React.cloneElement(renderEl, {
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
onOpenChange(!open)
|
||||
renderEl.props.onClick?.(e)
|
||||
},
|
||||
})
|
||||
},
|
||||
PopoverContent: ({ children }: { children: React.ReactNode }) => {
|
||||
const { open } = React.useContext(PopoverCtx)
|
||||
return open ? <div data-testid="popover-content">{children}</div> : null
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Import after mocks
|
||||
import { useUserSettingsStore } from '@/stores/user-settings/store'
|
||||
import { IdeaList } from '@/components/ideas/idea-list'
|
||||
import type { IdeaDto } from '@/lib/idea-dto'
|
||||
|
||||
// Minimal IdeaDto factory
|
||||
function makeIdea(overrides: Partial<IdeaDto> = {}): IdeaDto {
|
||||
return {
|
||||
id: 'idea-1',
|
||||
code: 'ID-1',
|
||||
title: 'Test Idee',
|
||||
description: null,
|
||||
status: 'draft',
|
||||
product_id: null,
|
||||
product: null,
|
||||
pbi_id: null,
|
||||
pbi: null,
|
||||
secondary_products: [],
|
||||
archived: false,
|
||||
has_grill_md: false,
|
||||
has_plan_md: false,
|
||||
created_at: '2024-01-01T00:00:00.000Z',
|
||||
updated_at: '2024-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const IDEAS: IdeaDto[] = [
|
||||
makeIdea({ id: 'idea-1', code: 'ID-1', title: 'Idee Concept', status: 'draft' }),
|
||||
makeIdea({ id: 'idea-2', code: 'ID-2', title: 'Idee Gegrilld', status: 'grilled' }),
|
||||
makeIdea({ id: 'idea-3', code: 'ID-3', title: 'Idee Gepland', status: 'planned' }),
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useUserSettingsStore.getState().hydrate({}, false)
|
||||
})
|
||||
|
||||
describe('IdeaList — filterpopover', () => {
|
||||
it('toont de "Filters"-knop in de toolbar (geen inline chip-rij)', () => {
|
||||
render(<IdeaList ideas={IDEAS} products={[]} isDemo={false} />)
|
||||
|
||||
// Filters-knop aanwezig
|
||||
expect(screen.getByText('Filters')).toBeInTheDocument()
|
||||
|
||||
// Status-labels zoals "Concept" mogen NIET los zichtbaar zijn zonder popover te openen
|
||||
// (anders was de oude inline chip-rij er nog)
|
||||
expect(screen.queryByRole('button', { name: 'Concept' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('klik op "Filters" opent de popover en toont 11 statusopties', () => {
|
||||
render(<IdeaList ideas={IDEAS} products={[]} isDemo={false} />)
|
||||
|
||||
// Popover nog niet open: content niet zichtbaar
|
||||
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('Filters'))
|
||||
|
||||
// Content verschijnt
|
||||
expect(screen.getByTestId('popover-content')).toBeInTheDocument()
|
||||
|
||||
// 11 statusopties + "Alle" = 12 buttons in de popover
|
||||
// Controleer specifiek de 11 status-labels
|
||||
const statusLabels = [
|
||||
'Concept', 'Grillen', 'Gegrilld', 'Plannen', 'Plan klaar',
|
||||
'Plan beoordelen', 'Gepland', 'Grill mislukt', 'Plan mislukt',
|
||||
'Beoordeling mislukt', 'Plan beoordeeld',
|
||||
]
|
||||
for (const label of statusLabels) {
|
||||
expect(screen.getByRole('button', { name: label })).toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
it('klik op een statuschip schrijft de status naar de store', () => {
|
||||
render(<IdeaList ideas={IDEAS} products={[]} isDemo={false} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Filters'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Concept' }))
|
||||
|
||||
const stored =
|
||||
useUserSettingsStore.getState().entities.settings.views?.ideasList?.filterStatuses
|
||||
expect(stored).toContain('draft')
|
||||
})
|
||||
|
||||
it('gehydrateerde filter toont "Filters (1)" en filtert de tabel', () => {
|
||||
useUserSettingsStore
|
||||
.getState()
|
||||
.hydrate({ views: { ideasList: { filterStatuses: ['draft'] } } }, false)
|
||||
|
||||
render(<IdeaList ideas={IDEAS} products={[]} isDemo={false} />)
|
||||
|
||||
// Trigger toont het actieve filteraantal
|
||||
expect(screen.getByText('Filters (1)')).toBeInTheDocument()
|
||||
|
||||
// Alleen het concept-idee is zichtbaar; de andere twee worden weggefilterd
|
||||
expect(screen.getByText('Idee Concept')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Idee Gegrilld')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Idee Gepland')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('"Wis filters" is disabled wanneer geen filter actief is', () => {
|
||||
render(<IdeaList ideas={IDEAS} products={[]} isDemo={false} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Filters'))
|
||||
|
||||
const wisButton = screen.getByRole('button', { name: 'Wis filters' })
|
||||
expect(wisButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('"Wis filters" is enabled en wist de filter wanneer een filter actief is', () => {
|
||||
useUserSettingsStore
|
||||
.getState()
|
||||
.hydrate({ views: { ideasList: { filterStatuses: ['draft'] } } }, false)
|
||||
|
||||
render(<IdeaList ideas={IDEAS} products={[]} isDemo={false} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Filters (1)'))
|
||||
|
||||
const wisButton = screen.getByRole('button', { name: 'Wis filters' })
|
||||
expect(wisButton).not.toBeDisabled()
|
||||
|
||||
fireEvent.click(wisButton)
|
||||
|
||||
const stored =
|
||||
useUserSettingsStore.getState().entities.settings.views?.ideasList?.filterStatuses
|
||||
expect(stored).toEqual([])
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue