diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index 8d3907bd..6f441bc0 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.38", + "version": "0.1.39", "type": "module", "description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.", "exports": { diff --git a/packages/email-renderer/src/blocks.ts b/packages/email-renderer/src/blocks.ts index 21567d1e..d736e436 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -460,7 +460,7 @@ export const videoBlock = leaf( { emailHtml: ({ node }) => emailCell(`${html(attrs(node).alt)}`, node), ampEmail: ({ node }) => emailCell(``, node), - mjml: ({ node }) => ``, + mjml: ({ node }) => ``, }, ({ node }) => `${String(attrs(node).title)}: ${String(attrs(node).href)}\n\n`, ); diff --git a/packages/email-renderer/test/marketing-images.test.ts b/packages/email-renderer/test/marketing-images.test.ts index e41698d2..a39a17a9 100644 --- a/packages/email-renderer/test/marketing-images.test.ts +++ b/packages/email-renderer/test/marketing-images.test.ts @@ -114,6 +114,45 @@ test("marketing image dimensions reject invalid or injected values", async () => } }); +test("video exports preserve thumbnail proportions, safe destination and accessible text", async () => { + const draft = document("content.video", { title: "Watch & learn", href: "https://example.com/watch?a=1&b=2", thumbnail: "https://example.com/poster.png", alt: 'Demo & "preview"', width: "360", height: "240" }); + const before = JSON.stringify(draft); + const result = await renderEmail(draft, { amp: true, mjml: true }); + expect(result.ok).toBe(true); + if (!result.ok) return; + for (const output of [result.value.parts.html.content, result.value.parts.amp?.content, result.value.exports?.mjml]) { + expect(output).toContain('href="https://example.com/watch?a=1&b=2"'); + expect(output).toContain('src="https://example.com/poster.png"'); + expect(output).toContain('alt="Demo & "preview""'); + } + expect(result.value.parts.html.content).toContain('width="360" height="240"'); + expect(result.value.parts.amp?.content).toContain('width="360" height="240" layout="responsive"'); + expect(result.value.exports?.mjml).toContain('width="360px" height="240px"'); + expect(result.value.parts.text.content).toContain("Watch & learn: https://example.com/watch?a=1&b=2"); + expect(result.value.parts.amp?.validation.valid).toBe(true); + expect(await validateMjml(result.value.exports!.mjml!)).toEqual([]); + expect(JSON.stringify(draft)).toBe(before); +}); + +test("social exports include only configured networks and retain safe links in plain text", async () => { + const props = { label: "Follow & connect", facebook: "https://example.com/facebook?a=1&b=2", linkedin: "https://example.com/linkedin", instagram: "", x: "" }; + const result = await renderEmail(document("content.social", props), { amp: true, mjml: true }); + expect(result.ok).toBe(true); + if (!result.ok) return; + for (const output of [result.value.parts.html.content, result.value.parts.amp?.content, result.value.exports?.mjml]) { + expect(output).toContain("Follow & connect"); + expect(output).toContain('href="https://example.com/facebook?a=1&b=2"'); + expect(output).toContain('href="https://example.com/linkedin"'); + expect(output).not.toContain(">Instagram"); + expect(output).not.toContain(">X"); + } + expect(result.value.parts.text.content).toContain("Facebook: https://example.com/facebook?a=1&b=2"); + expect(result.value.parts.text.content).toContain("LinkedIn: https://example.com/linkedin"); + expect(result.value.parts.amp?.validation.valid).toBe(true); + expect(await validateMjml(result.value.exports!.mjml!)).toEqual([]); + expect((await renderEmail(document("content.social", { ...props, facebook: "javascript:alert(1)" }))).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" } },