feat(PBI-98/T-1092): empty-state + component-tests
- components/dashboard/products-empty-state.tsx: nieuwe component met rounded-xl container, NL-tekst, en NewProductButton (verborgen in demo). - ProductsTable: vroege return ProductsEmptyState bij products.length===0. - __tests__/components/dashboard/products-empty-state.test.tsx: 4 tests (tekst, NewProductButton zichtbaar/verborgen op demo, container-styling). - __tests__/components/dashboard/product-row-actions.test.tsx: 7 tests (Activeer/Actief-badge zichtbaarheid per state, Docs/Backlog aria-labels, Meer-acties-knop disabled in demo). Mocks: next/navigation, sonner, actions/products + actions/active-product. - 1035 tests groen totaal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a6fcfe685a
commit
27728296ff
4 changed files with 152 additions and 0 deletions
72
__tests__/components/dashboard/product-row-actions.test.tsx
Normal file
72
__tests__/components/dashboard/product-row-actions.test.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/actions/products', () => ({
|
||||
archiveProductAction: vi.fn().mockResolvedValue({ success: true }),
|
||||
restoreProductAction: vi.fn().mockResolvedValue({ success: true }),
|
||||
deleteProductAction: vi.fn().mockResolvedValue({ success: true }),
|
||||
}))
|
||||
|
||||
vi.mock('@/actions/active-product', () => ({
|
||||
setActiveProductAction: vi.fn().mockResolvedValue({ success: true }),
|
||||
}))
|
||||
|
||||
import { ProductRowActions } from '@/components/dashboard/product-row-actions'
|
||||
|
||||
const baseProps = {
|
||||
productId: 'p-1',
|
||||
productName: 'Scrum4Me',
|
||||
isActive: false,
|
||||
isArchived: false,
|
||||
isDemo: false,
|
||||
}
|
||||
|
||||
describe('ProductRowActions', () => {
|
||||
it('toont Activeer-knop voor inactief, niet-archived product', () => {
|
||||
render(<ProductRowActions {...baseProps} />)
|
||||
expect(screen.queryByText(/activeer/i)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('toont "Actief"-badge voor active product (geen Activeer-knop)', () => {
|
||||
render(<ProductRowActions {...baseProps} isActive={true} />)
|
||||
expect(screen.queryByText('Actief')).not.toBeNull()
|
||||
expect(screen.queryByText(/^activeer$/i)).toBeNull()
|
||||
})
|
||||
|
||||
it('verbergt Activeer-knop én badge bij archived product', () => {
|
||||
render(<ProductRowActions {...baseProps} isArchived={true} />)
|
||||
expect(screen.queryByText(/^activeer$/i)).toBeNull()
|
||||
expect(screen.queryByText('Actief')).toBeNull()
|
||||
})
|
||||
|
||||
it('rendert Docs-knop met aria-label', () => {
|
||||
render(<ProductRowActions {...baseProps} />)
|
||||
expect(screen.queryByLabelText('Docs')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('rendert Open-backlog-knop met aria-label', () => {
|
||||
render(<ProductRowActions {...baseProps} />)
|
||||
expect(screen.queryByLabelText('Open backlog')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('Meer-acties-knop is disabled in demo-modus', () => {
|
||||
render(<ProductRowActions {...baseProps} isDemo={true} />)
|
||||
const more = screen.getByLabelText('Meer acties') as HTMLButtonElement
|
||||
expect(more.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('Meer-acties-knop is enabled voor reguliere user', () => {
|
||||
render(<ProductRowActions {...baseProps} />)
|
||||
const more = screen.getByLabelText('Meer acties') as HTMLButtonElement
|
||||
expect(more.disabled).toBe(false)
|
||||
})
|
||||
})
|
||||
37
__tests__/components/dashboard/products-empty-state.test.tsx
Normal file
37
__tests__/components/dashboard/products-empty-state.test.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/dialogs/product-dialog', () => ({
|
||||
ProductDialog: () => null,
|
||||
}))
|
||||
|
||||
import { ProductsEmptyState } from '@/components/dashboard/products-empty-state'
|
||||
|
||||
describe('ProductsEmptyState', () => {
|
||||
it('toont de empty-tekst', () => {
|
||||
render(<ProductsEmptyState isDemo={false} />)
|
||||
expect(screen.queryByText(/nog geen producten aangemaakt/i)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('toont NewProductButton voor reguliere user', () => {
|
||||
render(<ProductsEmptyState isDemo={false} />)
|
||||
expect(screen.queryByText(/nieuw product/i)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('toont GEEN NewProductButton in demo-modus', () => {
|
||||
render(<ProductsEmptyState isDemo={true} />)
|
||||
expect(screen.queryByText(/nieuw product/i)).toBeNull()
|
||||
})
|
||||
|
||||
it('rendert in een gestylde container', () => {
|
||||
const { container } = render(<ProductsEmptyState isDemo={false} />)
|
||||
const root = container.firstChild as HTMLElement
|
||||
expect(root.className).toContain('rounded-xl')
|
||||
expect(root.className).toContain('border')
|
||||
})
|
||||
})
|
||||
35
components/dashboard/products-empty-state.tsx
Normal file
35
components/dashboard/products-empty-state.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Empty-state voor de Dashboard products-tabel: wordt getoond wanneer de
|
||||
// gebruiker (en alle leden van zijn ProductMember-set) nog géén producten
|
||||
// hebben. Hergebruikt NewProductButton zodat de CTA dezelfde dialog opent
|
||||
// als de header-knop.
|
||||
|
||||
import { NewProductButton } from '@/components/dashboard/new-product-button'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Props {
|
||||
isDemo: boolean
|
||||
}
|
||||
|
||||
export function ProductsEmptyState({ isDemo }: Props) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl border border-border bg-surface-container-low p-12 text-center space-y-4"
|
||||
{...debugProps(
|
||||
'products-empty-state',
|
||||
'ProductsEmptyState',
|
||||
'components/dashboard/products-empty-state.tsx',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<p className="text-base font-medium">
|
||||
Nog geen producten aangemaakt
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Een product groepeert je backlog, sprints en docs. Maak er eentje aan
|
||||
om te beginnen.
|
||||
</p>
|
||||
</div>
|
||||
{!isDemo && <NewProductButton />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import {
|
|||
type ProductDialogProduct,
|
||||
} from '@/components/dialogs/product-dialog'
|
||||
import { ProductRowActions } from '@/components/dashboard/product-row-actions'
|
||||
import { ProductsEmptyState } from '@/components/dashboard/products-empty-state'
|
||||
import { useUserSettingsStore } from '@/stores/user-settings/store'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -124,6 +125,13 @@ export function ProductsTable({
|
|||
void setPref(['views', 'productsTable', 'sortDir'], newDir)
|
||||
}
|
||||
|
||||
// Vroege return: empty-state als de user nog geen enkel product heeft.
|
||||
// Filter-resultaten (incl. archived-toggle) tonen we wel via tabel met
|
||||
// "geen resultaten"-rij, zodat de filter-context zichtbaar blijft.
|
||||
if (products.length === 0) {
|
||||
return <ProductsEmptyState isDemo={isDemo} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...debugProps(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue