Aller au contenu

Next.js

Les frameworks SSR comme Next.js font des appels fetch côté serveur qui passent par le proxy sans contexte de navigateur. Le proxy identifie à quelle session appartiennent ces requêtes via l’en-tête x-test-rcrd-id. Le playwrightProxy.before() de Playwright le définit déjà sur la navigation navigateur qui déclenche le SSR, donc l’id est disponible dans next/headers — le travail consiste à l’attacher aux requêtes sortantes côté serveur. (Les tests navigateur uniquement n’ont besoin de rien de tout ça ; le proxy revient à la session définie globalement.)

Une ligne dans votre root layout tagge chaque fetch côté serveur — Server Components, Route Handlers, sur les runtimes Node et Edge :

app/layout.tsx
import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
registerProxyFetch(); // no-op en production sauf si TEST_PROXY_RECORDER_ENABLED=true

Il patche le fetch global pour copier le x-test-rcrd-id de la requête courante sur les requêtes sortantes, afin que le proxy puisse distinguer les sessions de replay concurrentes. Appelez-le depuis le root layout — pas instrumentation.ts, dont le contexte diffère de celui qui rend vos routes sur le runtime Edge, donc un patch là-bas ne se déclenche jamais silencieusement.

Si vos requêtes côté serveur passent par axios, enregistrez une fois chaque instance côté serveur :

import { registerProxyAxios } from 'test-proxy-recorder/nextjs';
registerProxyAxios(axiosForServer);

Il ajoute un intercepteur de requête qui pose l’id (sans jamais toucher au fetch global), il est donc immunisé contre la réserve du serveur de dev ci-dessus. No-op en production / dans le navigateur ; idempotent par instance ; n’écrase jamais un id défini par l’appelant.

Sans patch, et fonctionne aussi sous next dev. À utiliser pour un fetch unique, ou quand vous préférez ne pas patcher le fetch global :

import { headers } from 'next/headers';
import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
const res = await fetch('http://localhost:8100/api/data', {
headers: createHeadersWithRecordingId(await headers(), {
'Content-Type': 'application/json',
}),
});

Un proxy.ts (Next.js 16+, qui exporte proxy) ou middleware.ts (15 et antérieur, qui exporte middleware) appelant setNextProxyHeaders rend l’id disponible via next/headers, mais ne tagge pas les fetches sortants — il n’est donc pas requis quand vous utilisez l’un des helpers ci-dessus. Y recourir seulement si vous possédez déjà un middleware (auth, etc.), et le coupler tout de même avec un helper pour faire le tagging :

// proxy.ts (Next.js 16+)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
export function proxy(request: NextRequest) {
const response = NextResponse.next();
setNextProxyHeaders(request, response); // expose l'id ; à coupler avec un helper ci-dessus
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Voir la référence de l’API pour les signatures complètes des helpers test-proxy-recorder/nextjs. Un projet Edge complet et exécutable se trouve dans l’exemple Edge runtime.

Ne désactivez pas la mise en cache pour les tests — le recorder fonctionne avec une route mise en cache/ISR. Mais une règle décide de toute la conception : pour rejouer un fetch SSR, la page doit exécuter ce fetch au moment de la requête. Une route qui sert du HTML prérendu ou un rendu mis en cache obsolète ne fait jamais le fetch, donc le proxy n’a rien à servir et l’assertion voit du contenu obsolète.

La façon qui reste déterministe est de mettre en cache le fetch SSR avec next.revalidate + next.tags au niveau du fetch, puis d’invalider à la demande avant l’assertion :

// app/isr/page.tsx — pas de `export const dynamic`, pas de `export const revalidate`
const res = await fetch(`${BACKEND_URL}/todos`, {
next: { revalidate: 30, tags: ['isr-todos'] },
});
app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
revalidateTag('isr-todos', 'max'); // Next.js 16 exige le 2e argument de profil
e2e/isr.spec.ts
await page.request.post('/api/revalidate'); // purge dure
await page.goto('/isr'); // une seule navigation — déterministe
await expect(page.getByTestId('todo-text')).toHaveCount(1);

revalidateTag sur une entrée de cache de fetch est une purge dure : la lecture suivante est un cache miss qui bloque et refait le fetch à travers le proxy. Vous devez purger avant la navigation de replay car le cache de données survit entre les phases enregistrement → replay d’un même processus next start — sinon le replay sert le cache de la phase d’enregistrement et n’atteint jamais le proxy (un faux positif).

Pendant les tests, le fetch patché lit headers(), donc la page est rendue dynamiquement et exécute réellement le fetch. En production (recorder désactivé), rien ne lit headers() et la page est en ISR statique comme d’habitude — le rendu dynamique est limité aux tests, et il est intrinsèque à l’enregistrement d’un fetch SSR.

La revalidation à la demande est privilégiée (elle purge le cache et force une régénération), donc protégez la route avec un secret partagé — échouez en mode fermé s’il n’est pas défini, comparez en temps constant, et attachez le token depuis le test via use.extraHTTPHeaders de Playwright pour que le spec ne le manipule jamais.

Voir l’exemple complet et exécutable (partie de l’exemple Next.js 16) :

  • app/isr/page.tsx — la page mise en cache (next.tags au niveau du fetch)
  • app/api/revalidate/route.ts — comment protéger revalidateTag : échec en mode fermé + comparaison du secret en temps constant
  • e2e/isr.spec.ts — invalide, puis une seule navigation ; vérifie que l’appel de revalidation a réussi
  • playwright.config.ts — charge .env et attache le secret via extraHTTPHeaders

Démarrez les services depuis des scripts, pas depuis playwright.config.ts :

{
"scripts": {
"mock": "node mock-backend/server.mjs",
"proxy": "test-proxy-recorder http://localhost:3002 -p 8100 -d ./e2e/recordings",
"start:all": "concurrently \"pnpm mock\" \"pnpm proxy\" \"pnpm build && next start --port 3000\""
}
}

Un projet complet et exécutable se trouve dans l’exemple Next.js 16.