import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Toaster } from './toast'; import { TOAST_DISMISS_EVENT, TOAST_SHOW_EVENT, type ToastDismissEvent, type ToastShowEvent, } from './toast.types'; function setup(): HTMLElement { document.body.innerHTML = '
'; return document.body.firstElementChild as HTMLElement; } function toasts(region: HTMLElement): HTMLElement[] { return Array.from(region.querySelectorAll('[data-c42-toast]')); } describe('Toaster', () => { beforeEach(() => { document.body.innerHTML = ''; }); afterEach(() => { vi.useRealTimers(); }); it('wires region ARIA and position on init', () => { const region = setup(); new Toaster(region, { position: 'top-center' }); expect(region.getAttribute('role')).toBe('region'); expect(region.getAttribute('aria-live')).toBe('polite'); expect(region.getAttribute('aria-label')).toBe('Notifications'); expect(region.dataset.position).toBe('top-center'); }); it('defaults position to bottom-right', () => { const region = setup(); new Toaster(region); expect(region.dataset.position).toBe('bottom-right'); }); it('show creates a toast with role, variant and content', () => { const region = setup(); const toaster = new Toaster(region); const id = toaster.show({ title: 'Saved', description: 'All good', variant: 'success' }); const [el] = toasts(region); expect(typeof id).toBe('string'); expect(el.getAttribute('role')).toBe('status'); expect(el.dataset.variant).toBe('success'); expect(el.dataset.state).toBe('open'); expect(el.querySelector('[data-c42-toast-title]')!.textContent).toBe('Saved'); expect(el.querySelector('[data-c42-toast-description]')!.textContent).toBe('All good'); expect(el.querySelector('[data-c42-toast-close]')!.getAttribute('aria-label')).toBe('Dismiss'); }); it('defaults variant to info', () => { const region = setup(); const toaster = new Toaster(region); toaster.show({ title: 'Hi' }); expect(toasts(region)[0].dataset.variant).toBe('info'); }); it('removes a toast when the close button is clicked', () => { const region = setup(); const toaster = new Toaster(region); toaster.show({ title: 'Bye' }); const close = region.querySelector('[data-c42-toast-close]')!; close.click(); expect(toasts(region)).toHaveLength(0); }); it('auto-dismisses after the duration', () => { vi.useFakeTimers(); const region = setup(); const toaster = new Toaster(region, { duration: 3000 }); toaster.show({ title: 'Temp' }); expect(toasts(region)).toHaveLength(1); vi.advanceTimersByTime(2999); expect(toasts(region)).toHaveLength(1); vi.advanceTimersByTime(1); expect(toasts(region)).toHaveLength(0); }); it('persists when duration is 0', () => { vi.useFakeTimers(); const region = setup(); const toaster = new Toaster(region, { duration: 0 }); toaster.show({ title: 'Sticky' }); vi.advanceTimersByTime(100000); expect(toasts(region)).toHaveLength(1); }); it('honors a per-toast duration override', () => { vi.useFakeTimers(); const region = setup(); const toaster = new Toaster(region, { duration: 5000 }); toaster.show({ title: 'Quick', duration: 1000 }); vi.advanceTimersByTime(1000); expect(toasts(region)).toHaveLength(0); }); it('enforces the max cap by removing the oldest', () => { const region = setup(); const toaster = new Toaster(region, { max: 2, duration: 0 }); const first = toaster.show({ title: 'One' }); toaster.show({ title: 'Two' }); toaster.show({ title: 'Three' }); const ids = toasts(region).map((el) => el.id); expect(ids).toHaveLength(2); expect(ids).not.toContain(first); }); it('dispatches toast:show and toast:dismiss events', () => { const region = setup(); const toaster = new Toaster(region, { duration: 0 }); const onShow = vi.fn(); const onDismiss = vi.fn(); toaster.on(TOAST_SHOW_EVENT, onShow); toaster.on(TOAST_DISMISS_EVENT, onDismiss); const id = toaster.show({ title: 'Ping' }); expect(onShow).toHaveBeenCalledOnce(); expect(onShow.mock.calls[0][0].detail.id).toBe(id); toaster.dismiss(id); expect(onDismiss).toHaveBeenCalledOnce(); expect(onDismiss.mock.calls[0][0].detail.id).toBe(id); }); it('clear removes all toasts', () => { const region = setup(); const toaster = new Toaster(region, { duration: 0 }); toaster.show({ title: 'A' }); toaster.show({ title: 'B' }); toaster.clear(); expect(toasts(region)).toHaveLength(0); }); it('destroy removes toasts and listeners', () => { const region = setup(); const toaster = new Toaster(region, { duration: 0 }); const onShow = vi.fn(); toaster.on(TOAST_SHOW_EVENT, onShow); toaster.show({ title: 'A' }); toaster.destroy(); expect(toasts(region)).toHaveLength(0); region.dispatchEvent(new CustomEvent(TOAST_SHOW_EVENT, { detail: { id: 'x' } })); expect(onShow).toHaveBeenCalledOnce(); }); });