import { expect, test } from "bun:test"; import type { BlockDocument } from "@wrnexus/blocks"; import { renderEmail, validateMjml } from "../src/index.ts"; function document(type: string, props: Record): BlockDocument { return { schemaVersion: 1, kind: "email", meta: { subject: "Store update", lang: "en" }, theme: { tokens: {} }, root: { id: "root", type: "layout.email-document", version: 1, props: { width: "600" }, styles: {}, slots: { content: [ { id: "section", type: "layout.section", version: 1, props: { background: "#ffffff", padding: "24px" }, styles: {}, slots: { content: [{ id: "item", type, version: 1, props, styles: {}, slots: {} }] }, }, ], }, }, }; } test("product and RSS images survive HTML, AMP and strict MJML exports", async () => { for (const type of ["commerce.product", "content.feed-item"]) { const props: Record = type === "commerce.product" ? { title: "Tea & coffee", image: "https://example.com/product.png", href: "https://example.com/product", price: "$12.00", buttonLabel: "Shop now", width: "320", height: "180", } : { title: "Tea & coffee", image: "https://example.com/article.png", href: "https://example.com/article", summary: "Latest news", width: "320", height: "180", }; const result = await renderEmail(document(type, props), { amp: true, mjml: true }); expect(result.ok).toBe(true); if (!result.ok) continue; expect(result.value.parts.html.content).toContain("width:100%;max-width:320px;"); for (const output of [ result.value.parts.html.content, result.value.parts.amp?.content, result.value.exports?.mjml, ]) { expect(output).toContain(props.image); expect(output).toContain(props.href); expect(output).toContain("Tea & coffee"); if (type === "commerce.product") expect(output).toContain("$12.00"); } expect(result.value.parts.amp?.validation.valid).toBe(true); expect(result.value.parts.amp?.content).toContain( 'width="320" height="180" layout="responsive"', ); expect(result.value.parts.amp?.content).toContain(".marketing-image img{object-fit:contain}"); expect(await validateMjml(result.value.exports!.mjml!)).toEqual([]); } }); test("image-free RSS and older product documents remain renderable", async () => { const rss = await renderEmail( document("content.feed-item", { title: "News", summary: "Update", href: "https://example.com", image: "", }), { amp: true, mjml: true }, ); expect(rss.ok).toBe(true); if (rss.ok) expect(rss.value.parts.amp?.content).not.toContain(" { for (const width of ["0", "4001", "100%", '600" onerror="alert(1)']) { const result = await renderEmail( document("commerce.product", { title: "Product", price: "12", image: "https://example.com/p.png", href: "https://example.com", width, }), ); expect(result.ok).toBe(false); } }); test("basic images, video thumbnails and spacers validate dimensions before rendering", async () => { const cases: Array<{ type: string; props: Record }> = [ { type: "content.image", props: { src: "https://example.com/image.png", alt: "Image" } }, { type: "content.video", props: { href: "https://example.com/video", thumbnail: "https://example.com/image.png", title: "Video", alt: "Video", }, }, { type: "content.spacer", props: {} }, ]; for (const block of cases) { for (const height of ["-1", "4001", "100%", "24;position:fixed", '24" onload="bad', "1e3"]) { const result = await renderEmail( document(block.type, { ...block.props, height } as Record), ); expect(result.ok).toBe(false); } const valid = await renderEmail( document(block.type, { ...block.props, height: "4000" } as Record), ); expect(valid.ok).toBe(true); } expect((await renderEmail(document("content.spacer", { height: "0" }))).ok).toBe(true); expect( ( await renderEmail( document("content.image", { src: "https://example.com/image.png", height: "0" }), ) ).ok, ).toBe(false); });