import { expect, test } from "./helpers/instrumentedTest"; test.describe("runtime UI utility coverage", () => { test.beforeEach(async ({ page }) => { await page.goto("/docs/kitchen-sink.html?__test__=1", { waitUntil: "networkidle", }); }); test("exercises dynamic style scope registration and lifecycle", async ({ page }) => { const result = await page.evaluate(async () => { const { registerScope, setVariables, releaseScope, getScopeAttributeName, } = await import("/js/designSystem/dynamicStyles.js"); const firstScope = registerScope("coverage-scope", [":scope", "& .title", "> .child"]); const secondScope = registerScope("coverage-scope", [":scope"]); const firstUpdate = setVariables(firstScope, { "--card-gap": "12px", "--muted": "", invalid: "skip", }); const secondUpdate = setVariables(firstScope, { "--card-gap": 8, }); const missingScopeUpdate = setVariables("missing-scope", { "--x": "1" }); const releaseMissing = releaseScope("missing-scope"); const releaseFirst = releaseScope(firstScope); const releaseSecond = releaseScope(secondScope); return { firstScope, secondScope, firstUpdate, secondUpdate, missingScopeUpdate, releaseMissing, releaseFirst, releaseSecond, scopeAttr: getScopeAttributeName(), styleTagCount: document.querySelectorAll("style[data-ds-dynamic='true']").length, adoptedSheetCount: Array.isArray((document as any).adoptedStyleSheets) ? (document as any).adoptedStyleSheets.length : 0, }; }); expect(result.firstScope).toBe("coverage-scope"); expect(result.secondScope).toBe("coverage-scope-1"); expect(result.firstUpdate).toBe(true); expect(result.secondUpdate).toBe(true); expect(result.missingScopeUpdate).toBe(false); expect(result.releaseMissing).toBe(false); expect(result.releaseFirst).toBe(true); expect(result.releaseSecond).toBe(true); expect(result.scopeAttr).toBe("data-ds-style-id"); expect(result.styleTagCount + result.adoptedSheetCount).toBeGreaterThanOrEqual(1); }); test("exercises tag preference menu rendering, actions, and state transitions", async ({ page, }) => { const result = await page.evaluate(async () => { const { createTagPreferenceMenu, applyTagPreferenceMenuState, TAG_PREFERENCE_ACTIONS, } = await import("/js/ui/components/tagPreferenceMenu.js"); const actionEvents: Array<{ action: string; tag: string }> = []; const loggedOut = createTagPreferenceMenu({ tag: "#BitVid", isLoggedIn: false, }); document.body.appendChild(loggedOut!.panel); const loggedIn = createTagPreferenceMenu({ tag: " nostr ", isLoggedIn: true, membership: { state: "interest" }, onAction: (action: string, detail: any) => { actionEvents.push({ action, tag: detail.normalizedTag }); }, }); document.body.appendChild(loggedIn!.panel); loggedIn!.buttons.removeInterest.click(); loggedIn!.buttons.addDisinterest.click(); applyTagPreferenceMenuState({ buttons: loggedIn!.buttons, membership: { state: "disinterest", interest: true, disinterest: true }, isLoggedIn: true, }); return { actions: TAG_PREFERENCE_ACTIONS, loggedOutTag: loggedOut!.panel.dataset.tag, loggedOutMessage: loggedOut!.panel.textContent, loggedOutDisabled: { addInterest: loggedOut!.buttons.addInterest.disabled, removeInterest: loggedOut!.buttons.removeInterest.disabled, addDisinterest: loggedOut!.buttons.addDisinterest.disabled, removeDisinterest: loggedOut!.buttons.removeDisinterest.disabled, }, loggedInTag: loggedIn!.panel.dataset.tag, loggedInDisabledAfterTransition: { addInterest: loggedIn!.buttons.addInterest.disabled, removeInterest: loggedIn!.buttons.removeInterest.disabled, addDisinterest: loggedIn!.buttons.addDisinterest.disabled, removeDisinterest: loggedIn!.buttons.removeDisinterest.disabled, }, actionEvents, }; }); expect(result.actions.ADD_INTEREST).toBe("add-interest"); expect(result.loggedOutTag).toBe("bitvid"); expect(result.loggedInTag).toBe("nostr"); expect(result.loggedOutMessage).toContain("Sign in to personalize"); expect(result.loggedOutDisabled).toEqual({ addInterest: true, removeInterest: true, addDisinterest: true, removeDisinterest: true, }); expect(result.actionEvents.map((entry: any) => entry.action)).toEqual([ "remove-interest", "add-disinterest", ]); expect(result.actionEvents.every((entry: any) => entry.tag === "nostr")).toBe(true); expect(result.loggedInDisabledAfterTransition).toEqual({ addInterest: false, removeInterest: true, addDisinterest: true, removeDisinterest: false, }); }); test("exercises docs view initialization, hash routing, drawer toggle, and error handling", async ({ page, }) => { const result = await page.evaluate(async () => { document.body.innerHTML = `
`; (window as any).marked = { parse(markdown: string) { if (markdown.includes("SECOND_DOC")) { return '

Advanced

Second doc

Advanced'; } return '

Intro

const x = 1;
Intro'; }, }; const highlighted: string[] = []; (window as any).hljs = { highlightElement(el: Element) { highlighted.push(el.textContent || ""); }, }; const realFetch = window.fetch.bind(window); const fetchLog: string[] = []; window.fetch = (async (input: RequestInfo | URL) => { const url = String(input); fetchLog.push(url); if (url.endsWith("content/docs/toc.json")) { return new Response( JSON.stringify({ items: [ { slug: "intro", title: "Intro", path: "/content/docs/intro.md", children: [ { slug: "intro-nested", title: "Nested", path: "/content/docs/nested.md", }, ], }, { slug: "advanced", title: "Advanced", path: "/content/docs/advanced.md", }, ], }), { status: 200, headers: { "Content-Type": "application/json" } }, ); } if (url.endsWith("/content/docs/intro.md")) { return new Response("INTRO_DOC", { status: 200 }); } if (url.endsWith("/content/docs/advanced.md")) { return new Response("SECOND_DOC", { status: 200 }); } return realFetch(input); }) as typeof fetch; const { initDocsView } = await import("/js/docsView.js"); await initDocsView(); const initialTitle = document.title; const initialHash = window.location.hash; const topLevelLinks = Array.from(document.querySelectorAll("#docsTocList [data-docs-toc-item]")).map( (el) => (el as HTMLElement).dataset.slug, ); const drawerLinks = Array.from(document.querySelectorAll("#docsTocDrawerNav [data-docs-toc-item]")).map( (el) => (el as HTMLElement).dataset.slug, ); const toggle = document.querySelector("[data-docs-toc-toggle]") as HTMLButtonElement; const drawer = document.getElementById("docsTocDrawer") as HTMLElement; const backdrop = drawer.querySelector(".bv-modal-backdrop") as HTMLElement; toggle.click(); await new Promise((resolve) => setTimeout(resolve, 0)); const drawerOpen = drawer.getAttribute("data-open"); const toggleExpandedOpen = toggle.getAttribute("aria-expanded"); backdrop.click(); await new Promise((resolve) => setTimeout(resolve, 0)); const drawerClosed = drawer.getAttribute("data-open"); window.location.hash = "#view=docs&doc=advanced"; window.dispatchEvent(new HashChangeEvent("hashchange")); const waitForCondition = async (predicate: () => boolean, timeoutMs = 3000) => { const started = Date.now(); while (Date.now() - started < timeoutMs) { if (predicate()) { return true; } await new Promise((resolve) => setTimeout(resolve, 25)); } return false; }; await waitForCondition(() => { const html = document.getElementById("markdown-container")?.innerHTML || ""; return document.title === "Advanced | bitvid docs" && html.includes("Second doc"); }); const advancedTitle = document.title; const markdownHtml = document.getElementById("markdown-container")?.innerHTML || ""; window.fetch = (async (input: RequestInfo | URL) => { const url = String(input); if (url.endsWith("content/docs/toc.json")) { return new Response("broken", { status: 500 }); } return realFetch(input); }) as typeof fetch; document.getElementById("docsTocList")!.innerHTML = ""; document.getElementById("markdown-container")!.innerHTML = ""; await initDocsView(); const tocErrorText = document.getElementById("docsTocList")?.textContent || ""; const markdownErrorText = document.getElementById("markdown-container")?.textContent || ""; window.fetch = realFetch; return { initialTitle, initialHash, advancedTitle, markdownHtml, topLevelLinks, drawerLinks, drawerOpen, drawerClosed, toggleExpandedOpen, fetchLog, highlighted, tocErrorText, markdownErrorText, }; }); expect(result.initialTitle).toBe("Intro | bitvid docs"); expect(result.initialHash).toContain("view=docs&doc=intro"); expect(result.advancedTitle).toBe("Advanced | bitvid docs"); expect(result.markdownHtml).toContain("Second doc"); expect(result.topLevelLinks).toContain("intro"); expect(result.topLevelLinks).toContain("advanced"); expect(result.drawerLinks).toContain("intro"); expect(result.drawerOpen).toBe("true"); expect(result.drawerClosed).toBe("false"); expect(result.toggleExpandedOpen).toBe("true"); expect(result.fetchLog.some((url: string) => url.endsWith("content/docs/toc.json"))).toBe(true); expect(result.fetchLog.some((url: string) => url.endsWith("/content/docs/intro.md"))).toBe(true); expect(result.fetchLog.some((url: string) => url.endsWith("/content/docs/advanced.md"))).toBe(true); expect(result.highlighted.length).toBeGreaterThan(0); expect(result.tocErrorText).toContain("Unable to load docs table of contents"); expect(result.markdownErrorText).toContain("Error loading content"); }); });