// @vitest-environment jsdom import { afterEach, beforeAll, describe, expect, it } from "vitest"; import { clear, events, setLevel } from "./log"; import { installClientLog } from "./logHooks"; // History is wrapped ONCE per window (installClientLog is idempotent), so this file installs once // and every test reads only what it caused. // The app passes i18n/appCopy.ts; here only "Frissítés" (and "Refresh") is app copy, read as "Refresh". const appCopy = (text: string | null) => ["Refresh", "Frissítés"].includes(text?.trim() ?? "") ? "Refresh" : null; beforeAll(() => { installClientLog(appCopy); installClientLog(appCopy); // a hot reload's second call must not wrap History twice expect(events().map((e) => e.event)).toEqual(["app.start", "nav.view"]); clear(); }); afterEach(() => { setLevel("INFO"); window.history.replaceState(null, "", "/"); clear(); }); const views = () => events().filter((e) => e.event === "nav.view"); describe("client log hooks", () => { it("records a page change once, and not a replaceState that only saves a scroll position", () => { window.history.pushState({ sfPage: "feed" }, "", "/?show=all"); window.history.replaceState({ sfPage: "feed", sfScroll: 120 }, ""); window.history.replaceState({ sfPage: "feed", sfScroll: 480 }, ""); expect(views()).toHaveLength(1); expect(views()[0]!.attrs).toMatchObject({ via: "push", page: "feed", path: "/", params: ["show"], overlay: false, }); }); it("keeps parameter NAMES at the base level, values at DEBUG, typed search only at TRACE", () => { window.history.pushState({ sfPage: "feed" }, "", "/?tags=music&q=lofi"); expect(views()[0]!.attrs).not.toHaveProperty("values"); expect(JSON.stringify(views())).not.toContain("lofi"); setLevel("DEBUG"); window.history.pushState({ sfPage: "plex" }, "", "/?tags=music&q=lofi"); expect(views()[1]!.attrs!.values).toEqual({ tags: "music" }); setLevel("TRACE"); window.history.pushState({ sfPage: "feed", _yt: "lofi" }, "", "/?tags=music&q=lofi"); expect(views()[2]!.attrs).toMatchObject({ values: { tags: "music", q: "lofi" }, query: "lofi", }); }); it("records an overlay opening as a view of its own, and Back as a pop", () => { window.history.pushState({ sfPage: "feed", _ov: 1 }, ""); // What Back does: the browser restores the previous entry's state (the unwrapped prototype // method, as the browser itself would not go through the app's wrapper), then fires popstate. History.prototype.replaceState.call(window.history, { sfPage: "feed" }, ""); window.dispatchEvent(new PopStateEvent("popstate")); expect(views().map((e) => [e.attrs!.via, e.attrs!.overlay])).toEqual([ ["push", true], ["pop", false], ]); }); it("never records a token that rides in the address", () => { window.history.replaceState( { sfPage: "welcome" }, "", "/watch/Ab3xYz9Tok/unlock?token=zzz#reset=yyy", ); setLevel("TRACE"); window.history.pushState( { sfPage: "welcome2" }, "", "/watch/Ab3xYz9Tok/unlock?token=zzz#reset=yyy", ); const text = JSON.stringify(views()); expect(text).not.toContain("Ab3xYz9Tok"); expect(text).not.toContain("zzz"); expect(text).not.toContain("yyy"); }); it("records uncaught errors and unhandled rejections", () => { window.dispatchEvent( new ErrorEvent("error", { error: new TypeError("x is undefined"), message: "x is undefined", filename: "http://localhost/assets/index-B1x9.js", lineno: 3, colno: 7, }), ); const rejection = new Event("unhandledrejection") as Event & { reason: unknown }; rejection.reason = new Error("nope"); window.dispatchEvent(rejection); window.dispatchEvent(new Event("error")); // a failed load: not a script fault expect(events().map((e) => [e.event, e.level])).toEqual([ ["app.uncaught", "ERROR"], ["app.unhandled_rejection", "ERROR"], ]); expect(events()[0]!.attrs).toMatchObject({ error: "TypeError", where: "/assets/index-B1x9.js:3:7", }); }); }); describe("control capture (a raised diagnostics level only)", () => { const clicks = () => events().filter((e) => e.event === "ui.click"); afterEach(() => { document.body.innerHTML = ""; }); it("records nothing at the base level", () => { document.body.innerHTML = ``; document.querySelector("button")!.click(); expect(clicks()).toEqual([]); }); it("names a control by its test id, icon, app-copy label and state", () => { setLevel("DEBUG"); window.history.replaceState({ sfPage: "feed" }, ""); document.body.innerHTML = `
`; document.querySelector("svg")!.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(clicks().map((e) => e.attrs)).toEqual([ { tag: "button", within: "feed-toolbar", icon: "refresh-cw", label: "Refresh", pressed: "true", page: "feed", }, ]); }); it("never records what the user wrote: a title on a card, an input's value, a link's query", () => { setLevel("TRACE"); document.body.innerHTML = ` Some video title elsewhere
x
`; for (const el of document.querySelectorAll("button, input, a, [role=button]")) { el.addEventListener("click", (e) => e.preventDefault()); (el as HTMLElement).click(); } const text = JSON.stringify(clicks()); for (const secret of [ "holiday", "typed private", "Ab3xYz9Tok", "zzz", "Some video", "someone", ]) { expect(text).not.toContain(secret); } expect(clicks().map((e) => e.attrs)).toMatchObject([ { control: "video-card" }, { tag: "input", type: "text" }, { tag: "input", type: "checkbox", checked: false }, // the state the click is turning it to { tag: "a", href: "/watch/{}" }, { tag: "a", href: "external" }, { tag: "div", role: "button" }, ]); }); });