/** * Tests for ArsToast * @vi-environment jsdom */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ArsToast } from "./ars-toast.js"; describe("ArsToast", () => { let element: ArsToast; beforeEach(() => { document.body.innerHTML = ""; vi.useFakeTimers(); element = new ArsToast(); }); afterEach(() => { vi.useRealTimers(); }); // --- Registration --- it("registers the ars-toast custom element", () => { expect(customElements.get("ars-toast")).toBe(ArsToast); }); it("creates a shadow root on construction", () => { expect(element.shadowRoot).toBeTruthy(); }); // --- Declarative rendering --- it("renders toast content when open attribute is set", () => { element.setAttribute("message", "Hello"); element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector(".toast")).toBeTruthy(); expect(element.shadowRoot!.querySelector(".message")?.textContent).toContain("Hello"); }); it("does not render toast when open is not set", () => { element.setAttribute("message", "Hidden"); document.body.appendChild(element); expect(element.shadowRoot!.querySelector(".toast")).toBeNull(); }); // --- Severity --- it("defaults severity to info", () => { expect(element.severity).toBe("info"); }); it("applies severity class to toast element", () => { element.setAttribute("severity", "error"); element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector(".toast--error")).toBeTruthy(); }); it("supports all four severity levels", () => { document.body.appendChild(element); element.setAttribute("open", ""); for (const sev of ["info", "success", "warning", "error"] as const) { element.severity = sev; expect(element.shadowRoot!.querySelector(`.toast--${sev}`)).toBeTruthy(); } }); // --- Dismissible --- it("shows close button when dismissible", () => { element.setAttribute("dismissible", ""); element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector(".close-btn")).toBeTruthy(); }); it("hides close button when not dismissible", () => { element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector(".close-btn")).toBeNull(); }); // --- Progress bar --- it("renders progress bar when progress attribute is set", () => { element.setAttribute("progress", ""); element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector(".progress-bar")).toBeTruthy(); }); it("does not render progress bar by default", () => { element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector(".progress-bar")).toBeNull(); }); it("defaults progress to false", () => { expect(element.progress).toBe(false); }); it("reflects progress via property", () => { element.progress = true; expect(element.hasAttribute("progress")).toBe(true); }); it("ArsToast.show() applies progress option", () => { const toast = ArsToast.show("Test", { progress: true }); expect(toast.progress).toBe(true); }); // --- Auto-dismiss --- it("auto-dismisses after duration", () => { element.setAttribute("open", ""); element.setAttribute("duration", "3000"); document.body.appendChild(element); const events: string[] = []; element.addEventListener("ars-toast:dismiss", (e) => { events.push((e as CustomEvent).detail.reason); }); vi.advanceTimersByTime(3000); expect(events).toContain("timeout"); }); it("does not auto-dismiss when duration is 0", () => { element.setAttribute("open", ""); element.setAttribute("duration", "0"); document.body.appendChild(element); const events: any[] = []; element.addEventListener("ars-toast:dismiss", () => events.push(true)); vi.advanceTimersByTime(10000); expect(events).toEqual([]); }); it("defaults duration to 5000ms", () => { expect(element.duration).toBe(5000); }); // --- dismiss() method --- it("dismiss() emits ars-toast:dismiss with reason", () => { element.setAttribute("open", ""); document.body.appendChild(element); const events: string[] = []; element.addEventListener("ars-toast:dismiss", (e) => { events.push((e as CustomEvent).detail.reason); }); element.dismiss("user"); expect(events).toEqual(["user"]); }); it("dismiss() defaults to programmatic reason", () => { element.setAttribute("open", ""); document.body.appendChild(element); const events: string[] = []; element.addEventListener("ars-toast:dismiss", (e) => { events.push((e as CustomEvent).detail.reason); }); element.dismiss(); expect(events).toEqual(["programmatic"]); }); // --- Close button click --- it("dismisses on close button click", () => { element.setAttribute("open", ""); element.setAttribute("dismissible", ""); document.body.appendChild(element); const events: string[] = []; element.addEventListener("ars-toast:dismiss", (e) => { events.push((e as CustomEvent).detail.reason); }); element.shadowRoot!.querySelector(".close-btn")!.dispatchEvent( new MouseEvent("click", { bubbles: true }), ); expect(events).toEqual(["user"]); }); // --- Static API --- it("ArsToast.show() creates and appends a toast to the body", () => { const toast = ArsToast.show("Test message"); expect(toast).toBeInstanceOf(ArsToast); expect(toast.message).toBe("Test message"); expect(toast.hasAttribute("open")).toBe(true); }); it("ArsToast.show() applies severity option", () => { const toast = ArsToast.show("Error!", { severity: "error" }); expect(toast.severity).toBe("error"); }); it("ArsToast.show() applies dismissible option", () => { const toast = ArsToast.show("Info", { dismissible: true }); expect(toast.dismissible).toBe(true); }); it("ArsToast.show() creates container with aria-live", () => { ArsToast.show("Test", { position: "bottom-left" }); const container = document.querySelector(".ars-toast-container--bottom-left"); expect(container).toBeTruthy(); expect(container!.getAttribute("aria-live")).toBe("polite"); }); // --- Slots --- it("provides a default slot for custom content", () => { element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector("slot:not([name])")).toBeTruthy(); }); it("provides an action slot", () => { element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector('slot[name="action"]')).toBeTruthy(); }); // --- Observed attributes --- it("observes the expected attributes", () => { const attrs = ArsToast.observedAttributes; for (const attr of ["message", "severity", "duration", "dismissible", "progress", "open"]) { expect(attrs).toContain(attr); } }); // --- Property setters --- it("reflects message via property", () => { element.message = "Hello"; expect(element.getAttribute("message")).toBe("Hello"); }); it("reflects severity via property", () => { element.severity = "warning"; expect(element.getAttribute("severity")).toBe("warning"); }); it("reflects duration via property", () => { element.duration = 8000; expect(element.getAttribute("duration")).toBe("8000"); }); // --- CSS tokens --- it("uses design token variables in styles", () => { element.setAttribute("open", ""); document.body.appendChild(element); const styles = element.shadowRoot!.querySelector("style")?.textContent ?? ""; expect(styles).toContain("--arswc-color-success"); expect(styles).toContain("--arswc-color-warning"); expect(styles).toContain("--arswc-color-danger"); expect(styles).toContain("--arswc-color-accent"); }); // --- XSS --- it("escapes HTML in message", () => { element.setAttribute("message", ""); element.setAttribute("open", ""); document.body.appendChild(element); const msg = element.shadowRoot!.querySelector(".message"); expect(msg?.textContent).toContain(""); expect(msg?.querySelector("script")).toBeNull(); }); // --- Reduced motion --- it("includes prefers-reduced-motion media query", () => { element.setAttribute("open", ""); document.body.appendChild(element); const styles = element.shadowRoot!.querySelector("style")?.textContent ?? ""; expect(styles).toContain("prefers-reduced-motion"); }); // --- ARIA --- it("has role=alert on the toast element", () => { element.setAttribute("open", ""); document.body.appendChild(element); expect(element.shadowRoot!.querySelector('[role="alert"]')).toBeTruthy(); }); });