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();
});
});