Files
design-system/src/design-system/composites/Toast/Toast.test.tsx
hsiegeln 433d582da6
All checks were successful
Build & Publish / publish (push) Successful in 1m2s
feat: migrate all icons to Lucide React
Replace unicode characters, emoji, and inline SVGs with lucide-react
components across the entire design system and page layer. Update
tests to assert on SVG elements instead of text content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:25:43 +01:00

253 lines
7.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, act, fireEvent } from '@testing-library/react'
import { ToastProvider, useToast } from './Toast'
// ── Helpers ───────────────────────────────────────────────────────────────
/** Renders a ToastProvider and exposes the useToast API via a ref-like object */
function renderProvider() {
let api!: ReturnType<typeof useToast>
function Consumer() {
api = useToast()
return null
}
render(
<ToastProvider>
<Consumer />
</ToastProvider>,
)
return { getApi: () => api }
}
// ── Tests ─────────────────────────────────────────────────────────────────
describe('Toast', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
})
it('shows a toast when toast() is called', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Hello', variant: 'info' }) })
expect(screen.getByTestId('toast')).toBeInTheDocument()
expect(screen.getByText('Hello')).toBeInTheDocument()
})
it('renders title and description', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Success!', variant: 'success', description: 'It worked.' }) })
expect(screen.getByText('Success!')).toBeInTheDocument()
expect(screen.getByText('It worked.')).toBeInTheDocument()
})
it('applies correct variant data attribute — error', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Error!', variant: 'error' }) })
expect(screen.getByTestId('toast')).toHaveAttribute('data-variant', 'error')
})
it('applies success variant', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Success!', variant: 'success' }) })
expect(screen.getByTestId('toast')).toHaveAttribute('data-variant', 'success')
})
it('applies warning variant', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Warning!', variant: 'warning' }) })
expect(screen.getByTestId('toast')).toHaveAttribute('data-variant', 'warning')
})
it('applies info variant by default when no variant given', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Default' }) })
expect(screen.getByTestId('toast')).toHaveAttribute('data-variant', 'info')
})
it('shows correct icon for info variant', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Info', variant: 'info' }) })
expect(screen.getByTestId('toast').querySelector('[aria-hidden="true"] svg')).toBeInTheDocument()
})
it('shows correct icon for success variant', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'OK', variant: 'success' }) })
expect(screen.getByTestId('toast').querySelector('[aria-hidden="true"] svg')).toBeInTheDocument()
})
it('shows correct icon for warning variant', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Warn', variant: 'warning' }) })
expect(screen.getByTestId('toast').querySelector('[aria-hidden="true"] svg')).toBeInTheDocument()
})
it('shows correct icon for error variant', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Err', variant: 'error' }) })
expect(screen.getByTestId('toast').querySelector('[aria-hidden="true"] svg')).toBeInTheDocument()
})
it('dismisses toast when close button is clicked', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Closeable' }) })
expect(screen.getByTestId('toast')).toBeInTheDocument()
fireEvent.click(screen.getByLabelText('Dismiss notification'))
// After exit animation duration
act(() => { vi.advanceTimersByTime(300) })
expect(screen.queryByTestId('toast')).not.toBeInTheDocument()
})
it('auto-dismisses after default duration (5000ms)', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Auto' }) })
expect(screen.getByTestId('toast')).toBeInTheDocument()
// Advance past default duration + exit animation
act(() => { vi.advanceTimersByTime(5000 + 300) })
expect(screen.queryByTestId('toast')).not.toBeInTheDocument()
})
it('is still visible just before auto-dismiss fires', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Auto', duration: 1000 }) })
act(() => { vi.advanceTimersByTime(999) })
expect(screen.getByTestId('toast')).toBeInTheDocument()
})
it('auto-dismisses after custom duration', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Fast', duration: 1000 }) })
act(() => { vi.advanceTimersByTime(1000 + 300) })
expect(screen.queryByTestId('toast')).not.toBeInTheDocument()
})
it('renders multiple toasts', () => {
const { getApi } = renderProvider()
act(() => {
getApi().toast({ title: 'First', variant: 'info' })
getApi().toast({ title: 'Second', variant: 'error' })
getApi().toast({ title: 'Third', variant: 'warning' })
})
expect(screen.getAllByTestId('toast')).toHaveLength(3)
})
it('caps visible toasts at 5', () => {
const { getApi } = renderProvider()
act(() => {
for (let i = 0; i < 7; i++) {
getApi().toast({ title: `Toast ${i}` })
}
})
expect(screen.getAllByTestId('toast')).toHaveLength(5)
})
it('keeps newest 5 when capped', () => {
const { getApi } = renderProvider()
act(() => {
for (let i = 0; i < 7; i++) {
getApi().toast({ title: `Toast ${i}` })
}
})
// Toast 0 and 1 should be gone (oldest), Toast 2-6 remain
expect(screen.queryByText('Toast 0')).not.toBeInTheDocument()
expect(screen.queryByText('Toast 1')).not.toBeInTheDocument()
expect(screen.getByText('Toast 6')).toBeInTheDocument()
})
it('returns a string id from toast()', () => {
const { getApi } = renderProvider()
let id!: string
act(() => { id = getApi().toast({ title: 'Test' }) })
expect(typeof id).toBe('string')
expect(id.length).toBeGreaterThan(0)
})
it('dismiss() by id removes only the specified toast', () => {
const { getApi } = renderProvider()
let id1!: string
act(() => {
id1 = getApi().toast({ title: 'First' })
getApi().toast({ title: 'Second' })
})
expect(screen.getAllByTestId('toast')).toHaveLength(2)
act(() => { getApi().dismiss(id1) })
act(() => { vi.advanceTimersByTime(300) })
expect(screen.getAllByTestId('toast')).toHaveLength(1)
expect(screen.queryByText('First')).not.toBeInTheDocument()
expect(screen.getByText('Second')).toBeInTheDocument()
})
it('toast container has aria-live attribute', () => {
const { getApi } = renderProvider()
act(() => { getApi().toast({ title: 'Accessible' }) })
expect(screen.getByLabelText('Notifications')).toBeInTheDocument()
expect(screen.getByLabelText('Notifications')).toHaveAttribute('aria-live', 'polite')
})
it('throws if useToast is used outside ToastProvider', () => {
function BadComponent() {
useToast()
return null
}
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
expect(() => render(<BadComponent />)).toThrow('useToast must be used within a ToastProvider')
consoleSpy.mockRestore()
})
})