mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
fix(e2e): OPS83 — estabilizar os e2e do verify sem reduzir cobertura; AgendaFeed roda em prod (Closes #824)
This commit is contained in:
parent
627492000b
commit
e4cb8d1006
13 changed files with 260 additions and 35 deletions
7
.github/workflows/ci-pr.yml
vendored
7
.github/workflows/ci-pr.yml
vendored
|
|
@ -208,11 +208,15 @@ jobs:
|
|||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Build for e2e
|
||||
# Selected mode needs the build too (the Playwright webServer serves
|
||||
# the `.next-e2e` production build).
|
||||
# the `.next-e2e` production build). NEXT_PUBLIC_SITE_URL is inlined
|
||||
# at build time, so the canonical `.test` origin must reach THIS build
|
||||
# — it lets the agenda feed dialog generate a link under the
|
||||
# production build when AgendaFeed is in the blast radius (OPS83).
|
||||
if: steps.scope.outputs.e2e_mode == 'selected'
|
||||
run: pnpm build
|
||||
env:
|
||||
NEXT_DIST_DIR: .next-e2e
|
||||
NEXT_PUBLIC_SITE_URL: https://feed.e2e.teqo.test
|
||||
- name: E2E tests (blast radius — manifest-selected specs)
|
||||
if: steps.scope.outputs.e2e_mode == 'selected'
|
||||
# OPS72: the PR never runs e2e full — mode `full` (high-risk diff)
|
||||
|
|
@ -221,6 +225,7 @@ jobs:
|
|||
# 4 workers in one process replace the former 2 shards × 2 workers.
|
||||
env:
|
||||
PLAYWRIGHT_WORKERS: 4
|
||||
NEXT_PUBLIC_SITE_URL: https://feed.e2e.teqo.test
|
||||
# The `--` separates pnpm from the scope paths: pnpm consumes it and
|
||||
# playwright receives the bare spec paths (no flag parsing drift).
|
||||
run: pnpm test:e2e -- ${{ steps.scope.outputs.e2e_specs }}
|
||||
|
|
|
|||
5
.github/workflows/deploy.yml
vendored
5
.github/workflows/deploy.yml
vendored
|
|
@ -141,15 +141,20 @@ jobs:
|
|||
- name: Build for e2e
|
||||
# Same dir the Playwright webServer serves (playwright.config.ts
|
||||
# falls back to '.next-e2e'; gate-ci.mjs builds there too).
|
||||
# NEXT_PUBLIC_SITE_URL is inlined at build time, so the canonical
|
||||
# `.test` origin must reach THIS build — it is what lets the agenda
|
||||
# feed dialog generate a link under the production build (OPS83).
|
||||
run: pnpm build
|
||||
env:
|
||||
NEXT_DIST_DIR: .next-e2e
|
||||
NEXT_PUBLIC_SITE_URL: https://feed.e2e.teqo.test
|
||||
- name: E2E tests (full suite, single job, 4 workers)
|
||||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||||
# No `--` separator: pnpm would forward it verbatim to playwright and
|
||||
# the flag would be parsed as a file pattern ("No tests found").
|
||||
env:
|
||||
PLAYWRIGHT_WORKERS: 4
|
||||
NEXT_PUBLIC_SITE_URL: https://feed.e2e.teqo.test
|
||||
run: pnpm test:e2e
|
||||
|
||||
deploy:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
---
|
||||
|
||||
**OPS83 — desbloquear deploy: estabilizar os e2e do verify sem reduzir cobertura; AgendaFeed volta a rodar em prod (2026-08-24):** o deploy manual (`deploy.yml`) estava bloqueado pelo job `verify` (run #15: 1 failed + 7 flaky + 2 skipped + 1 did-not-run). Causas estudadas e corrigidas por reescrita de specs (settle/poll/hermeticidade/escopo de locator) — **nenhum** `test.flaky`, nenhuma remoção de cobertura, nenhuma mudança de workers/retries, zero mudança de app/contrato/acesso: (1) kill switch da home — o poll de server-HTML convergia enquanto a seção renderizada ainda mostrava o link do Instagram; passou a pollar o **DOM rendered** escopado a `[data-home-section="contents"]` (`expectSectionCardCount`, usado também no sibling de fallback IG); (2) section-hidden — o revalidate `tag=posts` era engolido por `.catch`; agora o POST é **assertado** e a convergência ganha orçamento explícito (não bump cego); (3) SavedFilters strict-mode — o link 'Municípios' agora é escopado ao container `[data-slot="sidebar"]` (2º match = bottom nav, #669 absorvida); (4) People — gate `S:` + settle do stream antes de asserir rows (testes 15, pós-search e C118); (5) Municípios:642 — poll DOM da liderança específica em vez de `.first()` no stream; (6) Municípios:386 B176 — `settleMunicipalityStream` entre remove-chip e o próximo clique de opção; (7) Newsletter:222 — `toHaveURL(/#novidades/)` sem `$` (o `?e2e=scroll-…` é legítimo); (8) DemandVisibility:62 — `.goto` com cache-buster para a asserção de 404 do peer (a cache RSC servia a demanda acessível após a troca de cookie). **AgendaFeed (Decision B):** os 2 `test.skip(isProdMode)` foram **removidos** — o build e2e agora inlina `NEXT_PUBLIC_SITE_URL=https://feed.e2e.teqo.test` (`.test` TLD, passa o fail-closed `requireProductionDNSOrigin`), o diálogo gera o link canônico e os testes asserem o **path** e buscam via `campaign.baseURL + path`; confirmado que share usa `window.location.origin`, webauthn é desacoplado via request-host e webhooks usam `campaign.baseURL` — nenhum outro e2e depende do origin. Escopo config: `playwright.config.ts` (default do webServer env em prod), `deploy.yml` e `ci-pr.yml` (env do build-for-e2e + do passo de testes). Verificação: specs afetadas verdes em dev mode (2×) e AgendaFeed verde em `E2E_PROD=1`; `pnpm lint`/`tsc`/`format:check`/`knip`/`check:cycles`/`test:unit` (2443) verdes; PR em main com auto-merge.
|
||||
|
||||
**OPS79 — última migração de dados em produção: plataforma antiga → nova (vertical campanha) (2026-08-23):** com a fonte congelada pelo OPS80 (zero escritas no Neon desde 2026-08-23 22:30 UTC), reconciliação e migração do residual. **Descoberta-chave:** a vertical campanha JÁ estava 100% íntegra no `teo_1313` (aceite do OPS51 confirmado por reconciliação repetível — diff 0 em contagens e junta de IDs das 13 coleções; `municipality_rels` só 3 links de edição de portfólio pós-OPS51, absorvidos como não-dado). O único residual real entre Neon e target era a `signature` pública 1486 (petition `fim-escala-6x1`) com seu contato + subscription, cujo contato originário (Juares) **não existia** no target (o id 2221 do target é outra pessoa). **Ferramentas:** `scripts/reconcile-campaign-vertical.mjs` (read-only, 19/19 PASS, nunca imprime PII) + `scripts/migrate-signature-orphan.mjs` (dry-run/apply, guard `OPS79_MIGRATE_CONFIRM=1`, transação única, PII mascarada nos logs). **Execução 100% no homeserver:** aplicado contact novo **2225** + signature **1486** preservada + subscription **1492** + sequences ajustadas; pós-verificação `signature` 1485=1485, reconcile 19/19, smoke `/` `/campanha` `/admin` 200. Runbook (rollback = DELETE das 3 linhas + setval) em `docs/ops/teqo-1313-deploy.md` §OPS79. Sem migration de schema, sem access/Consent novo, sem UI. Closes #797.
|
||||
|
||||
**E2E-FULL-FLAKE-C142MUN — endurecer write-chrome assertions sob 4 workers (2026-08-23):** as 3 asserts de `campaignPermissionProfile.e2e.spec.ts:85` (carteira FAB visível), `:101` (tudo FAB visível) e `campaignMunicipalities.e2e.spec.ts:115` (checkbox de advisor no `/editar`) falhavam consistentemente no `deploy.yml → verify` (suíte full, 4 workers, prod build) por latência de render/hydration do shell streamed sob carga — a sonda SSR prova que o FAB está correto no HTML; o falha é que `networkidle` finda antes do chunk `S:` commitar, e a assert `toBeVisible`/`.check()` usa o budget default que é insuficiente no runner hosted. Fix: adicionar `page.waitForFunction(() => document.querySelectorAll('div[id^="S:"]').length === 0)` (padrão C106 já consolidado nos irmãos do arquivo) antes das asserts de presença — dois bursts de spec, sem mudança de produto/access/migration. Verificado: `gate:fast` (2439 unit tests, all green); e2e das 2 specs isoladas + repeat-each=3 em 4 workers: 21/21 passed. Sem alteração de access, sem migration, sem redução de workers do full.
|
||||
|
|
|
|||
1
docs/changelog/2026-08-24-ops83.md
Normal file
1
docs/changelog/2026-08-24-ops83.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**OPS83 — desbloquear deploy: estabilizar os e2e do verify sem reduzir cobertura; AgendaFeed volta a rodar em prod (2026-08-24):** o deploy manual (`deploy.yml`) estava bloqueado pelo job `verify` (run #15: 1 failed + 7 flaky + 2 skipped + 1 did-not-run). Causas estudadas e corrigidas por reescrita de specs (settle/poll/hermeticidade/escopo de locator) — **nenhum** `test.flaky`, nenhuma remoção de cobertura, nenhuma mudança de workers/retries, zero mudança de app/contrato/acesso: (1) kill switch da home — o poll de server-HTML convergia enquanto a seção renderizada ainda mostrava o link do Instagram; passou a pollar o **DOM rendered** escopado a `[data-home-section="contents"]` (`expectSectionCardCount`, usado também no sibling de fallback IG); (2) section-hidden — o revalidate `tag=posts` era engolido por `.catch`; agora o POST é **assertado** e a convergência ganha orçamento explícito (não bump cego); (3) SavedFilters strict-mode — o link 'Municípios' agora é escopado ao container `[data-slot="sidebar"]` (2º match = bottom nav, #669 absorvida); (4) People — gate `S:` + settle do stream antes de asserir rows (testes 15, pós-search e C118); (5) Municípios:642 — poll DOM da liderança específica em vez de `.first()` no stream; (6) Municípios:386 B176 — `settleMunicipalityStream` entre remove-chip e o próximo clique de opção; (7) Newsletter:222 — `toHaveURL(/#novidades/)` sem `$` (o `?e2e=scroll-…` é legítimo); (8) DemandVisibility:62 — `.goto` com cache-buster para a asserção de 404 do peer (a cache RSC servia a demanda acessível após a troca de cookie). **AgendaFeed (Decision B):** os 2 `test.skip(isProdMode)` foram **removidos** — o build e2e agora inlina `NEXT_PUBLIC_SITE_URL=https://feed.e2e.teqo.test` (`.test` TLD, passa o fail-closed `requireProductionDNSOrigin`), o diálogo gera o link canônico e os testes asserem o **path** e buscam via `campaign.baseURL + path`; confirmado que share usa `window.location.origin`, webauthn é desacoplado via request-host e webhooks usam `campaign.baseURL` — nenhum outro e2e depende do origin. Escopo config: `playwright.config.ts` (default do webServer env em prod), `deploy.yml` e `ci-pr.yml` (env do build-for-e2e + do passo de testes). Verificação: specs afetadas verdes em dev mode (2×) e AgendaFeed verde em `E2E_PROD=1`; `pnpm lint`/`tsc`/`format:check`/`knip`/`check:cycles`/`test:unit` (2443) verdes; PR em main com auto-merge.
|
||||
130
docs/plans/desbloquear-deploy-e2e-verify-impl.md
Normal file
130
docs/plans/desbloquear-deploy-e2e-verify-impl.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Impl: OPS83 — Desbloquear deploy: estabilizar os e2e do verify (falhas + flakes) sem reduzir cobertura; entregar deploy real
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-08-24
|
||||
Issue: #824
|
||||
Intenção: docs/plans/desbloquear-deploy-e2e-verify.md
|
||||
Appetite restante: herdado (~2–3 dias eng; outcome verificável = deploy manual real verde na intenção; a forma técnica aqui fica dentro de "reescrever specs", que já foi human-gated)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** um deploy manual a partir de main termina com o job `verify` verde (suíte e2e **full**, prod mode, 4 workers) e o job `deploy` concluído — app novo no ar. Baseline #15 (HEAD a2db543e): 1 failed + 7 flaky + 2 skipped + 1 did-not-run + 191 passed.
|
||||
- **O que NÃO negociar:** não reduzir cobertura; não `test.flaky()`; não subir workers/retries como estratégia; os 2 skips do `campaignAgendaFeed` são **removidos** (rodam em prod mode); tratar a **família inteira** de classes estudadas (cast rotativa), não só as que falharam no #15; nunca tocar contrato público nem modelo de acesso.
|
||||
- **O que reavaliar:** (1) a hipótese de "DOM-scoped poll vs server-HTML poll" na home pode revelar que o helper atual `waitForHomeHTML` precisa ser _parametrizado_, não duplicado — depth check antes de criar twin; (2) se a causa raiz de `frontend:781` for só propagação de ISR, o conserto pode ser um _clearing determinístico da cache_ em vez de bump de orçamento cego.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Fase 1: asserts de shape\nNewsletter + SavedFilters] --> B[Fase B: Decision B\nAgendaFeed prod mode]
|
||||
B --> C[Fase 2: stream settle\nPeople + Municipios:642]
|
||||
C --> D[Fase 3: optimistic/POST\nMunicipios:386]
|
||||
D --> E[Fase 4: home DOM poll\nfrontend:781 + :1413]
|
||||
E --> F[Fase 5: swap hermético\nDemandVisibility:62]
|
||||
F --> G[Fase 6: família + CI verify]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A (só consertar as 9 que falharam no #15) | B (**família inteira de classes**, reescrita settle/poll/hermeticidade/escopo, como manda a intenção human-gated).
|
||||
|
||||
**Recomendação:** B — porque a cast rotativa faz a suíte re-falhar em classes irmãs a cada run; parar em A resolve o #16 e entrega o deploy amanhã, mas não cumpre o aceite de "2 runs consecutivos de verify com zero failed/flaky recorrente nas classes estudadas". Família aqui = os specs que **compartilham a classe de race**, não "rodar tudo de novo": as reescritas de helper da home e o padrão settle/`expectPostResponse` são aplicados aos testes-irmãos do mesmo serial-describe/spec que compartilham o mesmo código.
|
||||
|
||||
**Rejeitadas:** A porque resolve só os sintomas do #15 (a cast rotativa, estatística de 4 workers, rearranja as falhas no #16); e qualquer caminho de "configuração mágica" (workers/retries/`test.flaky`) porque viola o mandato de produto.
|
||||
|
||||
### Decisões de engenharia
|
||||
|
||||
- **D-1 — Home convergence (`frontend:1413` kill switch, `frontend:781` section hidden).**
|
||||
- Opções: A) continuar polling **server-HTML** body-wide e só trocar `toHaveCount` por `expect.poll`; B) **parametrizar `waitForHomeSectionState`/`waitForHomeHTML` para pollar o DOM rendered**, escopado ao `[data-home-section="contents"]`, e reter o server poll como gate de convergência secundário; C) apagar a cache ISR (`posts` fetch-cache) deterministamente antes de cada teste.
|
||||
- **Recomendação: B (+C para a ISR de `frontend:781`)** — porque o bug de `1413` é o IG card renderizar _no cliente dentro do payload de seção_ mesmo com a ausência convergida no body-server; pollar o DOM rendered é a ÚNICA fonte que observa o payload de seção. Reuso: `gotoHomeFresh` (cache-busting de navegação já existe), `expect.poll` (já usado no spec). Depth check: **estender** `waitForHomeSectionState` para aceitar `page` + `scope`, não criar `waitForHomeSectionDom` twin. Para `781`, a causa é propagação de ISR sob load; o trato é: (a) afirmar que o POST de `revalidate?tag=posts` **aterrissou** via `expectPostResponse(page, '/api/revalidate')` (determinístico, não wall-clock), e (b) esperar a convergência no DOM (não só no body) com orçamento explícito. Não subir "12s→N cego".
|
||||
- **Rejeitadas:** A porque não observa o payload de seção (o próprio flake); C-sozinho porque não cobre o componente cliente IG e surge como twin do helper existente (violação de depth check / reaproveitamento).
|
||||
- **D-2 — AgendaFeed prod mode (2 skips).**
|
||||
- Opções: A) mudança de **app** (override explícito de origem na action); B) mudança só de **teste/config**: no `webServer.env` em prod mode setar `NEXT_PUBLIC_SITE_URL=https://feed.e2e.teqo.test` (origem canônica HTTPS, satisfaz `requireProductionDNSOrigin` — `DNS_HOSTNAME_PATTERN` casa, não é `localhost`/IP/`.local*`/`.internal`) e nos 2 testes manter o fluxo do diálogo, asserir o `path` (`/campanha/agenda/ical/<36>`), e buscar o feed via `campaign.baseURL + path` (hermético contra o server local).
|
||||
- **Recomendação: B** — porque `requireProductionDNSOrigin` (validado em `src/utilities/campaignInviteOrigin.ts`) aceita `.test`-TLD; produção real continua fail-closed (o `NEXT_PUBLIC_SITE_URL` de prod é DNS público real); nenhum dead-open é introduzido — a configuração canônica é injetada só no server de teste do Playwright. Confirmado: **nenhuma mudança de app é estritamente exigida**; a action `createCalendarFeedLink` → `buildFeedUrl` lê `NEXT_PUBLIC_SITE_URL` do processo do server e passa a usar a origem canônica de teste, gerando o mesmo caminho; o teste ignora o origin absoluto ao buscar.
|
||||
- **Rejeitadas:** A porque muda código de produção (contrato/segurança) para ceder terreno a teste — desnecessário e mais caro de reverter.
|
||||
- **D-3 — Strict-mode locator `campaignSavedFilters:64` (`Municípios` resolve 2).**
|
||||
- Opções: A) `.first()`/`.last()`; B) escopar ao bar-região sidebar onde o shortcut vive (ex.: `campaignPageChrome` → container de sidebar) antes do `getByRole('link', {name:'Municípios', exact:true})`; C) trocar por `goto` (abandonaria a navegação hermética).
|
||||
- **Recomendação: B, fallback A** — porque a intenção explicitamente preserva o _goto-avoidance_ (navegação por link evita o `net::ERR_ABORTED` de RSC in-flight); escopar ao container de sidebar é o mais correto semânticamente (é o "Municípios" da navegação que queremos). O strict-mode (#669) é a feature que expôs o 2º match — absorvida aqui como causa-raiz.
|
||||
- **Rejeitadas:** C porque reintroduz a race que o comentário do teste documenta (medida em CI 2026-07-30).
|
||||
- **D-4 — DSPR/streaming settle `campaignPeople:37` + `campaignMunicipalities:709`.**
|
||||
- Opções: A) aplicar em cada teste o `waitForFunction(div[id^="S:"]===0)` (padrão `campaignMunicipalities:708`) **seguido de um poll DOM escopado na row específica**; B) só o `S:` gate genérico; C) `expect(locator).toBeVisible()` hard (estado atual).
|
||||
- **Recomendação: A** — `S:` gate existente já provado no spec irmão; mas o comentário em `:642` admite que o chunk da lista pode cair depois do quiesce, então o gate genérico NÃO é suficiente — o poll em `expect.poll` escopado na row é o que observa o payload real. Recomendado **evitar `.first()`** em stream não-determinístico de milhares (usa-se poll até stabilizar match único).
|
||||
- **Rejeitadas:** B porque o comentário do próprio spec admite que o chunk atrasa além do `S:`; C é a causa da flake.
|
||||
- **D-5 — Optimistic auto-apply `campaignMunicipalities:386` (facet churn).** Envolver cada interação removendo chip em `expectPostResponse(page, ...)` (fixture já provê, `:530`), e trocar `toHaveCount`/clique hard por `expect.poll` em contagem de chips e presença de opção. Opções de isolar o churn: esperar `listbox` re-render → `expect.poll` no count (Recomendação); `waitForTimeout` cego (Rejeitada — enche, não estabiliza).
|
||||
- **D-6 — Swap hermenêutico `campaignDemandVisibility:62`.** Após `sessionFor(peer)` + `goto`, a navegação pode vir de RSC cache/stream da troca responsável→peer (servindo a demanda acessível em vez do 404). Opções: A) `.goto(URL, { cache: 'no-store' })`/`cache-buster` no `demandURL` da asserção de 404; B) `router.refresh()`/"nova requisição" entre os swaps; C) navegar para `about:blank` e voltar ao `demandURL` com cache-buster. **Recomendação: A** (fetch bypass de cache na navegação da asserção) — mínimo e mirar a raiz (RSC serve da cache). A guarda do swap em si (`sessionFor`→`clearCookies`+token) permanece.
|
||||
- **D-7 — "1 did not run" do #15.** **Não virar task**: explicar por run (worker crash ou `serial-describe` abortado quando um teste do describe falha). Tratar como observável no Fase 6 se reproduzir, nunca como item barato de primeiro.
|
||||
|
||||
## Componentes / mudanças
|
||||
|
||||
- **`tests/e2e/frontend.e2e.spec.ts`** (serial `Campaign home content section`): parametrizar portar os helpers `waitForHomeSectionState`/`waitForHomeHTML` para aceitar `page` + scope de seção; reescrever `781` e `1413` para DOM-poll escopado; aplicar o mesmo padrão aos irmãos `1276/1337` (S2 share + IG fallback) que compartilham a classe IG card/API-down.
|
||||
- **`playwright.config.ts`**: no bloco `webServer` do prod mode (`isProdMode`), setar `NEXT_PUBLIC_SITE_URL` para `https://feed.e2e.teqo.test` quando não vier do ambiente (canonical test origin) — o default atual é `baseURL` (localhost), que `requireProductionDNSOrigin` rejeita. Não tocar o `dev` mode.
|
||||
- **`tests/e2e/campaignAgendaFeed.e2e.spec.ts`**: remover os 2 `test.skip(isProdMode, …)`; manter o fluxo do diálogo (cover); asserir path `/campanha/agenda/ical/<36>`; buscar via `campaign.baseURL + path` ao invés do URL absoluto.
|
||||
- **`tests/e2e/campaignSavedFilters.e2e.spec.ts`**: escopar o `getByRole('link', {name:'Municípios', exact:true})` ao container de sidebar (fallback `.first()`).
|
||||
- **`tests/e2e/campaignPeople.e2e.spec.ts`** (e irmãos `239/338`): `waitForFunction(S:)` + poll DOM na row específica; evitar `.first()` no stream.
|
||||
- **`tests/e2e/campaignMunicipalities.e2e.spec.ts`**: `386` → `expectPostResponse` + `expect.poll` em chips/opções; `642` → poll DOM na row específica.
|
||||
- **`tests/e2e/campaignNewsletter.e2e.spec.ts`**: `222` → em `toHaveURL(/#novidades/)` (drop do `$`) ou assert de `location.hash`.
|
||||
- **`tests/e2e/campaignDemandVisibility.e2e.spec.ts`**: `62` → cache-busted `.goto` na asserção de 404 do peer.
|
||||
- **Migration:** sem migration (nenhuma mudança de schema).
|
||||
- **Access / Consent:** nenhuma; nada toca modelo de acesso nem Consent — parede de produto respeitada.
|
||||
- **UI:** N/A — sem UI.
|
||||
|
||||
## Dados → forma (se aplicável)
|
||||
|
||||
N/A — sem superfície de dados; o "dado" do aceite é o resultado do workflow (failed/flaky/skipped por run).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Fase 1 — asserts de shape (barato, zero risco).**
|
||||
- `campaignNewsletter:222` (regex sem `$`); `campaignSavedFilters:26` (escopo sidebar).
|
||||
- Verificação: `pnpm test:e2e --no-deps --project=campaign -- tests/e2e/campaignNewsletter.e2e.spec.ts` e `-- tests/e2e/campaignSavedFilters.e2e.spec.ts` (rodar cada 2× local).
|
||||
2. **Fase B — Decision B, AgendaFeed prod mode (track independente).**
|
||||
- `playwright.config.ts` `webServer.env` prod; reescrever os 2 testes sem `skip`.
|
||||
- Verificação: `pnpm test:e2e --no-deps --project=campaign -- tests/e2e/campaignAgendaFeed.e2e.spec.ts` com `E2E_PROD=1`; confirmar que os 2 voltaram a **passar** em prod mode (antes: skipped) e que o C113 continua verde.
|
||||
3. **Fase 2 — stream settle.**
|
||||
- `campaignPeople:15` (+ irmãos 239/338); `campaignMunicipalities:642`.
|
||||
- Verificação: `pnpm test:e2e --no-deps --project=campaign -- tests/e2e/campaignPeople.e2e.spec.ts` e `-- tests/e2e/campaignMunicipalities.e2e.spec.ts` (2×).
|
||||
4. **Fase 3 — optimistic/POST.**
|
||||
- `campaignMunicipalities:386` (`expectPostResponse` + `expect.poll`).
|
||||
- Verificação: mesma spec 2× + `--grep "B176"` no no-deps.
|
||||
5. **Fase 4 — home DOM poll.**
|
||||
- `frontend.e2e.spec.ts`: parametrizar helpers; reescrever `781` + `1413`; alinhar irmãos `1276/1337`.
|
||||
- Verificação: `pnpm test:e2e --no-deps --project=frontend -- tests/e2e/frontend.e2e.spec.ts` (2×; serial describe dá determinismo).
|
||||
6. **Fase 5 — swap hermétic.**
|
||||
- `campaignDemandVisibility:62`.
|
||||
- Verificação: `pnpm test:e2e --no-deps --project=campaign -- tests/e2e/campaignDemandVisibility.e2e.spec.ts` (2×).
|
||||
7. **Fase 6 — família inteira + job verify.**
|
||||
- Rodar a **família de classes estudadas** (não só o #15): re-rodar todos os specs tocados **mais** os irmãos da classe (frontend S2/IG, municipios stream/omnibox, people stream) e confirmar zero failed/flaky recorrente.
|
||||
- Verificação local full family; depois `CI`/verificar o job `verify` do `.github/workflows/deploy.yml` (via `workflow_dispatch`) — o mete o `deploy`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Correr atrás de 1 flake isolado e fechar (rabbit hole 1 da intenção).
|
||||
- "Marcar flaky/retry 3+"; subir workers (rabbit hole 2).
|
||||
- Infra nova de "um server/DB por worker" sem tentar o barato primeiro (rabbit hole 3).
|
||||
- O "1 did not run" do #15: **não presumir** — se não reproduzir no Fase 6, é explicado por run (worker crash / serial abort) e documentado, não vira task.
|
||||
- `campaignLeaderships:59`, `campaignActivity:131`, `campaignPermissionProfile:85/101` (já curado em a97cd56f): **verificar** (Fase 6) mas não reescrever à toa se verde.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Test hidden em prod mode volta a falhar por origem canônica não-resolvível.** Mitigação: a busca do feed usa `campaign.baseURL + path` (ignora o origin absoluto), e o origin canônico é só a string do `NEXT_PUBLIC_SITE_URL` do server de teste — nunca navegamos para ele. `requireProductionDNSOrigin` foi validado contra `.test`.
|
||||
- **Helper DOM poll regressa os irmãos do serial-describe da home.** Mitigação: re‑rodar o `frontend` serial inteiro 2× (Fase 4); reuso em vez de twin reduz a superfície.
|
||||
- **Dom-poll nova fonte de timeout** (orçamento). Mitigação: `expect.poll` com timeout explícito herdado do `expect` (`10s` default), não hard assert.
|
||||
- **Fase 1 quiebra strict-mode em outro ponto** (unit C142 #805). Mitigação: rodar unit/int da suíte afetada (`pnpm gate:fast` de domínio) ao fechar cada fase.
|
||||
- **Deploy verde num run, vermelho no próximo (cast rotativa).** Mitigação: aceite exige **2 runs consecutivos**; família inteira no Fase 6, não só o #15.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção coberto: deploy manual `verify` verde + `deploy` concluído (sem app change além do já-curtado a97cd56f)
|
||||
- [ ] Invariantes AGENTS/engineering-standards: nada de contrato público / acesso / Consent; reuso de helpers (`expectPostResponse`, `waitForFunction(S:)`, `waitForRouterSettled`, `gotoHomeFresh`) em vez de twins
|
||||
- [ ] Nenhum `test.flaky()`, nenhum skip removido indevidamente (os 2 skips do AgendaFeed **viram execução** em prod mode)
|
||||
- [ ] Família de classes estudadas re-rodada (não só o #15); zero failed/flaky recorrente em 2 runs
|
||||
- [ ] Unit/int de domínios tocados verdes (`pnpm gate:fast`) a cada fase com mudança de helper
|
||||
|
||||
## Decision quality self-score
|
||||
|
||||
1. Decisões caras têm rejeitadas? **Sim** — D-1 (A/C rejeitadas), D-2 (A rejeitada), D-3 (C rejeitada), D-4 (B/C), D-5 (timeout cego), D-6 (B/C).
|
||||
2. Abordagem cabe no appetite da intenção? **Sim** — script de família-estudada + reescritas locais; sem infra nova (~2–3 dias).
|
||||
3. Rabbit holes nomeados? **Sim** — seção própria.
|
||||
4. Depth check: reusa shells/helpers existentes? **Sim** — parametriza `waitForHomeSectionState` (não twin), reusa `expectPostResponse`/`waitForFunction(S:)`/`waitForRouterSettled`/`gotoHomeFresh`/`campaign.baseURL`.
|
||||
5. Intenção (aceite de produto) permanece satisfeita? **Sim** — sem perda de cobertura; sem toc em contrato/acesso; os 2 skips viram execução.
|
||||
**Score: 5/5 (gate ≥4).**
|
||||
|
||||
Todos os diagnósticos foram grounded no código real (specs, `waitForHomeHTML`, `requireProductionDNSOrigin` validando `.test`, `expectPostResponse`, padrão `S:`), e a Decisão B (AgendaFeed prod mode) foi confirmada como test/config-only — sem mudança de app exigida.
|
||||
|
|
@ -68,6 +68,16 @@ const youtubeStubUrl = youtubeStubUrlFor(baseURL)
|
|||
const youtubeStubPort = new URL(youtubeStubUrl).port
|
||||
const instagramStubUrl = instagramStubUrlFor(baseURL)
|
||||
const instagramStubPort = new URL(instagramStubUrl).port
|
||||
/*
|
||||
* Canonical HTTPS origin the e2e prod build inlines as NEXT_PUBLIC_SITE_URL
|
||||
* (OPS83 Decision B): `requireProductionDNSOrigin` demands an HTTPS public DNS
|
||||
* name, so the agenda feed dialog can generate a link under the production
|
||||
* build. `.test` is a reserved TLD (RFC 6761) — never resolves, exists only so
|
||||
* the feed link passes the guard; the specs fetch the feed via `campaign.baseURL
|
||||
* + path`, never this origin.
|
||||
*/
|
||||
const CANONICAL_E2E_ORIGIN = 'https://feed.e2e.teqo.test'
|
||||
|
||||
const workersOverride = process.env.PLAYWRIGHT_WORKERS
|
||||
const workers = workersOverride === undefined ? 2 : Number(workersOverride)
|
||||
if (!Number.isInteger(workers) || workers < 2) {
|
||||
|
|
@ -216,7 +226,14 @@ export default defineConfig({
|
|||
// owns that entire directory and may clear nested production bundles.
|
||||
NEXT_DIST_DIR: process.env.NEXT_DIST_DIR ?? '.next-e2e',
|
||||
PAYLOAD_SECRET: process.env.PAYLOAD_SECRET ?? 'test-only-secret-not-used-in-production',
|
||||
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL ?? baseURL,
|
||||
// Prod mode serves a production build where `getCampaignInviteBaseURL`
|
||||
// fails closed unless NEXT_PUBLIC_SITE_URL is an HTTPS public DNS name
|
||||
// (C98). The e2e build inlines this same value (see the "Build for e2e"
|
||||
// steps in deploy.yml/ci-pr.yml), so the dialog-generated feed link is
|
||||
// canonical — and the spec fetches it back via `campaign.baseURL`, never
|
||||
// the absolute origin (OPS83 Decision B).
|
||||
NEXT_PUBLIC_SITE_URL:
|
||||
process.env.NEXT_PUBLIC_SITE_URL ?? (isProdMode ? CANONICAL_E2E_ORIGIN : baseURL),
|
||||
// Lets e2e specs bust the `posts` tag after direct REST deletes (cleanup),
|
||||
// mirroring the documented post-seed runbook against the deployed site.
|
||||
REVALIDATE_SECRET: process.env.REVALIDATE_SECRET ?? 'e2e-revalidate-secret',
|
||||
|
|
|
|||
|
|
@ -5,22 +5,17 @@ import { hookFilledCreateData } from '../../src/utilities/hookFilledData.js'
|
|||
import { campaignPageChrome, expect, test } from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
/**
|
||||
* The full generate → fetch flow needs the app served from a local origin:
|
||||
* `getCampaignInviteBaseURL` rejects non-public DNS in NODE_ENV=production
|
||||
* (fail-closed — the feed URL must be canonical), and CI/`pnpm push` boot the
|
||||
* production build (`E2E_PROD=1`) with `NEXT_PUBLIC_SITE_URL=http://localhost:3000`.
|
||||
* The CI covers the unlocked dialog via the C93 component unit and the
|
||||
* filterless feed content via the C93 int tests; the e2e here pins the real
|
||||
* end-to-end flow on the dev server.
|
||||
* OPS83 Decision B — these flows now run under the production build too: the
|
||||
* e2e build inlines `NEXT_PUBLIC_SITE_URL=https://feed.e2e.teqo.test`
|
||||
* (`.test` TLD, passes `requireProductionDNSOrigin` fail-closed), so the
|
||||
* dialog can generate a canonical feed link. The tests assert the PATH and
|
||||
* fetch the feed via `campaign.baseURL + path` — never the canonical origin,
|
||||
* which is deliberately non-resolving.
|
||||
*/
|
||||
const isProdMode =
|
||||
['1', 'true'].includes(process.env.E2E_PROD ?? '') || ['1', 'true'].includes(process.env.CI ?? '')
|
||||
|
||||
test.describe('Agenda — link de import (C98)', () => {
|
||||
test.setTimeout(90_000)
|
||||
|
||||
test('gera o link sem filtros pelo header e o feed responde iCal', async ({ campaign, page }) => {
|
||||
test.skip(isProdMode, 'fluxo completo exige origem local (dev server)')
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
|
@ -61,10 +56,13 @@ test.describe('Agenda — link de import (C98)', () => {
|
|||
const linkInput = dialog.getByRole('textbox', { name: 'Link de import' })
|
||||
await expect(linkInput).toBeVisible({ timeout: 15_000 })
|
||||
const feedUrl = await linkInput.inputValue()
|
||||
// The link carries the canonical origin (OPS83 Decision B) — the PATH is
|
||||
// the contract, the origin is test-only and deliberately non-resolving.
|
||||
expect(feedUrl).toMatch(/\/campanha\/agenda\/ical\/[0-9a-f-]{36}$/)
|
||||
|
||||
// O link responde com o feed iCal do recorte pedido (a atividade criada).
|
||||
const response = await page.request.get(feedUrl)
|
||||
// Fetch against the local server by path — never the absolute origin.
|
||||
const response = await page.request.get(`${campaign.baseURL}${new URL(feedUrl).pathname}`)
|
||||
expect(response.status()).toBe(200)
|
||||
expect(response.headers()['content-type']).toContain('text/calendar')
|
||||
const body = await response.text()
|
||||
|
|
@ -73,7 +71,6 @@ test.describe('Agenda — link de import (C98)', () => {
|
|||
})
|
||||
|
||||
test('gera o link sem filtros pelo FAB mobile', async ({ campaign, page }) => {
|
||||
test.skip(isProdMode, 'fluxo completo exige origem local (dev server)')
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
|
|
|||
|
|
@ -95,8 +95,13 @@ test.describe('C143 — demand visibility (explicit responsibles)', () => {
|
|||
await page.getByRole('button', { name: 'Salvar responsáveis' }).click()
|
||||
await expect(page.getByRole('button', { name: `Remover ${peer.name}` })).toHaveCount(0)
|
||||
|
||||
// The peer already rendered THIS URL (granted, line 88); the Next router
|
||||
// cache serves the previous RSC payload on a plain goto even after the
|
||||
// cookie swap (OPS83 run #15: the accessible demand showed instead of the
|
||||
// 404). The route ignores unknown query params — bust the cache so the
|
||||
// revoked access is observed from the server.
|
||||
await campaign.sessionFor(context, peer)
|
||||
await page.goto(demandURL)
|
||||
await page.goto(`${demandURL}?e2e=${Date.now()}`)
|
||||
await expect(page.getByText('This page could not be found.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ const visibleMunicipalityButton = (page: Page, name: string) =>
|
|||
.getByRole('button', { name: new RegExp(`^${escapeRegExp(name)}(?: —|$)`) })
|
||||
.filter({ visible: true })
|
||||
|
||||
/**
|
||||
* Waits for the municipality list's RSC stream to commit (no transient `S:`
|
||||
* shell copies) before interacting with the rows/options again. Chip removal
|
||||
* and omnibox facet changes are client-side `navigate` calls that re-stream
|
||||
* the list; clicking an option against the stale DOM races that stream
|
||||
* (observed in the OPS83 verify run #15 at 4 workers).
|
||||
*/
|
||||
const settleMunicipalityStream = (page: Page) =>
|
||||
page.waitForFunction(() => document.querySelectorAll('div[id^="S:"]').length === 0)
|
||||
|
||||
const ensureWideMunicipalityList = async (page: Page) => {
|
||||
// The resizable panel hydrates independently from the RSC list and can
|
||||
// change the container stage after a cell first becomes visible. These
|
||||
|
|
@ -437,9 +447,12 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
await expect(tableRows).toHaveCount(1)
|
||||
await expect(page.getByRole('link', { name: municipalityA.name, exact: true })).toBeVisible()
|
||||
|
||||
// Removing the chip restores the unfiltered 25-row page 1.
|
||||
// Removing the chip restores the unfiltered 25-row page 1. The removal is
|
||||
// a client `navigate` — poll the chip out and let the stream commit before
|
||||
// touching the omnibox again (OPS83: the next option click raced this).
|
||||
await removeChip(`Dobradinha: ${deputyName} (BR176)`).click()
|
||||
await expect(removeChip(`Dobradinha: ${deputyName} (BR176)`)).toHaveCount(0)
|
||||
await settleMunicipalityStream(page)
|
||||
|
||||
// Partido by acronym → the same single municipality (its only dobradinha).
|
||||
await omnibox.fill('BR176')
|
||||
|
|
@ -450,6 +463,7 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
|
||||
await removeChip('Partido: BR176').click()
|
||||
await expect(removeChip('Partido: BR176')).toHaveCount(0)
|
||||
await settleMunicipalityStream(page)
|
||||
|
||||
// Liderança by contact name → municipality B only.
|
||||
await omnibox.fill(leadership.contactName)
|
||||
|
|
@ -704,9 +718,13 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
// C106 — the dossiê tab streams a transient `S:` shell copy; under load the
|
||||
// leadership-list chunk can land after `goto` settles, so let the stream
|
||||
// commit before asserting the leadership link (same class as the `:115`
|
||||
// editar checkbox and the C142 FAB).
|
||||
// editar checkbox and the C142 FAB). The generic `S:` gate alone is not
|
||||
// enough (observed in the OPS83 verify run #15): poll the DOM for the
|
||||
// specific link instead of `.first()` on the nondeterministic stream.
|
||||
await page.waitForFunction(() => document.querySelectorAll('div[id^="S:"]').length === 0)
|
||||
await expect(page.getByRole('link', { name: contact.name }).first()).toBeVisible()
|
||||
const leadershipLink = page.getByRole('link', { name: contact.name })
|
||||
await expect.poll(() => leadershipLink.count(), { timeout: 15_000 }).toBeGreaterThan(0)
|
||||
await expect(leadershipLink.first()).toBeVisible()
|
||||
await page.getByLabel('Quantos votos a liderança traz neste município?').fill('250')
|
||||
await page.getByRole('button', { name: 'Declarar' }).click()
|
||||
await expect(page.getByText('Declaração de votos registrada.')).toBeVisible()
|
||||
|
|
|
|||
|
|
@ -222,7 +222,9 @@ test.describe('Campaign home novidades hash scroll', () => {
|
|||
test('hero CTA scrolls to the newsletter section (mobile)', async ({ page }) => {
|
||||
await page.goto(`/?e2e=scroll-${scrollSuffix}`)
|
||||
await page.locator('[data-cta="secondary"]').click()
|
||||
await expect(page).toHaveURL(/#novidades$/)
|
||||
// The URL legitimately carries the `?e2e=scroll-…` cache-buster query,
|
||||
// so assert only the hash (no trailing `$`), which lands after the click.
|
||||
await expect(page).toHaveURL(/#novidades/)
|
||||
await expect(page.locator('[data-home-section="newsletter"]')).toBeInViewport()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ test.describe('Pessoas — lista unificada', () => {
|
|||
|
||||
// The merged row carries the leadership person with its capacity chips
|
||||
// (desktop table — the mobile card twin is `md:hidden` at this viewport).
|
||||
// The people list streams RSC chunks; under load the row can land after
|
||||
// `goto`'s settle, so let the stream commit and then poll the DOM for the
|
||||
// specific row (same class as the `S:` gate at campaignMunicipalities:708,
|
||||
// observed as a flake in the OPS83 verify run #15 at 4 workers).
|
||||
await page.waitForFunction(() => document.querySelectorAll('div[id^="S:"]').length === 0)
|
||||
await expect(page.getByRole('row', { name: new RegExp(contactName) })).toBeVisible()
|
||||
// The coordinator account itself is a staff person (with a ficha) — ≥ 2 rows.
|
||||
await expect(page.getByText(/^\d+ pessoas?$/)).toBeVisible()
|
||||
|
|
@ -43,6 +48,8 @@ test.describe('Pessoas — lista unificada', () => {
|
|||
await omnibox.fill(contactName.slice(0, 8))
|
||||
await omnibox.press('Enter')
|
||||
await expect(page).toHaveURL(/q=/)
|
||||
// The filter navigation re-streams the list — let it commit before the row.
|
||||
await page.waitForFunction(() => document.querySelectorAll('div[id^="S:"]').length === 0)
|
||||
await expect(page.getByRole('row', { name: new RegExp(contactName) })).toBeVisible()
|
||||
})
|
||||
|
||||
|
|
@ -95,6 +102,9 @@ test.describe('Pessoas — lista unificada', () => {
|
|||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/pessoas')
|
||||
// The list streams RSC chunks; clicking the name link against the stale DOM
|
||||
// races the stream (OPS83: C118 flaked under parallel load).
|
||||
await page.waitForFunction(() => document.querySelectorAll('div[id^="S:"]').length === 0)
|
||||
|
||||
await page.getByRole('link', { name: contactName, exact: true }).click()
|
||||
await expect(page).toHaveURL(new RegExp(`/campanha/pessoas/${contactId}$`))
|
||||
|
|
@ -329,6 +339,8 @@ test.describe('Pessoas — lista unificada', () => {
|
|||
await expect(page.getByText('Ausência: Sem contato')).toBeVisible()
|
||||
await expect(page.getByText('Ausência: Sem base')).toBeVisible()
|
||||
await expect(page.getByText('Ausência: Sem assessor')).toBeVisible()
|
||||
// Multi-facet navigation re-streams the list — commit before asserting rows.
|
||||
await page.waitForFunction(() => document.querySelectorAll('div[id^="S:"]').length === 0)
|
||||
await expect(page.getByRole('row', { name: new RegExp(noPhoneName) })).toBeVisible()
|
||||
// The union widened: the staff leaderships have no advisors, so they match
|
||||
// `sem_assessor` — the row hidden by `sem_contato` alone is back in.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
/**
|
||||
|
|
@ -22,6 +24,13 @@ const RENAMED = 'Recorte renomeado'
|
|||
*/
|
||||
const NAVIGATION = { timeout: 20_000 }
|
||||
|
||||
/**
|
||||
* The nav link, scoped to the desktop sidebar: the same name also exists in the
|
||||
* mobile bottom nav, so a bare `getByRole` violates strict mode (#669, OPS83).
|
||||
*/
|
||||
const municipalityNavLink = (page: Page) =>
|
||||
page.locator('[data-slot="sidebar"]').getByRole('link', { name: 'Municípios', exact: true })
|
||||
|
||||
test.describe('Filtros salvos de Municípios', () => {
|
||||
test('saves the current recorte, restores it from the sidebar and deletes it', async ({
|
||||
campaign,
|
||||
|
|
@ -61,7 +70,7 @@ test.describe('Filtros salvos de Municípios', () => {
|
|||
// Another recorte is not this one, and the shortcut brings it back. Use the
|
||||
// sidebar link — a bare `goto` here races an in-flight RSC navigation and
|
||||
// flakes as net::ERR_ABORTED under prod load (measured in CI 2026-07-30).
|
||||
await page.getByRole('link', { name: 'Municípios', exact: true }).click()
|
||||
await municipalityNavLink(page).click()
|
||||
await expect(page).toHaveURL('/campanha/municipios', NAVIGATION)
|
||||
await expect(shortcut).not.toHaveAttribute('aria-current', 'page')
|
||||
await shortcut.click()
|
||||
|
|
@ -81,7 +90,7 @@ test.describe('Filtros salvos de Municípios', () => {
|
|||
await expect(page.getByRole('link', { name: RENAMED })).toHaveCount(0)
|
||||
// Emptying the group unmounts the sub-list, so focus lands on the nav link
|
||||
// rather than falling to <body>.
|
||||
await expect(page.getByRole('link', { name: 'Municípios', exact: true })).toBeFocused()
|
||||
await expect(municipalityNavLink(page)).toBeFocused()
|
||||
|
||||
// Deleting is undoable, which is why it never asks for confirmation.
|
||||
await page.getByRole('button', { name: 'Desfazer' }).click()
|
||||
|
|
|
|||
|
|
@ -690,6 +690,20 @@ test.describe('Campaign home content section', () => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the RENDERED contents section for a card/link count, scoped to
|
||||
* `[data-home-section="contents"]`. The server-HTML polls above converge on
|
||||
* the ISR response, but the page may serve a heuristic-cached body that lags
|
||||
* the server (measured in the OPS83 verify run #15: the server showed the
|
||||
* kill switch applied while the rendered section still held the Instagram
|
||||
* link). This is the source that observes the actual payload — pair it with
|
||||
* `gotoHomeFresh` so the navigation itself misses the cache.
|
||||
*/
|
||||
const expectSectionCardCount = (locator: Locator, expected: number, timeout = 15_000) =>
|
||||
expect
|
||||
.poll(async () => locator.count(), { timeout, message: 'contents section card count' })
|
||||
.toBe(expected)
|
||||
|
||||
// The home route is ISR (s-maxage 300, no max-age): Chromium applies a
|
||||
// heuristic cache to such responses and a plain goto may serve the previous
|
||||
// body even after the poll converged (measured on the S2 snapshot tests —
|
||||
|
|
@ -782,17 +796,20 @@ test.describe('Campaign home content section', () => {
|
|||
// The dev server persists its `posts` cache to .next-e2e/cache/fetch-cache
|
||||
// between runs, so an earlier full-state run could leave it populated.
|
||||
// Busting first makes the empty state deterministic on every rerun.
|
||||
await request
|
||||
.post(`${baseURL}/api/revalidate?tag=posts`, {
|
||||
headers: { 'x-revalidate-secret': revalidateSecret },
|
||||
})
|
||||
.catch(() => undefined)
|
||||
await waitForHomeSectionState(request, 'absent')
|
||||
// OPS83: assert the revalidate actually landed (a swallowed failure made
|
||||
// the server poll below time out in verify run #15) and converge on the
|
||||
// rendered DOM with an explicit ISR-regeneration budget — never a blind
|
||||
// wall-clock bump.
|
||||
const revalidate = await request.post(`${baseURL}/api/revalidate?tag=posts`, {
|
||||
headers: { 'x-revalidate-secret': revalidateSecret },
|
||||
})
|
||||
expect(revalidate.ok()).toBeTruthy()
|
||||
await waitForHomeSectionState(request, 'absent', 30)
|
||||
|
||||
// Plain URL on purpose: the test also pins the canonical home URL.
|
||||
await page.goto('/')
|
||||
await expect(page).toHaveURL(/\/$/)
|
||||
await expect(page.locator('[data-home-section="contents"]')).toHaveCount(0)
|
||||
await expectSectionCardCount(page.locator('[data-home-section="contents"]'), 0)
|
||||
await expect(page.getByText('A caminhada, em tempo real')).toHaveCount(0)
|
||||
|
||||
const proof = await page.locator('[data-home-section="proof"]').boundingBox()
|
||||
|
|
@ -1331,8 +1348,10 @@ test.describe('Campaign home content section', () => {
|
|||
const section = page.locator('[data-home-section="contents"]')
|
||||
await expect(section).toBeVisible()
|
||||
await expect(section.getByRole('link', { name: /E2e Artigo fallback IG/ })).toBeVisible()
|
||||
await expect(section.getByText('E2e Post do muro')).toHaveCount(0)
|
||||
await expect(section.getByRole('link', { name: /Seguir no Instagram/ })).toHaveCount(0)
|
||||
// Same rendered-DOM class as the kill-switch test (OPS83 run #15): the
|
||||
// server poll converged but the page served a stale body — poll the DOM.
|
||||
await expectSectionCardCount(section.getByText('E2e Post do muro'), 0)
|
||||
await expectSectionCardCount(section.getByRole('link', { name: /Seguir no Instagram/ }), 0)
|
||||
|
||||
await page.goto('about:blank')
|
||||
await expect(page).toHaveURL(/about:blank/)
|
||||
|
|
@ -1456,14 +1475,17 @@ test.describe('Campaign home content section', () => {
|
|||
['E2e Artigo kill switch'],
|
||||
['E2e Vídeo em destaque', 'YouTube →', 'E2e Post do muro', 'Seguir no Instagram'],
|
||||
)
|
||||
// The page can serve a heuristic-cached body that lags the server poll
|
||||
// (OPS83 run #15): navigate fresh AND poll the rendered section DOM —
|
||||
// the IG link is what failed there, scoped to the same payload.
|
||||
await gotoHomeFresh(page)
|
||||
const section = page.locator('[data-home-section="contents"]')
|
||||
await expect(section).toBeVisible()
|
||||
await expect(section.getByRole('link', { name: /E2e Artigo kill switch/ })).toBeVisible()
|
||||
await expect(section.getByText('E2e Vídeo em destaque')).toHaveCount(0)
|
||||
await expect(section.getByRole('link', { name: /YouTube →/ })).toHaveCount(0)
|
||||
await expect(section.getByText('E2e Post do muro')).toHaveCount(0)
|
||||
await expect(section.getByRole('link', { name: /Seguir no Instagram/ })).toHaveCount(0)
|
||||
await expectSectionCardCount(section.getByText('E2e Vídeo em destaque'), 0)
|
||||
await expectSectionCardCount(section.getByRole('link', { name: /YouTube →/ }), 0)
|
||||
await expectSectionCardCount(section.getByText('E2e Post do muro'), 0)
|
||||
await expectSectionCardCount(section.getByRole('link', { name: /Seguir no Instagram/ }), 0)
|
||||
|
||||
await page.goto('about:blank')
|
||||
await expect(page).toHaveURL(/about:blank/)
|
||||
|
|
|
|||
Loading…
Reference in a new issue