import { resolve } from "node:path"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { render, fireEvent, act } from "@testing-library/preact"; import { Toast, ToastProvider, useToast } from "./Toast.js"; import { useRef } from "preact/hooks"; /** Helper component that triggers a toast via the hook */ function TestTrigger({ message = "Test message", status, duration, }: { message?: string; status?: "info" | "success" | "warning" | "error"; duration?: number; }) { const { toast } = useToast(); return ( ); } describe("Toast", () => { // ── Standalone Toast component ── it("renders message text", () => { const { getByText } = render(); expect(getByText("Hello")).toBeTruthy(); }); it("applies status class", () => { const { container } = render(); expect( container.querySelector(".strand-toast--success"), ).toBeTruthy(); }); it("has role status", () => { const { getByRole } = render(); expect(getByRole("status")).toBeTruthy(); }); it("an error toast is an alert, announced assertively", () => { const { getByRole } = render( , ); expect(getByRole("alert")).toHaveAttribute("aria-live", "assertive"); }); it("info toast has aria-live polite", () => { const { getByRole } = render( , ); expect(getByRole("status")).toHaveAttribute("aria-live", "polite"); }); it("a warning toast is an alert, announced assertively", () => { const { getByRole } = render( , ); expect(getByRole("alert")).toHaveAttribute("aria-live", "assertive"); }); it("renders dismiss button when onDismiss provided", () => { const onDismiss = vi.fn(); const { getByLabelText } = render( , ); fireEvent.click(getByLabelText("Dismiss")); expect(onDismiss).toHaveBeenCalledTimes(1); }); it("defaults to info status", () => { const { container } = render(); expect( container.querySelector(".strand-toast--info"), ).toBeTruthy(); }); it("renders status prefix for info", () => { const { container } = render(); const status = container.querySelector(".strand-toast__status"); expect(status).toBeTruthy(); expect(status!.textContent).toBe("INFO"); }); it("renders status prefix for success as COMPLETE", () => { const { container } = render(); const status = container.querySelector(".strand-toast__status"); expect(status).toBeTruthy(); expect(status!.textContent).toBe("COMPLETE"); }); it("renders status prefix for warning", () => { const { container } = render(); const status = container.querySelector(".strand-toast__status"); expect(status).toBeTruthy(); expect(status!.textContent).toBe("WARNING"); }); it("renders status prefix for error", () => { const { container } = render(); const status = container.querySelector(".strand-toast__status"); expect(status).toBeTruthy(); expect(status!.textContent).toBe("ERROR"); }); }); describe("ToastProvider + useToast", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("renders children", () => { const { getByText } = render(

App content

, ); expect(getByText("App content")).toBeTruthy(); }); it("useToast adds a toast that renders message", () => { const { getByText } = render( , ); fireEvent.click(getByText("Trigger")); expect(getByText("Hello toast")).toBeTruthy(); }); it("toast has correct status class", () => { const { getByText, container } = render( , ); fireEvent.click(getByText("Trigger")); expect( container.querySelector(".strand-toast--error"), ).toBeTruthy(); }); it("toast has role status", () => { const { getByText, getAllByRole } = render( , ); fireEvent.click(getByText("Trigger")); const statuses = getAllByRole("status"); expect(statuses.length).toBeGreaterThan(0); }); it("error toast in provider has aria-live assertive", () => { const { getByText, container } = render( , ); fireEvent.click(getByText("Trigger")); const toast = container.querySelector(".strand-toast--error")!; expect(toast.getAttribute("aria-live")).toBe("assertive"); }); it("toast auto-dismisses after duration", () => { const { getByText, queryByText } = render( , ); fireEvent.click(getByText("Trigger")); expect(getByText("Vanishing")).toBeTruthy(); act(() => { vi.advanceTimersByTime(3000); }); expect(queryByText("Vanishing")).toBeNull(); }); it("auto-dismiss waits while the pointer rests on the toast", () => { const { getByText, queryByText } = render( , ); fireEvent.click(getByText("Trigger")); const toastEl = getByText("Held").closest(".strand-toast")!; fireEvent.mouseEnter(toastEl); act(() => { vi.advanceTimersByTime(5000); }); expect(queryByText("Held")).toBeTruthy(); fireEvent.mouseLeave(toastEl); act(() => { vi.advanceTimersByTime(1000); }); expect(queryByText("Held")).toBeNull(); }); it("shows at most maxCount toasts, the newest kept", () => { const { getByText, queryByText } = render( , ); const buttons = document.querySelectorAll("button"); fireEvent.click(buttons[0]); fireEvent.click(buttons[1]); fireEvent.click(buttons[2]); expect(queryByText("One")).toBeNull(); expect(getByText("Two")).toBeTruthy(); expect(getByText("Three")).toBeTruthy(); }); it("a toast can be dismissed by the id toast() returned", () => { function Owner() { const { toast, dismiss } = useToast(); const id = useRef(null); return ( <> ); } const { getByText, queryByText } = render( , ); fireEvent.click(getByText("Show")); expect(getByText("Owned")).toBeTruthy(); fireEvent.click(getByText("Hide")); expect(queryByText("Owned")).toBeNull(); }); it("dismiss button removes toast", () => { const { getByText, getByLabelText, queryByText } = render( , ); fireEvent.click(getByText("Trigger")); expect(getByText("Dismissable")).toBeTruthy(); fireEvent.click(getByLabelText("Dismiss")); expect(queryByText("Dismissable")).toBeNull(); }); it("defaults to info status when none provided", () => { const { getByText, container } = render( , ); fireEvent.click(getByText("Trigger")); expect( container.querySelector(".strand-toast--info"), ).toBeTruthy(); }); it("multiple toasts stack", () => { const { getByText, container } = render( , ); fireEvent.click(getByText("Trigger")); fireEvent.click(getByText("Trigger")); const toasts = container.querySelectorAll(".strand-toast"); expect(toasts.length).toBe(2); }); it("custom className on provider container", () => { const { getByText, container } = render( , ); fireEvent.click(getByText("Trigger")); const toastContainer = container.querySelector( ".strand-toast__container", )!; expect(toastContainer.className).toContain("custom-provider"); }); }); import { snapshotFixtures } from "../../test/snapshot.js"; import { snapshotStylesheet } from "../../test/stylesheet.js"; import { fixtures } from "./Toast.fixtures.js"; snapshotFixtures(Toast, fixtures); snapshotStylesheet(resolve(__dirname, "./Toast.css"));