diff --git a/src/lib/__tests__/emailHtml.test.ts b/src/lib/__tests__/emailHtml.test.ts index 325b261..562233b 100644 --- a/src/lib/__tests__/emailHtml.test.ts +++ b/src/lib/__tests__/emailHtml.test.ts @@ -11,6 +11,35 @@ function documentWith(body: string, head = "") { return `${head}${body}`; } +function createStaticEmailDom(html: string, virtualConsole?: VirtualConsole) { + return new JSDOM(html, { + runScripts: "dangerously", + pretendToBeVisual: true, + virtualConsole, + beforeParse(window) { + // These tests exercise synchronous document setup, not iframe resizing. + // Leaving the real visual timers running lets a queued resize callback + // outlive JSDOM's window on slower CI workers. + window.requestAnimationFrame = (() => 1) as typeof window.requestAnimationFrame; + Object.defineProperty(window, "MutationObserver", { + configurable: true, + value: undefined, + }); + Object.defineProperty(window, "ResizeObserver", { + configurable: true, + value: undefined, + }); + }, + }); +} + +async function waitForWindowLoad(dom: JSDOM) { + if (dom.window.document.readyState === "complete") return; + await new Promise((resolve) => { + dom.window.addEventListener("load", () => resolve(), { once: true }); + }); +} + describe("prepareHtml", () => { it("injects into an existing head without losing its attributes", () => { const result = prepareHtml( @@ -166,7 +195,10 @@ describe("prepareHtml", () => { }); it("darkens near-white canvases without changing image-backed or coloured sections", async () => { - const dom = new JSDOM( + const scriptErrors: Error[] = []; + const virtualConsole = new VirtualConsole(); + virtualConsole.on("jsdomError", (error) => scriptErrors.push(error)); + const dom = createStaticEmailDom( prepareHtml( documentWith( [ @@ -180,43 +212,46 @@ describe("prepareHtml", () => { ), { colorMode: "dark" }, ), - { runScripts: "dangerously", pretendToBeVisual: true }, + virtualConsole, ); - await new Promise((resolve) => setTimeout(resolve, 0)); + try { + await waitForWindowLoad(dom); - const canvas = dom.window.document.querySelector("#canvas"); - const lightCopy = dom.window.document.querySelector("#light-copy"); - const darkCopy = dom.window.document.querySelector("#dark-copy"); - const imagePanel = dom.window.document.querySelector("#image-panel"); - const brandPanel = dom.window.document.querySelector("#brand-panel"); + const canvas = dom.window.document.querySelector("#canvas"); + const lightCopy = dom.window.document.querySelector("#light-copy"); + const darkCopy = dom.window.document.querySelector("#dark-copy"); + const imagePanel = dom.window.document.querySelector("#image-panel"); + const brandPanel = dom.window.document.querySelector("#brand-panel"); - assert.equal( - canvas?.getAttribute("data-email-client-adapted-background"), - "true", - ); - assert.equal( - lightCopy?.hasAttribute("data-email-client-adapted-text"), - false, - ); - assert.equal( - darkCopy?.getAttribute("data-email-client-adapted-text"), - "true", - ); - assert.equal( - imagePanel?.hasAttribute("data-email-client-adapted-background"), - false, - ); - assert.equal( - imagePanel?.hasAttribute("data-email-client-adapted-text"), - false, - ); - assert.equal( - brandPanel?.hasAttribute("data-email-client-adapted-background"), - false, - ); - - dom.window.close(); + assert.equal( + canvas?.getAttribute("data-email-client-adapted-background"), + "true", + ); + assert.equal( + lightCopy?.hasAttribute("data-email-client-adapted-text"), + false, + ); + assert.equal( + darkCopy?.getAttribute("data-email-client-adapted-text"), + "true", + ); + assert.equal( + imagePanel?.hasAttribute("data-email-client-adapted-background"), + false, + ); + assert.equal( + imagePanel?.hasAttribute("data-email-client-adapted-text"), + false, + ); + assert.equal( + brandPanel?.hasAttribute("data-email-client-adapted-background"), + false, + ); + assert.deepEqual(scriptErrors, []); + } finally { + dom.window.close(); + } }); it("never lets theme adaptation block iframe sizing", async () => { @@ -280,18 +315,21 @@ describe("prepareHtml", () => { }, ); - await new Promise((resolve) => setTimeout(resolve, 0)); - assert.ok( - resizeMessages.some( - (message) => - typeof message === "object" && - message !== null && - "type" in message && - message.type === "iframe-resize", - ), - ); - assert.deepEqual(scriptErrors, []); - dom.window.close(); + try { + await waitForWindowLoad(dom); + assert.ok( + resizeMessages.some( + (message) => + typeof message === "object" && + message !== null && + "type" in message && + message.type === "iframe-resize", + ), + ); + assert.deepEqual(scriptErrors, []); + } finally { + dom.window.close(); + } }); it("measures full scroll geometry and batches updates in animation frames", () => { @@ -306,28 +344,30 @@ describe("prepareHtml", () => { }); it("mirrors a missing trailing spacer in a centered email wrapper", async () => { - const dom = new JSDOM( + const dom = createStaticEmailDom( prepareHtml( documentWith( '
 

Message

', "", ), ), - { runScripts: "dangerously", pretendToBeVisual: true }, ); - await new Promise((resolve) => setTimeout(resolve, 0)); - const cells = dom.window.document.querySelectorAll( - 'table[width="100%"] > tbody > tr > td', - ); - assert.equal(cells.length, 3); - assert.equal(cells[2].className, "device-width"); - assert.equal( - cells[2].getAttribute("data-email-client-mirrored-spacer"), - "true", - ); - assert.equal(cells[2].getAttribute("aria-hidden"), "true"); - dom.window.close(); + try { + await waitForWindowLoad(dom); + const cells = dom.window.document.querySelectorAll( + 'table[width="100%"] > tbody > tr > td', + ); + assert.equal(cells.length, 3); + assert.equal(cells[2].className, "device-width"); + assert.equal( + cells[2].getAttribute("data-email-client-mirrored-spacer"), + "true", + ); + assert.equal(cells[2].getAttribute("aria-hidden"), "true"); + } finally { + dom.window.close(); + } }); describe("links", () => {