Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

283 lines
8.7 KiB
TypeScript

import { PortableText } from "astro-portabletext";
/**
* Renders WordPress-migrated image nodes through astro-portabletext using
* the SAME dispatch production uses (type.image -> Image.astro), then pins
* the local, provider, placeholder, and public Image component edge cases.
*/
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { describe, expect, test } from "vitest";
import EmDashImage from "../../src/components/EmDashImage.astro";
import Image from "../../src/components/Image.astro";
import OverrideImage from "./OverrideImage.astro";
const testMediaProviders = [
{
id: "mock-images",
name: "Mock Images",
capabilities: { list: false, upload: false, delete: false, metadata: false },
createProvider: () => ({
id: "mock-images",
name: "Mock Images",
capabilities: { list: false, upload: false, delete: false, metadata: false },
getEmbed: (_value: unknown, options: { width?: number; height?: number } = {}) => ({
type: "image",
src: `https://img.example.com/original?w=${options.width ?? "auto"}`,
getSrc: ({ width, height }: { width?: number; height?: number } = {}) =>
`https://img.example.com/render?w=${width ?? "auto"}&h=${height ?? "auto"}`,
}),
}),
},
];
(
globalThis as typeof globalThis & { __emdashTestMediaProviders?: typeof testMediaProviders }
).__emdashTestMediaProviders = testMediaProviders;
const node = {
_type: "image",
_key: "img66",
asset: {
_ref: "01KTRTJ5QSVC3TB57387DX445P",
url: "/_emdash/api/media/file/01KTRTJ55S65SADEH9P9TSY89H.png",
},
alt: "",
alignment: "right",
displayWidth: 136,
displayHeight: 201,
};
const value = [node];
const locals = {
emdash: { getPublicMediaUrl: (k: string) => `/_emdash/api/media/file/${k}` },
};
const imgSrc = (html: string) => html.match(/<img[^>]*\bsrc="([^"]*)"/)?.[1] ?? "(no <img>)";
const imgTag = (html: string) => html.match(/<img\b[^>]*>/)?.[0] ?? "";
const attr = (tag: string, name: string) =>
tag.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1]?.replaceAll("&amp;", "&");
const compact = (html: string) => html.replace(/\s+/g, " ").trim();
async function renderImage(
renderNode: Record<string, unknown>,
renderLocals: typeof locals | undefined = locals,
placeholder = true,
) {
const c = await AstroContainer.create();
return c.renderToString(Image, {
props: { node: renderNode, placeholder },
...(renderLocals ? { locals: renderLocals } : {}),
});
}
async function renderEmDashImage(props: Record<string, unknown>) {
const c = await AstroContainer.create();
return c.renderToString(EmDashImage, { props, locals });
}
describe("faithful render of migrated image node", () => {
test("default type.image=Image.astro", async () => {
const c = await AstroContainer.create();
const html = await c.renderToString(PortableText, {
props: { value, components: { type: { image: Image } } },
locals,
});
expect(imgSrc(html)).not.toBe("(no <img>)");
// #1404 fix: alignment now rendered as a figure class
expect(html).toContain("emdash-image--align-right");
});
test("delegating override -> emdash <Image>", async () => {
const c = await AstroContainer.create();
const html = await c.renderToString(PortableText, {
props: { value, components: { type: { image: OverrideImage } } },
locals,
});
expect(imgSrc(html)).not.toBe("(no <img>)");
});
test("default render with locals.emdash ABSENT", async () => {
const c = await AstroContainer.create();
const html = await c.renderToString(PortableText, {
props: { value, components: { type: { image: Image } } },
});
expect(imgSrc(html)).not.toBe("(no <img>)");
});
test("local image keeps wrapper, dimensions, alt, caption and alignment", async () => {
const html = await renderImage({
...node,
alt: "Migrated image",
caption: "A caption",
alignment: "center",
width: 1200,
height: 800,
displayWidth: 600,
displayHeight: undefined,
});
const tag = imgTag(html);
expect(compact(html)).toContain('<figure class="emdash-image emdash-image--align-center"');
expect(attr(tag, "src")).toContain("/_emdash/api/media/file/01KTRTJ55S65SADEH9P9TSY89H.png");
expect(attr(tag, "alt")).toBe("Migrated image");
expect(attr(tag, "width")).toBe("600");
expect(attr(tag, "height")).toBe("400");
expect(attr(tag, "loading")).toBe("lazy");
expect(attr(tag, "decoding")).toBe("async");
expect(attr(tag, "data-astro-image")).toBe("constrained");
expect(attr(tag, "srcset")).toBeTruthy();
expect(html).toContain("<figcaption");
expect(html).toContain("A caption");
});
test("bare local media ref falls back to the media file route", async () => {
const html = await renderImage({
...node,
asset: { _ref: "01LOCALONLY" },
width: 320,
height: 180,
displayWidth: undefined,
displayHeight: undefined,
});
expect(attr(imgTag(html), "src")).toContain("/_emdash/api/media/file/01LOCALONLY");
});
test("unknown dimensions still render without forcing Astro to infer remote size", async () => {
const html = await renderImage({
...node,
width: undefined,
height: undefined,
displayWidth: undefined,
displayHeight: undefined,
});
const tag = imgTag(html);
expect(attr(tag, "src")).toContain("/_emdash/api/media/file/01KTRTJ55S65SADEH9P9TSY89H.png");
expect(tag).not.toContain("srcset=");
expect(tag).not.toContain("data-astro-image");
});
test("LQIP fields survive the image rendering branch", async () => {
const html = await renderImage({
...node,
dominantColor: "rgb(10, 20, 30)",
});
expect(attr(imgTag(html), "style")).toContain("background-color: rgb(10, 20, 30)");
});
test("legacy LQIP metadata is still used", async () => {
const html = await renderImage({
...node,
asset: {
...node.asset,
meta: { dominantColor: "#102030" },
},
});
expect(attr(imgTag(html), "style")).toContain("background-color: #102030");
});
test("LQIP inline background can be disabled for strict CSP", async () => {
const html = await renderImage(
{
...node,
dominantColor: "rgb(10, 20, 30)",
},
locals,
false,
);
expect(attr(imgTag(html), "style")).toBeUndefined();
});
test("external providers keep provider-generated responsive URLs", async () => {
const html = await renderImage({
...node,
asset: {
_ref: "provider-image",
provider: "mock-images",
},
width: 1200,
height: 800,
displayWidth: 600,
displayHeight: undefined,
});
const tag = imgTag(html);
expect(attr(tag, "src")).toBe("https://img.example.com/original?w=600");
expect(attr(tag, "srcset")).toContain("https://img.example.com/render?w=640&h=427 640w");
expect(attr(tag, "srcset")).toContain("https://img.example.com/render?w=1080&h=720 1080w");
expect(attr(tag, "sizes")).toBe("(min-width: 600px) 600px, 100vw");
});
test("missing external provider falls back without crashing", async () => {
const html = await renderImage({
...node,
asset: {
_ref: "missing-provider-image",
provider: "missing-provider",
},
});
expect(attr(imgTag(html), "src")).toContain("/_emdash/api/media/file/missing-provider-image");
});
test("public EmDashImage preserves string URL fallback", async () => {
const html = await renderEmDashImage({
image: "https://cdn.example.com/photo.jpg",
alt: "CDN photo",
width: 640,
height: 360,
});
const tag = imgTag(html);
expect(attr(tag, "src")).toBe("https://cdn.example.com/photo.jpg");
expect(attr(tag, "alt")).toBe("CDN photo");
expect(attr(tag, "width")).toBe("640");
expect(attr(tag, "height")).toBe("360");
expect(attr(tag, "class")).toContain("emdash-image-media");
});
test("public EmDashImage uses Astro Image for same-origin local media", async () => {
const html = await renderEmDashImage({
image: {
id: "01CUSTOM",
src: "/media/custom.jpg",
alt: "Custom local media",
width: 640,
height: 360,
},
});
const tag = imgTag(html);
expect(attr(tag, "src")).toContain("/media/custom.jpg");
expect(attr(tag, "data-astro-image")).toBe("constrained");
expect(attr(tag, "srcset")).toBeTruthy();
expect(attr(tag, "loading")).toBe("lazy");
expect(attr(tag, "decoding")).toBe("async");
});
test("public EmDashImage preserves priority and passthrough attrs", async () => {
const html = await renderEmDashImage({
image: {
id: "01MEDIA",
src: "/_emdash/api/media/file/01MEDIA.jpg",
alt: "Stored media",
width: 800,
height: 600,
},
priority: true,
class: "hero-image",
"data-testid": "hero",
});
const tag = imgTag(html);
expect(attr(tag, "src")).toContain("/_emdash/api/media/file/01MEDIA.jpg");
expect(attr(tag, "loading")).toBe("eager");
expect(attr(tag, "fetchpriority")).toBe("high");
expect(attr(tag, "class")).toBe("hero-image");
expect(attr(tag, "data-testid")).toBe("hero");
});
});