Pular para o conteúdo

Next.js

Frameworks SSR como o Next.js fazem chamadas fetch do lado do servidor que passam pelo proxy sem um contexto de navegador. O proxy identifica a qual sessão essas requisições pertencem por meio do header x-test-rcrd-id. O playwrightProxy.before() do Playwright já o define na navegação do navegador que dispara o SSR, então o id fica disponível em next/headers — o trabalho é anexá-lo às requisições do lado do servidor de saída. (Testes somente de navegador não precisam de nada disso; o proxy recai na sessão definida globalmente.)

Uma linha no seu root layout marca cada fetch do lado do servidor — Server Components, Route Handlers, nos runtimes Node e Edge:

app/layout.tsx
import { registerProxyFetch } from 'test-proxy-recorder/nextjs';
registerProxyFetch(); // no-op em produção a menos que TEST_PROXY_RECORDER_ENABLED=true

Ele faz patch do fetch global para copiar o x-test-rcrd-id da requisição atual para as requisições de saída, para que o proxy consiga distinguir sessões de reprodução concorrentes. Chame-o a partir do root layout — não do instrumentation.ts, cujo contexto difere daquele que renderiza suas rotas no runtime Edge, então um patch ali silenciosamente nunca dispara.

Se suas requisições do lado do servidor passam pelo axios, registre cada instância do lado do servidor uma vez:

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

Ele adiciona um interceptor de requisição que carimba o id (nunca tocando no fetch global), então fica imune à ressalva do servidor de desenvolvimento acima. No-op em produção / no navegador; idempotente por instância; nunca sobrescreve um id definido pelo chamador.

Sem patch, e funciona também sob next dev. Use-o para um único fetch, ou quando você preferir não fazer patch do 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',
}),
});

Um proxy.ts (Next.js 16+, exportado como proxy) ou middleware.ts (15 e anteriores, exportado como middleware) chamando setNextProxyHeaders torna o id disponível via next/headers, mas não marca as buscas de saída — então não é obrigatório quando você usa um dos helpers acima. Use-o apenas se você já tem um middleware (auth, etc.) e ainda assim combine-o com um helper para fazer a marcação:

// 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); // expõe o id; combine com um helper acima
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Veja a referência da API para as assinaturas completas dos helpers de test-proxy-recorder/nextjs. Um projeto Edge completo e executável fica no exemplo de Edge runtime.

Não desative o cache para os testes — o recorder funciona com uma rota cacheada/ISR. Mas há uma regra que decide todo o design: para reproduzir um fetch SSR, a página precisa executar esse fetch no momento da requisição. Uma rota que serve HTML pré-renderizado ou um render cacheado obsoleto nunca faz o fetch, então o proxy não tem nada para servir e a asserção vê conteúdo obsoleto.

A forma de manter isso determinístico é cachear o fetch SSR com next.revalidate + next.tags no nível do fetch e depois invalidar sob demanda antes da asserção:

// app/isr/page.tsx — sem `export const dynamic`, sem `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'); // o Next.js 16 exige o 2º argumento de perfil
e2e/isr.spec.ts
await page.request.post('/api/revalidate'); // purge forçado
await page.goto('/isr'); // uma navegação — determinístico
await expect(page.getByTestId('todo-text')).toHaveCount(1);

O revalidateTag em uma entrada de cache de fetch é um purge forçado: a próxima leitura é um cache miss que bloqueia e refaz o fetch pelo proxy. Você precisa fazer o purge antes da navegação de reprodução porque o cache de dados sobrevive entre as fases de gravar → reproduzir de um único processo next start — caso contrário, a reprodução serve o cache da fase de gravação e nunca acessa o proxy (um falso positivo).

Durante os testes, o fetch com patch lê headers(), então a página renderiza dinamicamente e de fato executa o fetch. Em produção (recorder desativado), nada lê headers() e a página é ISR estática como de costume — o render dinâmico fica restrito aos testes e é intrínseco à gravação de um fetch SSR.

A revalidação sob demanda é privilegiada (ela purga o cache e força a regeneração), então proteja a rota com um segredo compartilhado — falhe de forma fechada se não estiver definido, compare em tempo constante e anexe o token a partir do teste via use.extraHTTPHeaders do Playwright, para que a spec nunca o manipule.

Veja o exemplo completo e executável (parte do exemplo de Next.js 16):

Inicie os serviços a partir de scripts, não do 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\""
}
}

Um projeto completo e executável fica no exemplo de Next.js 16.