import { describe, it, expect, afterEach } from "vitest"; import { renderToWechatHtml, toWechatHtmlFromDocument } from "../wechat"; function parseFragment(html: string): HTMLBodyElement { return new DOMParser().parseFromString(`${html}`, "text/html") .body as HTMLBodyElement; } describe("toWechatHtmlFromDocument", () => { afterEach(() => { document.head.innerHTML = ""; document.body.innerHTML = ""; }); it("inlines computed styles from the rendered preview DOM", () => { document.head.innerHTML = ` `; document.body.innerHTML = `

Styled headline

`; const body = parseFragment(toWechatHtmlFromDocument(document)); const section = body.querySelector("section"); const card = body.querySelector("article"); const title = body.querySelector("h1"); expect(section?.getAttribute("data-tool")).toBe("html-anything"); expect(card?.getAttribute("data-tool")).toBe("html-anything"); expect(card?.getAttribute("style")).toContain("background-color: rgb(12, 34, 56)"); expect(card?.getAttribute("style")).toContain("border-radius: 18px"); expect(card?.getAttribute("style")).toContain("padding: 24px"); expect(title?.getAttribute("style")).toContain("color: rgb(210, 55, 44)"); expect(title?.getAttribute("style")).toContain("font-size: 32px"); expect(title?.getAttribute("style")).toContain("font-weight: 800"); }); it("drops fragile page-layout styles so WeChat paste stays in article flow", () => { document.head.innerHTML = ` `; document.body.innerHTML = `

First paragraph

Second paragraph

`; const html = toWechatHtmlFromDocument(document); const body = parseFragment(html); const layoutStyle = body.querySelector(".layout")?.getAttribute("style") ?? ""; const panelStyle = body.querySelector(".panel")?.getAttribute("style") ?? ""; expect(layoutStyle).toContain("background-color: rgb(250, 248, 240)"); expect(layoutStyle).toContain("padding: 40px"); expect(panelStyle).toContain("color: rgb(25, 28, 32)"); expect(panelStyle).toContain("border-top: 2px solid rgb(80, 90, 100)"); expect(`${layoutStyle}; ${panelStyle}`).not.toMatch( /(?:^|;\s*)(position|display|grid-template-columns|width|height|min-height|gap|flex-direction|flex-wrap|flex):/, ); }); it("materializes ::before and ::after content into real DOM nodes", () => { document.body.innerHTML = `

Pro plan

`; const original = window.getComputedStyle.bind(window); const stub = ((el: Element, pseudo?: string | null) => { const base = original(el); if (!pseudo) return base; const overrides: Record = {}; if (pseudo === "::before" && (el as HTMLElement).matches?.(".check")) { overrides.content = '"✓"'; overrides.color = "rgb(255, 0, 0)"; } else if (pseudo === "::before" && (el as HTMLElement).matches?.(".tier")) { overrides.content = '"Recommended"'; overrides.color = "rgb(255, 255, 255)"; } else if (pseudo === "::after" && (el as HTMLElement).matches?.(".tier")) { overrides.content = '"★"'; } return new Proxy(base, { get(target, prop) { if (prop === "getPropertyValue") { return (name: string) => overrides[name] ?? target.getPropertyValue(name); } const value = (target as unknown as Record)[prop]; return typeof value === "function" ? (value as () => unknown).bind(target) : value; }, }); }) as typeof window.getComputedStyle; window.getComputedStyle = stub; try { const body = parseFragment(toWechatHtmlFromDocument(document)); const check = body.querySelector("li.check"); const tier = body.querySelector("p.tier"); const checkBefore = check?.firstElementChild; expect(checkBefore?.getAttribute("data-pseudo")).toBe("::before"); expect(checkBefore?.textContent).toBe("✓"); expect(checkBefore?.getAttribute("style") ?? "").toContain("color: rgb(255, 0, 0)"); const tierBefore = tier?.firstElementChild; expect(tierBefore?.getAttribute("data-pseudo")).toBe("::before"); expect(tierBefore?.textContent).toBe("Recommended"); const tierAfter = tier?.lastElementChild; expect(tierAfter?.getAttribute("data-pseudo")).toBe("::after"); expect(tierAfter?.textContent).toBe("★"); } finally { window.getComputedStyle = original as typeof window.getComputedStyle; } }); it("keeps descendant styles aligned when an ancestor has generated content", () => { document.body.innerHTML = `
Hello
`; const original = window.getComputedStyle.bind(window); const stub = ((el: Element, pseudo?: string | null) => { const base = original(el); const overrides: Record = {}; if (pseudo === "::before" && (el as HTMLElement).matches?.(".card")) { overrides.content = '"Badge"'; } else if (!pseudo && (el as HTMLElement).matches?.(".label")) { overrides.color = "rgb(255, 0, 0)"; } return new Proxy(base, { get(target, prop) { if (prop === "getPropertyValue") { return (name: string) => overrides[name] ?? target.getPropertyValue(name); } const value = (target as unknown as Record)[prop]; return typeof value === "function" ? (value as () => unknown).bind(target) : value; }, }); }) as typeof window.getComputedStyle; window.getComputedStyle = stub; try { const body = parseFragment(toWechatHtmlFromDocument(document)); const card = body.querySelector(".card"); const pseudo = card?.querySelector("[data-pseudo='::before']"); const label = card?.querySelector(".label"); expect(pseudo?.textContent).toBe("Badge"); expect(pseudo?.getAttribute("style") ?? "").not.toContain("color: rgb(255, 0, 0)"); expect(label?.getAttribute("style") ?? "").toContain("color: rgb(255, 0, 0)"); } finally { window.getComputedStyle = original as typeof window.getComputedStyle; } }); it("omits computed-hidden speaker notes", () => { document.head.innerHTML = ``; document.body.innerHTML = `

Visible slide

`; const exported = toWechatHtmlFromDocument(document); expect(exported).toContain("Visible slide"); expect(exported).not.toContain("SECRET NOTES"); expect(exported).not.toContain("class=\"notes\""); }); it("clamps oversized spacing and drops negative margins", () => { document.head.innerHTML = ` `; document.body.innerHTML = `

Too much spacing

`; const body = parseFragment(toWechatHtmlFromDocument(document)); const style = body.querySelector("p")?.getAttribute("style") ?? ""; expect(style).toContain("margin-bottom: 48px"); expect(style).toContain("padding: 48px"); expect(style).toContain("color: rgb(40, 40, 40)"); expect(style).not.toContain("-24px"); expect(style).not.toContain("180px"); expect(style).not.toContain("96px"); }); it("keeps every runtime-styled deck slide in the full rendered export", async () => { const fullDeck = `

Slide one

Slide two

`; const exported = await renderToWechatHtml(fullDeck); expect(exported).toContain("Slide one"); expect(exported).toContain("Slide two"); expect(exported).toContain("color: rgb(1, 2, 3)"); }); });