A lost stylesheet left the page unstyled until a reload, and every e2e step that finds elements by testid still passed on it. Only axe noticed: the one-off target-size failure on eight scattered controls. Losing the main stylesheet on purpose gives those same eight.
106 lines
3.2 KiB
TypeScript
106 lines
3.2 KiB
TypeScript
// @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<HTMLLinkElement>('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"]);
|
|
});
|
|
});
|