// @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clear, events } from "./log"; import { retryLostStylesheets } from "./stylesheetRetry"; const LOADED = { cssRules: [{}] }; // A lost stylesheet still has a `sheet`, and reading its rules throws (measured: Chromium, WebKit). const LOST = { get cssRules(): never { throw new DOMException("Cannot access rules", "SecurityError"); }, }; function link(href: string, loaded: boolean, sheet: unknown = loaded ? LOADED : LOST) { const el = document.createElement("link"); el.rel = "stylesheet"; el.href = href; el.setAttribute("crossorigin", ""); Object.defineProperty(el, "sheet", { value: sheet, configurable: true }); document.head.append(el); return el; } const links = () => [...document.head.querySelectorAll('link[rel="stylesheet"]')]; const logged = () => events().map((e) => e.event); describe("retryLostStylesheets", () => { beforeEach(() => { vi.useFakeTimers(); clear(); }); afterEach(() => { vi.useRealTimers(); document.head.innerHTML = ""; }); it("leaves a loaded stylesheet alone", () => { const fine = link("/a.css", true); retryLostStylesheets(); vi.runAllTimers(); expect(links()).toEqual([fine]); expect(logged()).toEqual([]); }); it("asks again for a lost one, in its place, keeping its attributes", () => { const first = link("/a.css", true); const lost = link("/b.css", false); const last = link("/c.css", true); retryLostStylesheets(); vi.advanceTimersByTime(0); const [a, b, c] = links(); expect(a).toBe(first); expect(b).not.toBe(lost); expect(b!.getAttribute("href")).toBe("/b.css"); expect(b!.hasAttribute("crossorigin")).toBe(true); expect(c).toBe(last); expect(logged()).toEqual(["app.stylesheet_lost"]); }); it.each([ ["no sheet at all", null], ["an empty sheet", { cssRules: [] }], ])("takes a link with %s for lost too", (_, sheet) => { const lost = link("/a.css", false, sheet); retryLostStylesheets(); vi.advanceTimersByTime(0); expect(links()[0]).not.toBe(lost); }); it("says so once the new request has loaded", () => { link("/a.css", false); retryLostStylesheets(); vi.advanceTimersByTime(0); links()[0]!.dispatchEvent(new Event("load")); vi.runAllTimers(); expect(logged()).toEqual(["app.stylesheet_lost", "app.stylesheet_recovered"]); }); it("waits longer each time and then gives up, reporting it", () => { link("/a.css", false); retryLostStylesheets(); const fail = () => links()[0]!.dispatchEvent(new Event("error")); vi.advanceTimersByTime(0); const second = links()[0]; fail(); vi.advanceTimersByTime(999); expect(links()[0]).toBe(second); vi.advanceTimersByTime(1); const third = links()[0]; expect(third).not.toBe(second); fail(); vi.advanceTimersByTime(2999); expect(links()[0]).toBe(third); vi.advanceTimersByTime(1); const fourth = links()[0]; expect(fourth).not.toBe(third); fail(); vi.runAllTimers(); expect(links()[0]).toBe(fourth); expect(logged()).toEqual(["app.stylesheet_lost", "app.stylesheet_gave_up"]); }); });