Files
cloudflare__vinext/tests/instrumentation.test.ts
T
Nathan Nguyen d056e50467 fix(config): pass correct Next.js phase to function-form configs + instrumentation test coverage (#273)
* fix(config): pass correct Next.js phase to function-form configs

unwrapConfig() hardcoded "phase-development-server" regardless of
whether vinext was running in dev or build mode. Apps that branch on
the phase argument in next.config.ts received wrong config during
production builds.

Map Vite's env.command to the correct Next.js phase constant:
- "serve" → "phase-development-server"
- "build" → "phase-production-build"

* test: add instrumentation module test coverage

The instrumentation module had zero tests. Add 11 tests covering:

- findInstrumentationFile: file discovery across root and src/,
  priority ordering, null when absent
- runInstrumentation: register() lifecycle, onRequestError handler
  storage, graceful handling of empty modules, error isolation
  when ssrLoadModule fails
- reportRequestError: handler invocation with correct args,
  no-op without handler, error isolation when handler throws

* fix(test): use shared helper for App Router static export test

The static-export test had its own startFixtureServer that loaded the
fixture's vite.config.ts via configFile. This caused RSC plugin timing
issues on Node 25 / macOS — the App Router dev server returned 404 for
all pages. The Pages Router section (same approach) worked because it
doesn't use the RSC plugin.

Switch the App Router section to use the shared startFixtureServerHelper
from helpers.ts, which uses configFile: false with explicit plugin
registration. This matches how app-router.test.ts (193 passing tests)
creates its dev server and avoids the RSC timing issue.

* fix: address PR review feedback

- Import PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants
  from shims/constants.ts instead of hardcoding string literals in
  index.ts and config/next-config.ts
- Add afterEach cleanup for temp directories in instrumentation tests

* refactor: use phase constants in tests, simplify pure function test setup

- Replace hardcoded phase strings in next-config.test.ts with
  PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants
- Remove unnecessary vi.resetModules() from findInstrumentationFile
  tests — it's a pure function with no module-level state to reset

* refactor: use shared startFixtureServer helper in static export tests

Remove the local startFixtureServer function — the shared helper from
tests/helpers.ts does the same thing. The Pages Router fixture's
vite.config.ts is just `vinext()` with no special options, so the
shared helper (which uses configFile: false + vinext plugin) works
identically.
2026-03-06 16:31:16 +00:00

177 lines
5.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { findInstrumentationFile } from "../packages/vinext/src/server/instrumentation.js";
// The runInstrumentation/reportRequestError describe blocks re-import via
// vi.resetModules() to get fresh module-level state (_onRequestError).
// findInstrumentationFile is a pure function — no reset needed.
describe("findInstrumentationFile", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-instr-"));
});
afterEach(() => {
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("returns the path when a file exists at root", () => {
fs.writeFileSync(path.join(tmpDir, "instrumentation.ts"), "");
const result = findInstrumentationFile(tmpDir);
expect(result).toBe(path.join(tmpDir, "instrumentation.ts"));
});
it("prefers root over src/ directory (priority order)", () => {
// Create both root and src/ variants
fs.writeFileSync(path.join(tmpDir, "instrumentation.ts"), "");
fs.mkdirSync(path.join(tmpDir, "src"));
fs.writeFileSync(path.join(tmpDir, "src", "instrumentation.ts"), "");
const result = findInstrumentationFile(tmpDir);
// Root files come first in INSTRUMENTATION_FILES, so root wins
expect(result).toBe(path.join(tmpDir, "instrumentation.ts"));
});
it("falls back to src/ directory", () => {
fs.mkdirSync(path.join(tmpDir, "src"));
fs.writeFileSync(path.join(tmpDir, "src", "instrumentation.ts"), "");
const result = findInstrumentationFile(tmpDir);
expect(result).toBe(path.join(tmpDir, "src", "instrumentation.ts"));
});
it("returns null when no instrumentation file exists", () => {
const result = findInstrumentationFile(tmpDir);
expect(result).toBeNull();
});
});
describe("runInstrumentation", () => {
let runInstrumentation: typeof import("../packages/vinext/src/server/instrumentation.js").runInstrumentation;
let getOnRequestErrorHandler: typeof import("../packages/vinext/src/server/instrumentation.js").getOnRequestErrorHandler;
beforeEach(async () => {
vi.resetModules();
const mod = await import("../packages/vinext/src/server/instrumentation.js");
runInstrumentation = mod.runInstrumentation;
getOnRequestErrorHandler = mod.getOnRequestErrorHandler;
});
it("calls register() when exported", async () => {
const register = vi.fn();
const server = {
ssrLoadModule: vi.fn().mockResolvedValue({ register }),
};
await runInstrumentation(server, "/fake/instrumentation.ts");
expect(register).toHaveBeenCalledOnce();
});
it("stores onRequestError handler for later retrieval", async () => {
const onRequestError = vi.fn();
const server = {
ssrLoadModule: vi.fn().mockResolvedValue({ onRequestError }),
};
await runInstrumentation(server, "/fake/instrumentation.ts");
expect(getOnRequestErrorHandler()).toBe(onRequestError);
});
it("handles modules with no register or onRequestError gracefully", async () => {
const server = {
ssrLoadModule: vi.fn().mockResolvedValue({}),
};
// Should not throw
await runInstrumentation(server, "/fake/instrumentation.ts");
expect(getOnRequestErrorHandler()).toBeNull();
});
it("logs error and continues when ssrLoadModule fails", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const server = {
ssrLoadModule: vi.fn().mockRejectedValue(new Error("Module not found")),
};
// Should not throw
await runInstrumentation(server, "/fake/instrumentation.ts");
expect(consoleSpy).toHaveBeenCalledWith(
"[vinext] Failed to load instrumentation:",
"Module not found",
);
consoleSpy.mockRestore();
});
});
describe("reportRequestError", () => {
let runInstrumentation: typeof import("../packages/vinext/src/server/instrumentation.js").runInstrumentation;
let reportRequestError: typeof import("../packages/vinext/src/server/instrumentation.js").reportRequestError;
const sampleRequest = { path: "/test", method: "GET", headers: {} };
const sampleContext = {
routerKind: "App Router" as const,
routePath: "/test",
routeType: "render" as const,
};
beforeEach(async () => {
vi.resetModules();
const mod = await import("../packages/vinext/src/server/instrumentation.js");
runInstrumentation = mod.runInstrumentation;
reportRequestError = mod.reportRequestError;
});
it("calls the registered handler with correct args", async () => {
const onRequestError = vi.fn();
const server = {
ssrLoadModule: vi.fn().mockResolvedValue({ onRequestError }),
};
await runInstrumentation(server, "/fake/instrumentation.ts");
const error = new Error("boom");
await reportRequestError(error, sampleRequest, sampleContext);
expect(onRequestError).toHaveBeenCalledWith(
error,
sampleRequest,
sampleContext,
);
});
it("no-ops when no handler is registered", async () => {
// No runInstrumentation called, so _onRequestError is null.
// Should not throw.
await reportRequestError(new Error("boom"), sampleRequest, sampleContext);
});
it("catches and logs errors thrown by the handler", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const onRequestError = vi.fn().mockRejectedValue(new Error("handler broke"));
const server = {
ssrLoadModule: vi.fn().mockResolvedValue({ onRequestError }),
};
await runInstrumentation(server, "/fake/instrumentation.ts");
await reportRequestError(new Error("boom"), sampleRequest, sampleContext);
expect(consoleSpy).toHaveBeenCalledWith(
"[vinext] onRequestError handler threw:",
"handler broke",
);
consoleSpy.mockRestore();
});
});