mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
2354ca6e60
* test(trailing-slash): pin core enforcement contract for App Router (#1332) Adds focused integration tests against a dev-server fixture built from app-basic with a minimal `next.config.ts` overriding `trailingSlash`. The core enforcement contract documented in Next.js test/e2e/trailing-slashes and test/e2e/app-dir/trailingslash was already implemented across the dev server, prod-server, app-rsc-handler, and the deploy worker entry by #1395 and #1413, but no end-to-end test asserted the full contract end-to-end through the dev server. These regression tests close that gap so future refactors of the trailing-slash redirect or `<Link>` normalisation paths fail loudly. Coverage: - trailingSlash: true - GET /about → 308 Location: /about/ - GET /about/ → 200 - <Link href="/about"> renders href="/about/" - trailingSlash: false (default) - GET /about/ → 308 Location: /about - GET /about → 200 - <Link href="/about"> renders href="/about" (no trailing slash) Refs #1332. * test(trailing-slash): address bonk review nits - Rename tests/trailing-slash-1332.test.ts → tests/trailing-slash.test.ts to match repo naming convention (the issue ref lives in the header comment and describe titles). - Switch to the shared `startFixtureServer` helper from tests/helpers.ts; drop the local `startServer` and the now-unused `createServer` / `Server` imports. - Tighten the second describe title to "trailingSlash: false" — the fixture explicitly sets `false`, not omits the field. The default-vs- explicit behaviour is identical today, so the merged-name was misleading. - Document why we keep `fs.cpSync` rather than `createIsolatedFixture`: app-basic's node_modules carries fixture-local symlinks (fake-context-lib, fake-css-lib, …) that `createIsolatedFixture` would break by replacing node_modules with a workspace symlink, and the isolated copy also gets its own `.vite` optimizer cache (preventing races against the workspace cache used by other tests). Verified empirically: the bot-suggested `createIsolatedFixture` swap reproduces 500s on every request, mirroring the rationale already captured in tests/favicon-short-circuit.test.ts. Refs #1332. * test(trailing-slash): bump 200-serve timeouts to 30s for dev-mode compile CI showed both 200-serve cases hitting the 5s default while the dev server was compiling the /about route on first hit. Match the headroom we already use elsewhere (e.g. tests/app-router.test.ts). * style: apply biome formatter to trailing-slash.test.ts
126 lines
4.9 KiB
TypeScript
126 lines
4.9 KiB
TypeScript
/**
|
|
* Regression coverage for issue #1332 — trailing slash configuration not
|
|
* enforced. These tests cover the core enforcement contract documented in
|
|
* Next.js test/e2e/trailing-slashes/* and test/e2e/app-dir/trailingslash/*:
|
|
*
|
|
* - With `trailingSlash: true`, a request to `/foo` returns 308 → `/foo/`
|
|
* - With `trailingSlash: false`, a request to `/foo/` returns 308 → `/foo`
|
|
* - App Router pages obey the redirect
|
|
* - <Link href="/foo"> renders as href="/foo/" when trailingSlash is true
|
|
*
|
|
* Refs cloudflare/vinext#1332
|
|
*/
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
|
|
import type { ViteDevServer } from "vite-plus";
|
|
import { APP_FIXTURE_DIR, startFixtureServer } from "./helpers.js";
|
|
|
|
/**
|
|
* Copy the app-basic fixture and overwrite next.config.ts to set only
|
|
* `trailingSlash`. We use `fs.cpSync({ recursive: true })` (not
|
|
* `createIsolatedFixture`) for two reasons:
|
|
* 1. app-basic's node_modules contains symlinks to fixture-local packages
|
|
* (`fake-context-lib`, etc.). `createIsolatedFixture` replaces
|
|
* node_modules with a workspace symlink and would break those.
|
|
* 2. Each isolated copy gets its own `.vite` dep-optimizer cache, so
|
|
* concurrent test runs cannot race on a shared optimization output.
|
|
* See `tests/favicon-short-circuit.test.ts` for the same rationale.
|
|
*/
|
|
function copyAppFixtureWithTrailingSlash(prefix: string, trailingSlash: boolean): string {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
fs.cpSync(APP_FIXTURE_DIR, tmpDir, { recursive: true });
|
|
fs.rmSync(path.join(tmpDir, "node_modules", ".vite"), { recursive: true, force: true });
|
|
// Replace the default app-basic next.config.ts. It carries a long list of
|
|
// redirects/rewrites/headers used by other suites; we want a clean slate so
|
|
// behaviour under test is exactly the trailingSlash policy.
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, "next.config.ts"),
|
|
`import type { NextConfig } from "vinext";
|
|
const nextConfig: NextConfig = { trailingSlash: ${trailingSlash} };
|
|
export default nextConfig;
|
|
`,
|
|
);
|
|
return tmpDir;
|
|
}
|
|
|
|
describe("App Router trailingSlash: true (#1332)", () => {
|
|
let tmpDir: string;
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = copyAppFixtureWithTrailingSlash("vinext-ts-true-", true);
|
|
({ server, baseUrl } = await startFixtureServer(tmpDir));
|
|
}, 60000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("redirects /about → /about/ with 308", async () => {
|
|
const res = await fetch(`${baseUrl}/about`, { redirect: "manual" });
|
|
expect(res.status).toBe(308);
|
|
const location = res.headers.get("location");
|
|
expect(location).not.toBeNull();
|
|
expect(new URL(location!, baseUrl).pathname).toBe("/about/");
|
|
});
|
|
|
|
// First hit triggers a dev-mode compile of the /about route; on CI this
|
|
// routinely exceeds the 5s default. Mirrors the headroom used elsewhere
|
|
// (see tests/app-router.test.ts).
|
|
it("serves /about/ with 200 (no redirect)", async () => {
|
|
const res = await fetch(`${baseUrl}/about/`, { redirect: "manual" });
|
|
expect(res.status).toBe(200);
|
|
const html = await res.text();
|
|
expect(html).toContain("About");
|
|
}, 30000);
|
|
|
|
it("home page <Link> renders href with a trailing slash", async () => {
|
|
// app-basic's homepage has <Link href="/about">. Under trailingSlash: true
|
|
// the rendered href should be normalised to "/about/".
|
|
const res = await fetch(`${baseUrl}/`);
|
|
expect(res.status).toBe(200);
|
|
const html = await res.text();
|
|
expect(html).toMatch(/href="\/about\/"/);
|
|
});
|
|
});
|
|
|
|
describe("App Router trailingSlash: false (#1332)", () => {
|
|
let tmpDir: string;
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = copyAppFixtureWithTrailingSlash("vinext-ts-false-", false);
|
|
({ server, baseUrl } = await startFixtureServer(tmpDir));
|
|
}, 60000);
|
|
|
|
afterAll(async () => {
|
|
await server?.close();
|
|
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("redirects /about/ → /about with 308", async () => {
|
|
const res = await fetch(`${baseUrl}/about/`, { redirect: "manual" });
|
|
expect(res.status).toBe(308);
|
|
expect(new URL(res.headers.get("location")!, baseUrl).pathname).toBe("/about");
|
|
});
|
|
|
|
it("serves /about with 200", async () => {
|
|
const res = await fetch(`${baseUrl}/about`, { redirect: "manual" });
|
|
expect(res.status).toBe(200);
|
|
const html = await res.text();
|
|
expect(html).toContain("About");
|
|
}, 30000);
|
|
|
|
it("home page <Link> renders href without a trailing slash", async () => {
|
|
const res = await fetch(`${baseUrl}/`);
|
|
expect(res.status).toBe(200);
|
|
const html = await res.text();
|
|
expect(html).toMatch(/href="\/about(?!\/)"/);
|
|
});
|
|
});
|