Files
cloudflare__vinext/tests/image-optimization-parity.test.ts
T
James Anderson 4bc2dd3235 fix(image): match App Router deploy parity (#2310)
* fix(image): restore app router deploy parity

* test(image): expect configured optimizer widths

* fix(image): match responsive width selection
2026-06-26 20:36:27 +01:00

179 lines
6.5 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import type { ViteDevServer } from "vite-plus";
import { APP_FIXTURE_DIR, PAGES_FIXTURE_DIR, startFixtureServer } from "./helpers.js";
const PNG_1X1 = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4//8/AwAI/AL+X8n26QAAAABJRU5ErkJggg==",
"base64",
);
async function createImageFixture(router: "app" | "pages"): Promise<string> {
const baseFixtureDir = router === "app" ? APP_FIXTURE_DIR : PAGES_FIXTURE_DIR;
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), `vinext-${router}-image-parity-`));
await fs.cp(baseFixtureDir, rootDir, { recursive: true });
try {
await fs.access(path.join(rootDir, "node_modules"));
} catch {
await fs.symlink(
path.resolve(import.meta.dirname, "../node_modules"),
path.join(rootDir, "node_modules"),
"junction",
);
}
await fs.mkdir(path.join(rootDir, "public"), { recursive: true });
await fs.writeFile(path.join(rootDir, "public", "äöüščří.png"), PNG_1X1);
await fs.writeFile(path.join(rootDir, "public", "hello world.png"), PNG_1X1);
if (router === "app") {
await fs.rm(path.join(rootDir, "app", "alias-test"), { recursive: true, force: true });
await fs.rm(path.join(rootDir, "app", "baseurl-test"), { recursive: true, force: true });
await fs.mkdir(path.join(rootDir, "app", "image-parity"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "app", "image-parity", "page.tsx"),
`import Image from "next/image";
export default function Page() {
return (
<main>
<Image alt="unicode" src="/äöüščří.png" width={64} height={64} />
<Image alt="space" src="/hello world.png" width={64} height={64} />
</main>
);
}
`,
);
} else {
await fs.mkdir(path.join(rootDir, "pages"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "pages", "image-parity.tsx"),
`import Image from "next/image";
export default function Page() {
return (
<main>
<Image alt="unicode" src="/äöüščří.png" width={64} height={64} />
<Image alt="space" src="/hello world.png" width={64} height={64} />
</main>
);
}
`,
);
}
return rootDir;
}
function getImageSrcFromHtml(html: string, alt: string): string {
for (const match of html.matchAll(/<img\b[^>]*>/g)) {
const tag = match[0];
if (!tag.includes(`alt="${alt}"`)) continue;
const srcMatch = tag.match(/\ssrc="([^"]+)"/);
if (srcMatch) return srcMatch[1].replaceAll("&amp;", "&");
}
throw new Error(`Could not find <img> tag for alt="${alt}"`);
}
async function fetchHtmlWithRetry(baseUrl: string, pagePath: string): Promise<string> {
let lastStatus = 0;
let lastBody = "";
for (let attempt = 0; attempt < 10; attempt++) {
const res = await fetch(`${baseUrl}${pagePath}`);
const body = await res.text();
if (res.status === 200) return body;
lastStatus = res.status;
lastBody = body;
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(
`Expected ${pagePath} to return 200, got ${lastStatus}: ${lastBody.slice(0, 500)}`,
);
}
function runLocalImageUrlParitySuite(router: "app" | "pages"): void {
describe(`${router === "app" ? "App" : "Pages"} Router next/image local URL parity`, () => {
let server: ViteDevServer;
let baseUrl: string;
let fixtureDir: string;
beforeAll(async () => {
fixtureDir = await createImageFixture(router);
({ server, baseUrl } = await startFixtureServer(fixtureDir, { appRouter: router === "app" }));
}, 30000);
afterAll(async () => {
await server?.close();
await fs.rm(fixtureDir, { recursive: true, force: true });
});
// Ported from Next.js: test/integration/next-image-new/unicode/test/index.test.ts
// https://github.com/vercel/next.js/blob/canary/test/integration/next-image-new/unicode/test/index.test.ts
it("serves internal unicode image URLs through the optimizer route", async () => {
const pagePath = "/image-parity";
const html = await fetchHtmlWithRetry(baseUrl, pagePath);
const src = getImageSrcFromHtml(html, "unicode");
const imageUrl = new URL(src, baseUrl);
expect(imageUrl.pathname).toBe("/_next/image");
expect(imageUrl.searchParams.get("url")).toBe("/äöüščří.png");
expect(imageUrl.searchParams.get("w")).toBe("128");
expect(imageUrl.searchParams.get("q")).toBe("75");
const res = await fetch(imageUrl);
expect(res.status).toBe(200);
});
// Ported from Next.js: test/integration/next-image-new/unicode/test/index.test.ts
// https://github.com/vercel/next.js/blob/canary/test/integration/next-image-new/unicode/test/index.test.ts
it("serves internal image URLs with spaces through the optimizer route", async () => {
const pagePath = "/image-parity";
const html = await fetchHtmlWithRetry(baseUrl, pagePath);
const src = getImageSrcFromHtml(html, "space");
const imageUrl = new URL(src, baseUrl);
expect(imageUrl.pathname).toBe("/_next/image");
expect(imageUrl.searchParams.get("url")).toBe("/hello world.png");
expect(imageUrl.searchParams.get("w")).toBe("128");
expect(imageUrl.searchParams.get("q")).toBe("75");
const res = await fetch(imageUrl);
expect(res.status).toBe(200);
});
// Both /_next/image and /_vinext/image are accepted so apps wired to
// either prefix get images served through the same optimizer pipeline.
it("routes /_vinext/image requests through the optimizer", async () => {
const vinextUrl = new URL("/_vinext/image", baseUrl);
vinextUrl.searchParams.set("url", "/hello world.png");
vinextUrl.searchParams.set("w", "64");
vinextUrl.searchParams.set("q", "75");
const res = await fetch(vinextUrl);
expect(res.status).toBe(200);
});
});
}
describe("image deployment query parity", () => {
it("accepts Next.js deployment IDs without including them in the source path", async () => {
const { parseImageParams } =
await import("../packages/vinext/src/server/image-optimization.js");
const requestUrl = new URL(
"http://vinext.test/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.hash.png&w=828&q=85&dpl=deploy-1",
);
expect(parseImageParams(requestUrl)).toEqual({
imageUrl: "/_next/static/media/test.hash.png",
width: 828,
quality: 85,
});
});
});
runLocalImageUrlParitySuite("app");
runLocalImageUrlParitySuite("pages");