Preserve video thumbnail height and verify social target outputs
Quality / quality (ubuntu-latest) (push) Failing after 1s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 18:43:48 +05:30
1 parent df382ed550
commit 0fa3c01a16
3 files changed
+41 -2

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/email-renderer", "name": "@wrnexus/email-renderer",
"version": "0.1.38", "version": "0.1.39",
"type": "module", "type": "module",
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.", "description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
"exports": { "exports": {
+1 -1
View File
@@ -460,7 +460,7 @@ export const videoBlock = leaf(
{ {
emailHtml: ({ node }) => emailCell(`<a href="${html(attrs(node).href)}" aria-label="${html(attrs(node).title)}"><img src="${html(attrs(node).thumbnail)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}" height="${html(attrs(node).height)}" style="border:0;display:block;height:auto;width:100%;max-width:${html(attrs(node).width)}px;" /></a>`, node), emailHtml: ({ node }) => emailCell(`<a href="${html(attrs(node).href)}" aria-label="${html(attrs(node).title)}"><img src="${html(attrs(node).thumbnail)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}" height="${html(attrs(node).height)}" style="border:0;display:block;height:auto;width:100%;max-width:${html(attrs(node).width)}px;" /></a>`, node),
ampEmail: ({ node }) => emailCell(`<a href="${html(attrs(node).href)}" aria-label="${html(attrs(node).title)}"><amp-img src="${html(attrs(node).thumbnail)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}" height="${html(attrs(node).height)}" layout="responsive"></amp-img></a>`, node), ampEmail: ({ node }) => emailCell(`<a href="${html(attrs(node).href)}" aria-label="${html(attrs(node).title)}"><amp-img src="${html(attrs(node).thumbnail)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}" height="${html(attrs(node).height)}" layout="responsive"></amp-img></a>`, node),
mjml: ({ node }) => `<mj-image href="${html(attrs(node).href)}" src="${html(attrs(node).thumbnail)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}px" />`, mjml: ({ node }) => `<mj-image href="${html(attrs(node).href)}" src="${html(attrs(node).thumbnail)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}px" height="${html(attrs(node).height)}px" />`,
}, },
({ node }) => `${String(attrs(node).title)}: ${String(attrs(node).href)}\n\n`, ({ node }) => `${String(attrs(node).title)}: ${String(attrs(node).href)}\n\n`,
); );
@@ -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&amp;b=2"');
expect(output).toContain('src="https://example.com/poster.png"');
expect(output).toContain('alt="Demo &amp; &quot;preview&quot;"');
}
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 &amp; connect");
expect(output).toContain('href="https://example.com/facebook?a=1&amp;b=2"');
expect(output).toContain('href="https://example.com/linkedin"');
expect(output).not.toContain(">Instagram</a>");
expect(output).not.toContain(">X</a>");
}
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 () => { test("basic images, video thumbnails and spacers validate dimensions before rendering", async () => {
const cases: Array<{ type: string; props: Record<string, string> }> = [ const cases: Array<{ type: string; props: Record<string, string> }> = [
{ type: "content.image", props: { src: "https://example.com/image.png", alt: "Image" } }, { type: "content.image", props: { src: "https://example.com/image.png", alt: "Image" } },