115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
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<string, string>): 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<string, string> =
|
|
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;
|
|
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("<amp-img");
|
|
const product = await renderEmail(
|
|
document("commerce.product", {
|
|
title: "Product",
|
|
price: "12",
|
|
image: "https://example.com/p.png",
|
|
href: "https://example.com",
|
|
}),
|
|
{ amp: true },
|
|
);
|
|
expect(product.ok).toBe(true);
|
|
if (product.ok) expect(product.value.parts.amp?.content).toContain('width="600" height="400"');
|
|
});
|
|
|
|
test("marketing image dimensions reject invalid or injected values", async () => {
|
|
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);
|
|
}
|
|
});
|