Files
cloudflare__vinext/tests/font-google-build.test.ts
T
James Anderson d03c5aa045 test(msw): introduce MSW infrastructure and migrate font-google-build (#1444)
* test(msw): introduce MSW infrastructure and migrate font-google-build

Replaces ad-hoc `globalThis.fetch` hijacking in tests with Mock Service
Worker. MSW intercepts both `globalThis.fetch` and `node:http`/`https`,
so it covers fetches issued by in-process Vite servers and fixture pages
without each test having to manage its own stub.

The setup file enables `onUnhandledRequest: 'error'` so any future test
that issues an unmocked external request fails loudly instead of
silently hitting the network. Loopback hostnames (localhost, 127.0.0.1,
::1) are bypassed because the integration project deliberately hits
in-process test servers via fetch.

What this PR contains:
- `tests/_msw/{handlers,server,setup}.ts` — shared infra
- `setupFiles` wired into both vitest projects (unit + integration)
- `msw` + `@mswjs/interceptors` added to the workspace catalog
- `msw` postinstall skipped via `allowBuilds` (it only copies a browser
  service worker; we exclusively use `msw/node`)
- `tests/font-google-build.test.ts` migrated as the proof of pattern

Temporary handlers for `httpbin.org/uuid` and
`example.com/not-cacheable` keep the existing fixture pages
(`revalidate-tag-test`, `dynamic-error-fetch`) working under the new
guard. These are placeholders flagged with `TODO(msw-migration)` — a
follow-up will replace those live URLs with test-local endpoints.

This is PR 1 of 3:
- PR 2 will migrate `tests/fetch-cache.test.ts`
- PR 3 will migrate `tests/font-google.test.ts` and the font block in
  `tests/app-router.test.ts`

* test(msw): restrict MSW to the unit project + use passthrough for loopback

Two fixes to the initial MSW wiring that surfaced when CI ran the full
suite:

1. The integration project is now excluded from MSW. Integration tests
   spin up in-process HTTP servers and fixture dev servers and exercise
   them via `fetch("http://127.0.0.1:<port>/...")`. Even with a loopback
   `passthrough()` handler installed, the @mswjs/interceptors layer
   interferes with that traffic in subtle ways:

   - `tests/prerender.test.ts > errors without writing .rsc when the
     middleware short-circuit fallback RSC request fails` timed out at
     30s — the 5xx response body never reached the caller.
   - `tests/ecosystem.test.ts > nuqs` hung in `beforeAll` waiting for
     its fixture process to come up, because the readiness fetch
     stalled inside the interceptor.

   Integration tests already talk to real local servers, not the
   internet, so the unhandled-request guard buys little there. The
   value of MSW for this repo is mocking external HTTP for unit tests
   of fetch wrappers (font-google, font-google-build, fetch-cache),
   which is preserved.

2. The `onUnhandledRequest` callback in `setup.ts` that tried to
   silently let loopback through wasn't sufficient — MSW's interceptor
   still ran for those requests and could stall them. Replaced with a
   default `http.all(LOOPBACK_URL_PATTERN, () => passthrough())`
   handler in `handlers.ts` and a plain `onUnhandledRequest: "error"`
   in `setup.ts`. Unit tests like the middleware-rewrite proxy in
   `tests/shims.test.ts` and the JSX-in-JS suite need this to keep
   their in-process HTTP servers reachable.

* test(msw): address BigBonk review notes

- Update PR description in the commit body to drop the stale mention of
  TODO(msw-migration) handlers — they were removed when the integration
  project was excluded from MSW; the fixture pages run there.
- Tighten the loopback IPv4 octet regex from `\d+` to `\d{1,3}` so it
  doesn't match absurdly long octet strings. `URL.hostname` normalises
  real addresses, so this is purely a readability fix.

* test(msw): address remaining BigBonk notes from the initial review

- Add `0.0.0.0` to the loopback passthrough pattern so test servers that
  bind to the unspecified address keep working without per-test mocks.
- Add a `TODO(msw-3)` comment in `server.ts` flagging the
  `SetupServerApi` deprecation in MSW 2.14.6 — the `FetchInterceptor`-only
  construction will need to migrate to `defineNetwork`'s interceptor
  config when we bump to MSW 3.

Both raised by BigBonk on the first review of this PR (the second review
focused on the stale description and the loose `\d+` octet bound, which
were addressed in the previous commit).

* test(msw): scope server.use() inside the try block in font-google-build

Minor structural fix from BigBonk's review: moves the `server.use()`
handler registration inside the `try` block so all setup and the
matching `fs.unlink` cleanup share the same try/finally scope.

No behaviour change — `server.use()` is synchronous and infallible, so
this is purely a structural tidy-up.

* test(msw): correct comment in font-google-build to match interceptor config

BigBonk caught that the comment described the default `setupServer()`
behaviour (which uses both `FetchInterceptor` and `ClientRequestInterceptor`),
but this project's `server.ts` deliberately constructs `SetupServerApi`
with only `FetchInterceptor`. Update the comment to reflect that — it
intercepts `globalThis.fetch`, not `node:http`/`node:https`.

No code change, comment only.
2026-05-22 13:03:08 +00:00

88 lines
3.0 KiB
TypeScript

import { describe, it, expect, afterAll } from "vite-plus/test";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import { createBuilder } from "vite-plus";
import { http, HttpResponse } from "msw";
import { server } from "./_msw/server.js";
import vinext from "../packages/vinext/src/index.js";
const APP_FIXTURE_DIR = path.resolve(import.meta.dirname, "./fixtures/font-google-multiple");
async function buildFontGoogleMultipleFixture(): Promise<string> {
const outDir = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-font-google-multiple-"));
const rscOutDir = path.join(outDir, "server");
const ssrOutDir = path.join(outDir, "server", "ssr");
const clientOutDir = path.join(outDir, "client");
const nodeModulesLink = path.join(APP_FIXTURE_DIR, "node_modules");
try {
// Intercept the Google Fonts CSS fetch issued by the in-process Vite build.
// The server is configured with `FetchInterceptor` only (see `server.ts`),
// so MSW intercepts `globalThis.fetch` — which is what Vite/vinext uses to
// pull font CSS. Handlers are reset by the global `afterEach` in
// `tests/_msw/setup.ts`.
server.use(
http.get("https://fonts.googleapis.com/*", ({ request }) => {
const url = request.url;
if (url.includes("Geist") && !url.includes("Mono")) {
return HttpResponse.text("@font-face { font-family: 'Geist'; src: url(/geist.woff2); }", {
headers: { "content-type": "text/css" },
});
}
return HttpResponse.text(
"@font-face { font-family: 'Geist Mono'; src: url(/geist-mono.woff2); }",
{ headers: { "content-type": "text/css" } },
);
}),
);
const projectNodeModules = path.resolve(import.meta.dirname, "../node_modules");
await fs.rm(nodeModulesLink, { recursive: true, force: true });
await fs.symlink(projectNodeModules, nodeModulesLink);
const builder = await createBuilder({
root: APP_FIXTURE_DIR,
configFile: false,
plugins: [
vinext({
appDir: APP_FIXTURE_DIR,
rscOutDir,
ssrOutDir,
clientOutDir,
}),
],
logLevel: "silent",
});
await builder.buildApp();
return path.join(outDir, "server", "index.js");
} finally {
await fs.unlink(nodeModulesLink).catch(() => {});
}
}
describe("font-google build integration", () => {
let buildOutputPath: string;
let outDir: string;
afterAll(async () => {
if (outDir) {
await fs.rm(outDir, { recursive: true, force: true });
}
});
it("should build and transform multiple Google fonts (Geist + Geist_Mono)", async () => {
buildOutputPath = await buildFontGoogleMultipleFixture();
outDir = path.dirname(path.dirname(buildOutputPath));
const content = await fs.readFile(buildOutputPath, "utf-8");
expect(content).toContain("Geist");
expect(content).toContain("_vinext");
expect(content).toContain("selfHostedCSS");
}, 120000);
});