-
Notifications
You must be signed in to change notification settings - Fork 587
Expand file tree
/
Copy pathplaywright.config.ts
More file actions
172 lines (167 loc) · 8.11 KB
/
Copy pathplaywright.config.ts
File metadata and controls
172 lines (167 loc) · 8.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import { readFileSync } from "node:fs";
import { defineConfig } from "@playwright/test";
/**
* Lê o `.env.e2e` — o ambiente LOCAL da suíte.
*
* Falha ALTO se o arquivo não existir, em vez de deixar o app cair no
* `.env.local`: o modo de falha silencioso aqui é a suíte rodar contra o banco
* de PRODUÇÃO, que foi exatamente o que acontecia antes deste arquivo existir
* (medido em 2026-08-06).
*/
function envDoE2E(): Record<string, string> {
let bruto: string;
try {
bruto = readFileSync(".env.e2e", "utf8");
} catch {
throw new Error(
"Falta o .env.e2e — rode `pnpm e2e:env` (precisa do Supabase local de pé).\n" +
"Sem ele o app sob teste carregaria o .env.local, que aponta para PRODUÇÃO.",
);
}
const env: Record<string, string> = {};
for (const linha of bruto.split("\n")) {
const limpa = linha.trim();
if (limpa === "" || limpa.startsWith("#")) continue;
const i = limpa.indexOf("=");
if (i <= 0) continue;
env[limpa.slice(0, i)] = limpa.slice(i + 1);
}
const url = env.NEXT_PUBLIC_SUPABASE_URL ?? "";
// Um `.env.e2e` apontando para fora do localhost é pior que nenhum, porque
// parece seguro.
if (!url.startsWith("http://127.0.0.1") && !url.startsWith("http://localhost")) {
throw new Error(`.env.e2e aponta para um Supabase que não é local (${url}) — recusado.`);
}
return env;
}
/**
* Publica o `.env.e2e` no ambiente do PROCESSO DE TESTE, não só do servidor.
*
* ## O defeito, medido em 2026-08-08 num worktree limpo
*
* `pnpm e2e:build && pnpm test:e2e` — o caminho documentado — morria antes do
* primeiro teste:
*
* Error: Sem credenciais do Supabase: defina NEXT_PUBLIC_SUPABASE_URL e
* SUPABASE_SERVICE_ROLE_KEY no ambiente (…) ou no .env.local
* at credenciaisSupabaseDeTeste (scripts/lib/env-de-teste.ts)
* at scripts/seed-e2e-credentials.ts
*
* A cadeia: as specs semeiam a própria precondição com `execFileSync`, o filho
* herda o ambiente do RUNNER, e o runner não recebia nada — `envDoE2E()`
* alimentava apenas `webServer.env`. `env-de-teste.ts` cai no `.env.local` como
* plano B, e num worktree limpo esse arquivo **não existe de propósito**: a
* ausência dele é o que impede a suíte de escrever em produção.
*
* Ou seja as duas proteções se anulavam: a que tira o `.env.local` do disco e a
* que injeta o ambiente só no servidor deixavam o seed sem nenhuma das duas
* fontes. O CI não notava porque contorna por dois caminhos — publica o arquivo
* no `$GITHUB_ENV` e ainda faz `cp .env.e2e .env.local`, recriando justamente o
* arquivo cuja ausência é a proteção.
*
* ## Por que aqui
*
* Este é o único ponto que já lê e valida o arquivo (inclusive recusando um
* `.env.e2e` que aponte para fora do localhost). Resolver no `package.json` com
* `set -a; . ./.env.e2e` funcionaria para quem usa o script e não para quem
* chama `playwright test` direto — e é o caminho que a mensagem de erro sugere,
* o que já prova que alguém teve de descobrir isto na mão.
*
* `process.env` VENCE quando a chave já existe: é a mesma precedência de
* `scripts/lib/env-de-teste.ts`, e é o que mantém de pé o
* `AUTH_RATE_LIMIT_LOGIN_IP` que o workflow define por fora. Sobrescrever aqui
* criaria a colisão descrita no comentário do `webServer` abaixo — servidor e
* processo de teste resolvendo a mesma chave para valores diferentes, que foi
* como `INTERNAL_SECRET` derrubou 8 specs com 401.
*/
function publicarNoProcesso(env: Record<string, string>): Record<string, string> {
for (const [chave, valor] of Object.entries(env)) {
if (process.env[chave] === undefined) process.env[chave] = valor;
}
return env;
}
// Porta do dev server sob teste. Default 3001; sobrescreva com E2E_PORT quando
// a 3001 já estiver ocupada por outro checkout/worktree.
const PORT = process.env.E2E_PORT ?? "3001";
const BASE_URL = `http://localhost:${PORT}`;
export default defineConfig({
testDir: "./tests/e2e",
timeout: 30_000,
fullyParallel: false,
/**
* UM worker. `fullyParallel: false` serializa apenas DENTRO de cada arquivo —
* entre arquivos o Playwright continua abrindo vários workers, e estes specs
* compartilham a MESMA organização, os MESMOS usuários e o MESMO banco: um
* spec revoga acesso enquanto outro checa escopo, um cria convite enquanto
* outro conta membros.
*
* Medido em 2026-07-31: rodando a suíte inteira, 10 a 15 specs falhavam com
* `waitForURL` estourando depois do login e elementos "not found"; os MESMOS
* specs, rodados isolados, passavam em 18s. Não era lógica nem lentidão: era
* interferência.
*
* O custo é wall-clock no CI. O benefício é que um vermelho volta a significar
* "quebrou" em vez de "deu azar na ordem" — e suíte que falha por azar ninguém
* lê, o que na prática desliga o gate inteiro.
*/
workers: 1,
retries: 0,
use: {
baseURL: BASE_URL,
// ⚠️ Era `on-first-retry`, e com `retries: 0` logo acima isso significa
// **trace nunca gravado**. As duas linhas estão certas isoladamente e
// erradas juntas: uma diz "só no retry", a outra diz "não há retry".
//
// O preço apareceu inteiro numa investigação real: um vermelho em
// `marca-logo.spec.ts` afirmava "a recusa apagou o logo", e responder o que
// de fato aconteceu com o DOM custou quatro agentes e uma cadeia de
// eliminação — porque o único artefato do run era um `error-context.md`
// que fotografou a página do `afterAll`, não a que falhou. Um trace teria
// dado URL, DOM e rede daquele instante, sem hipótese nenhuma.
//
// `retain-on-failure` grava sempre e descarta no verde: custa disco só
// quando já se está pagando o custo maior, que é ter um vermelho.
trace: "retain-on-failure",
screenshot: "only-on-failure",
},
webServer: {
// Produção (`next build` antes!): dev-server compila por rota (40-80s) e
// Turbopack dev quebra cookies() fora do request scope — inviável p/ e2e.
command: `pnpm exec next start --port ${PORT}`,
// O ambiente do servidor sob teste vem do `.env.e2e`, INJETADO aqui — e não
// do `.env.local`, que num checkout de trabalho aponta para PRODUÇÃO.
// Variável de ambiente real tem precedência sobre os arquivos `.env*` que o
// Next carrega sozinho, e é isto que impede a suíte de escrever no banco
// real (medido em 2026-08-06: sem esta injeção, ela escrevia).
//
// ⚠️ Isto cobre o SERVIDOR. Os scripts de seed que as specs chamam sozinhas
// liam `.env.local` direto do disco e escapavam daqui — o conserto do outro
// lado é `scripts/lib/env-de-teste.ts`, que faz `process.env` vencer. E é o
// `publicarNoProcesso` acima que garante que o `process.env` do runner tenha
// o que aquele conserto precisa: sem ele, num worktree sem `.env.local`, o
// seed não tinha NENHUMA das duas fontes.
env: publicarNoProcesso(envDoE2E()),
url: BASE_URL,
// false: reusar um server que já ocupa a porta pode ser OUTRO processo
// (ex.: bundle do Remotion na 3000) — o teste precisa do NOSSO next start.
reuseExistingServer: false,
// Sobre a precedência de `env`, MEDIDO (Playwright 1.5x, 2026-08-07) com um
// webServer que imprime o que recebeu:
//
// var só no process.env → CHEGA ao servidor (mescla, não substitui)
// var só no `env:` do config → chega
// var nos DOIS, valores dif. → vence a do `env:` do config
//
// A primeira linha é o que mantém `AUTH_RATE_LIMIT_LOGIN_IP` funcionando:
// ele é definido no passo do workflow e quem aplica o teto é o SERVIDOR.
//
// A terceira é a armadilha. Uma chave que exista no `.env.e2e` E no ambiente
// do CI silenciosamente resolve para valores DIFERENTES nos dois lados —
// servidor com um, processo de teste com outro. Foi assim que
// `INTERNAL_SECRET` derrubou 8 specs com 401. Por isso o workflow publica o
// `.env.e2e` inteiro no ambiente do job em vez de redigitar valores: uma
// fonte não colide consigo mesma.
timeout: 120_000,
},
projects: [{ name: "chromium", use: { browserName: "chromium" } }],
});