Voegt components/dialogs/product-dialog.tsx toe op basis van het entity-dialog-patroon. Gebruikt react-hook-form + zodResolver voor client-side validatie. Roept createProductAction/updateProductAction aan en werkt stores/products-store.ts optimistisch bij. Demo-modus disabled alle velden + submit-knop via DemoTooltip. 7 tests groen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
|
|
vi.mock('@/actions/products', () => ({
|
|
createProductAction: vi.fn(),
|
|
updateProductAction: vi.fn(),
|
|
}))
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
|
vi.mock('@/stores/products-store', () => ({
|
|
useProductsStore: vi.fn((selector: (s: { addProduct: () => void; updateProduct: () => void }) => unknown) =>
|
|
selector({ addProduct: vi.fn(), updateProduct: vi.fn() })
|
|
),
|
|
}))
|
|
|
|
import { ProductDialog } from '@/components/dialogs/product-dialog'
|
|
import { createProductAction, updateProductAction } from '@/actions/products'
|
|
import { toast } from 'sonner'
|
|
|
|
const mockCreate = createProductAction as ReturnType<typeof vi.fn>
|
|
const mockUpdate = updateProductAction as ReturnType<typeof vi.fn>
|
|
const mockToast = toast as { success: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> }
|
|
|
|
const PRODUCT = {
|
|
id: 'prod-1',
|
|
name: 'Mijn Product',
|
|
code: 'MP',
|
|
description: 'Een product',
|
|
repo_url: 'https://github.com/org/repo',
|
|
definition_of_done: 'Alles groen',
|
|
auto_pr: false,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('ProductDialog — create mode', () => {
|
|
it('rendert met lege velden en "Nieuw product" titel', () => {
|
|
render(
|
|
<ProductDialog mode="create" open={true} onOpenChange={vi.fn()} />
|
|
)
|
|
expect(screen.getByText('Nieuw product')).toBeTruthy()
|
|
expect(screen.getByLabelText(/Naam/)).toBeTruthy()
|
|
expect((screen.getByLabelText(/Naam/) as HTMLInputElement).value).toBe('')
|
|
})
|
|
|
|
it('toont validatiefout als naam leeg is bij submit', async () => {
|
|
render(
|
|
<ProductDialog mode="create" open={true} onOpenChange={vi.fn()} />
|
|
)
|
|
fireEvent.click(screen.getByRole('button', { name: 'Aanmaken' }))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Naam is verplicht')).toBeTruthy()
|
|
})
|
|
expect(mockCreate).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('roept createProductAction aan bij geldig formulier', async () => {
|
|
mockCreate.mockResolvedValue({ success: true, productId: 'new-prod' })
|
|
|
|
render(
|
|
<ProductDialog mode="create" open={true} onOpenChange={vi.fn()} />
|
|
)
|
|
|
|
fireEvent.change(screen.getByLabelText(/Naam/), { target: { value: 'Nieuw Product' } })
|
|
fireEvent.submit(document.getElementById('product-form')!)
|
|
|
|
await waitFor(() => {
|
|
expect(mockCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({ name: 'Nieuw Product' })
|
|
)
|
|
})
|
|
expect(mockToast.success).toHaveBeenCalledWith('Product aangemaakt')
|
|
})
|
|
|
|
it('toont error-toast als createProductAction een error retourneert', async () => {
|
|
mockCreate.mockResolvedValue({ error: 'Code is al in gebruik' })
|
|
|
|
render(
|
|
<ProductDialog mode="create" open={true} onOpenChange={vi.fn()} />
|
|
)
|
|
|
|
fireEvent.change(screen.getByLabelText(/Naam/), { target: { value: 'Test' } })
|
|
fireEvent.submit(document.getElementById('product-form')!)
|
|
|
|
await waitFor(() => {
|
|
expect(mockToast.error).toHaveBeenCalledWith('Code is al in gebruik')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ProductDialog — edit mode', () => {
|
|
it('rendert met bestaande waarden vooringevuld', () => {
|
|
render(
|
|
<ProductDialog mode="edit" open={true} onOpenChange={vi.fn()} product={PRODUCT} />
|
|
)
|
|
expect(screen.getByText('Product bewerken')).toBeTruthy()
|
|
expect((screen.getByLabelText(/Naam/) as HTMLInputElement).value).toBe('Mijn Product')
|
|
})
|
|
|
|
it('roept updateProductAction aan bij opslaan', async () => {
|
|
mockUpdate.mockResolvedValue({ success: true })
|
|
|
|
render(
|
|
<ProductDialog mode="edit" open={true} onOpenChange={vi.fn()} product={PRODUCT} />
|
|
)
|
|
|
|
fireEvent.change(screen.getByLabelText(/Naam/), { target: { value: 'Gewijzigd Product' } })
|
|
fireEvent.submit(document.getElementById('product-form')!)
|
|
|
|
await waitFor(() => {
|
|
expect(mockUpdate).toHaveBeenCalledWith(
|
|
PRODUCT.id,
|
|
expect.objectContaining({ name: 'Gewijzigd Product' })
|
|
)
|
|
})
|
|
expect(mockToast.success).toHaveBeenCalledWith('Product opgeslagen')
|
|
})
|
|
})
|
|
|
|
describe('ProductDialog — demo mode', () => {
|
|
it('submit-knop is disabled in demo-modus', () => {
|
|
render(
|
|
<ProductDialog mode="create" open={true} onOpenChange={vi.fn()} isDemo={true} />
|
|
)
|
|
const submitBtn = screen.getByRole('button', { name: 'Aanmaken' })
|
|
expect(submitBtn).toHaveProperty('disabled', true)
|
|
})
|
|
})
|