// Vitest coverage for the toast primitive in // `src/lib/toast.svelte.ts`. The store keeps one active toast at a // time, replaces it on a fresh `show`, auto-dismisses after the // configured duration, runs the `onAction` callback once on the // action button, and ignores a stale `dismiss(id)` whose target was // already replaced. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { toast } from "../src/lib/toast.svelte"; beforeEach(() => { toast.resetForTests(); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); toast.resetForTests(); }); describe("toast.show", () => { test("sets current and assigns a fresh id", () => { const id = toast.show({ messageKey: "common.loading", }); expect(id).toBeTruthy(); expect(toast.current).not.toBeNull(); expect(toast.current?.id).toBe(id); expect(toast.current?.messageKey).toBe("common.loading"); }); test("a second show replaces the previous descriptor", () => { const first = toast.show({ messageKey: "common.loading" }); const second = toast.show({ messageKey: "common.dismiss", }); expect(second).not.toBe(first); expect(toast.current?.id).toBe(second); expect(toast.current?.messageKey).toBe("common.dismiss"); }); test("auto-dismisses after durationMs", () => { toast.show({ messageKey: "common.loading", durationMs: 2_000, }); expect(toast.current).not.toBeNull(); vi.advanceTimersByTime(1_999); expect(toast.current).not.toBeNull(); vi.advanceTimersByTime(1); expect(toast.current).toBeNull(); }); test("durationMs=null makes the toast sticky", () => { toast.show({ messageKey: "common.loading", durationMs: null, }); vi.advanceTimersByTime(60_000); expect(toast.current).not.toBeNull(); }); }); describe("toast.dismiss", () => { test("clears current when called without an id", () => { toast.show({ messageKey: "common.loading" }); toast.dismiss(); expect(toast.current).toBeNull(); }); test("ignores a stale id whose target was replaced", () => { const first = toast.show({ messageKey: "common.loading", durationMs: 5_000, }); const second = toast.show({ messageKey: "common.dismiss" }); toast.dismiss(first); expect(toast.current?.id).toBe(second); expect(toast.current?.messageKey).toBe("common.dismiss"); }); test("auto-dismiss timer of the replaced toast does not clobber the live one", () => { toast.show({ messageKey: "common.loading", durationMs: 500 }); const second = toast.show({ messageKey: "common.dismiss", durationMs: null, }); vi.advanceTimersByTime(500); // The first toast's timer fired but the dismiss is a no-op // because `current.id !== first`. The sticky second toast // stays alive. expect(toast.current?.id).toBe(second); }); }); describe("onAction", () => { test("ignored unless the action button is invoked manually", () => { const onAction = vi.fn(); toast.show({ messageKey: "common.loading", actionLabelKey: "common.dismiss", onAction, }); vi.advanceTimersByTime(1_000); expect(onAction).not.toHaveBeenCalled(); }); test("toast-host wiring is exercised by the layout: callback fires when host calls onAction then dismiss", () => { // The host component runs `onAction()` and then `dismiss(id)`. // We simulate that sequence here to pin the contract the host // relies on: a single invocation of the user callback per // descriptor, and the toast clears afterwards. const onAction = vi.fn(); const id = toast.show({ messageKey: "common.loading", actionLabelKey: "common.dismiss", onAction, }); const current = toast.current; current?.onAction?.(); toast.dismiss(current?.id); expect(onAction).toHaveBeenCalledTimes(1); expect(toast.current).toBeNull(); // id stays unique — a follow-up show must return a different one. expect(toast.show({ messageKey: "common.loading" })).not.toBe(id); }); });