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 = `
`;
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 = `
`;
const exported = await renderToWechatHtml(fullDeck);
expect(exported).toContain("Slide one");
expect(exported).toContain("Slide two");
expect(exported).toContain("color: rgb(1, 2, 3)");
});
});