All checks were successful
Build & Publish / publish (push) Successful in 1m2s
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>
253 lines
7.6 KiB
TypeScript
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()
|
|
})
|
|
})
|