fix(e2e): clique no dia do calendário pelo contrato data-day — desbloqueia o verify (C104, miss #55)

O verify do deploy falhava nas 3 tentativas no e2e C104: o clique do dia
de término usava getByRole(name: "2 de Outubro de 2026"), e o matcher de
nome do Playwright é substring + case-insensitive — casava também os dias
12 e 22 do mesmo grid (strict mode violation) sempre que hoje+1 caía em
dia 01–09 do mês (02/10 no run de 2026-10-01).

O C104 passa a clicar via calendarDaySelector ([data-day="DD/MM/YYYY"], o
contrato do Calendar.tsx) e o guard miss #55 no e2eNavigationConventions
bane o padrão antigo em todos os specs, com teste do contrato do helper;
TESTING.md, changelog e post-mortem registram a miss.

Related #1406
This commit is contained in:
fsolla 2026-10-01 16:33:33 +00:00 committed by Francisco Solla
parent cd4942d9e2
commit 90768be1cf
6 changed files with 152 additions and 10 deletions

View file

@ -14,6 +14,8 @@ CI (`ci-pr.yml`) is a **single sequential job `checks`** (OPS62 X1): guards →
**Local e2e (OPS72 — skill step, discretionary):** before push, run the e2e specs you created + the same affected surface — `pnpm test:e2e:affected` runs `e2e-affected.mjs` against `origin/main`, migrates/seeds, then `pnpm test:e2e` with the selected specs (full suite on high-risk diffs). High-risk diffs diverge from PR CI by design: the PR runs no e2e (selected only, never full) and the local mirror runs the full suite. Forward extra Playwright args after `--` for a narrowed repro. Opt into production mode with `E2E_PROD=1 pnpm test:e2e:affected` (CI and prod mode serve the production build via `pnpm start`; plain local runs use `pnpm dev` — dev-mode flakes like cold-compile timeouts and dev-only console warnings do not reproduce in prod mode).
**Calendar day-cell clicks (miss #55, 2026-10-01):** click a calendar day cell through its stable contract — `calendarDaySelector(civilDate)` → `[data-day="DD/MM/YYYY"]` (`tests/e2e/helpers/agendaPeriodLabels.ts`) — never through the accessible name. date-fns formats the label as `"d de mês de ano"`, which is a substring of `"1d…"/"2d… de mês de ano"` in the same grid, and Playwright's name matcher is substring + case-insensitive by default: `"2 de outubro de 2026"` resolves to days 2, 12 and 22 (strict mode violation). The bug is date-dependent — it only fires when the target lands on day 01–09 of the month (the C104 spec picks today + 1). The `e2eNavigationConventions` unit guard fails the build on name-based day clicks.
**Navigation discipline in e2e (miss #54, 2026-07-30):** `page.goto` is for cold loads and URL contracts (deep links, redirects, legacy params); when the scenario is _continuity inside the authenticated shell_ — especially after a heavy list route — navigate in-shell instead (sidebar links, buttons, form submits), the way the user does. Never fire two `page.goto` back-to-back with nothing settling the first navigation (an `await expect(...)`, `waitForURL`, or interaction): the second aborts the first's in-flight RSC fetch (`net::ERR_ABORTED`, measured in `campaignSavedFilters`). The `e2eNavigationConventions` unit guard fails the build on unsettled goto pairs. Two sibling rules of the same family: wait for **interactive state** before clicking dynamic controls — `toBeVisible` proves SSR paint, not hydration (pre-hydration clicks no-op silently; `checkRadixWhenHydrated` is the precedent, P3-C class) — and gate WebAuthn ceremonies on `expectCampaignBiometricsReady` before the island's one-shot probe runs (miss #53). **Dev-mode caveat (closed 2026-08-10, P4-L/OPS30):** `campaignLeaderships`' chip-cell spec was failing deterministically under `pnpm dev` before B34 rewrote it; with the OPS30 prewarm/login hardening it ran ×3 green in dev + ×1 `E2E_PROD=1` (evidence in TECH-DEBT).
## Minimal test database (`pnpm db:seed:minimal`)

View file

@ -0,0 +1 @@
**Fix CI — clique no dia do calendário pelo nome acessível derrubava o verify do deploy (C104, miss #55) (2026-10-01):** o `verify` do deploy ficou vermelho em 2026-10-01 (runs `36816518055` e `36820002374`; Issue #1406, auto-unblock) porque o e2e do C104 clicava a célula de término com `getByRole('button', { name: '2 de Outubro de 2026' })` e o matcher de nome do Playwright é substring + case-insensitive: o nome acessível real do react-day-picker pt-BR ("sexta-feira, 2 de outubro de 2026") casa também com os dias 12 e 22 do mesmo grid — `strict mode violation ... resolved to 3 elements` nas 3 tentativas. A classe só dispara quando hoje + 1 cai em dia 01–09, por isso passou desde o C134 (commit `fe0a7999`), que removeu a data fixa do C104 mas manteve o clique pelo nome; a primeira detecção (#1401) foi fechada sem PR de fix. O run vermelho segurava a lane e os merges seguintes tiveram o deploy preflight-skipped (`77e7b0ef`, `cd4942d9`) — deploy do main bloqueado, sem impacto em produção (o `verify` roda antes de publicar). O fix troca o clique pelo contrato estável do Calendar, `calendarDaySelector(civilDate)` → `[data-day="DD/MM/YYYY"]` (novo helper em `tests/e2e/helpers/agendaPeriodLabels.ts`, aditivo), e adiciona o guard miss #55 no `e2eNavigationConventions`, que varre todos os `tests/e2e/**/*.e2e.spec.ts` e falha o build em clique de célula de dia por nome (mais o teste do contrato zero-padded), além do parágrafo de convenção no `docs/TESTING.md`; sem mudança de schema/produto. Provas: repro local falha sem o fix com o mesmo strict mode violation, o guard falha com o spec antigo (offender `tests/e2e/campaignActivity.e2e.spec.ts:355`), e2e C104 1 passed (1.4m, dev) e `pnpm gate:fast` verde (458 arquivos / 5.071 testes; os 7 flaky do run passaram no retry, sem relação causal com o diff). PR a preencher (este PR); a confirmação é o `verify` verde no deploy pós-merge e produção depende do approve humano (não aprovado por esta sessão).

View file

@ -0,0 +1,89 @@
# Post-mortem: clique no dia do calendário pelo nome acessível derruba o verify do deploy
> Template do `/bug-fix`. Preencha com fatos apurados; o que não for apurado fica "não apurado" — nunca invente.
## Registro
| Campo | Valor |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| Data do post-mortem | 2026-10-01 |
| Severidade | alta (bloqueou o deploy do main; CI vermelho — sem impacto em produção, o `verify` roda antes de publicar) |
| Ambiente | CI (job `verify` do Deploy no GitHub Actions) |
| Issue(s) | #1406 (auto-unblock; não fechar — o wrapper cuida) + #1401 (primeira detecção, fechada sem fix) |
| PR do fix | a preencher (este PR) — número registrado por commit de follow-up (precedente `59a6480d`) |
| Detectado por | CI (deploy run vermelho) / wrapper auto-unblock |
## Timeline
| Momento | Data/hora | Evento |
| ----------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Início provável | 2026-08-09 | C104 (`ea700e31`) criou o picker de término com data fixa (`/17 de agosto de 2026/`). |
| Fragilidade latente | 2026-08-13 | C134 (commit `fe0a7999`) passou a derivar hoje + 1, mas manteve o clique pelo nome acessível — a fragilidade só dispara quando hoje + 1 cai em dia 01–09 do mês. |
| Detecção 1 | 2026-10-01 04:44–05:02 UTC | run `36816518055` (commit `20d3781e`, docs(C240), docs-only), job `verify`, passo "E2E tests (full suite, single job, 4 workers)" falhou: C104 falhou nas 3 tentativas (mesmo strict mode violation); 2 failed (C104 + `campaignSpeechAcervo` C222, que não reapareceu no run seguinte), 3 flaky, 326 passed. Issue #1401 aberta (single-flight) e fechada em 2026-10-01T15:38:43Z sem PR de fix. |
| Detecção 2 / do disparo | 2026-10-01 ~15:40 / 16:08 UTC | run `36820002374` (commit `ca843fb4`, attempt 2 ~15:40Z), passo do `verify` falhou às 16:08Z: C104 falhou nas 3 tentativas; 1 failed, 7 flaky, 323 passed. Issue #1406 aberta (auto-unblock). |
| Bloqueio do deploy | 2026-10-01 | O run vermelho segurava a lane; os deploys de `77e7b0ef` (run `36886280567`) e `cd4942d9` (run `36887968057`) foram preflight-skipped. |
| Correção implementada | 2026-10-01 | Worktree `fix/falha-do-job-verify-do-deploy-github-actions-desbloqueie`. |
| Correção mergeada | a preencher | a preencher (este PR). |
| Deploy | a preencher | a preencher. |
| Verificado em prod | não se aplica | Bug de CI/teste; o comportamento público não muda. A confirmação é o `verify` verde no run de deploy pós-merge; produção depende do approve humano (não aprovado por esta sessão). |
## O bug
O passo de e2e full do `verify` falhava deterministicamente no teste `[campaign] › tests/e2e/campaignActivity.e2e.spec.ts › Agenda — calendário operacional › cria compromisso "Todo o dia" no slot, sem horário e com faixa no calendário (C104)`: ao abrir o date picker de "Término" e clicar no dia de hoje + 1, o Playwright abortava com `strict mode violation: ... getByRole('button', { name: '2 de Outubro de 2026' }) resolved to 3 elements` (dias 02, 12 e 22 de outubro). O CI ficou vermelho no run de 2026-10-01 e os merges seguintes tiveram o deploy preflight-skipped — o deploy do main ficou bloqueado. Nenhum impacto em produção: o `verify` roda antes de publicar. **Sintoma — não a causa.**
## Causa-raiz
Origem `file:line` pré-fix: `tests/e2e/campaignActivity.e2e.spec.ts:355` (`endPicker.getByRole('button', { name: endDayLabel }).click()`), com `endDayLabel` montado em `:352` ("2 de Outubro de 2026") a partir de `civilDatePlusDays(formatBahiaCivilDate(new Date()), 1)` (`:348`).
O accessible name real do botão vem do react-day-picker com locale pt-BR: `"sexta-feira, 2 de outubro de 2026"` (date-fns `PPPP`); `src/components/ui/Calendar.tsx:167` também expõe `data-day="02/10/2026"`. O matcher de nome do Playwright é substring + case-insensitive por default; `"2 de outubro de 2026"` é substring de `"12 de outubro de 2026"` e `"22 de outubro de 2026"` (mesmo grid). `exact: true` não serve (o nome tem o dia da semana). O bug só dispara quando hoje + 1 cai em dia 01–09; em outubro/2026 caiu em 02.
### 5 whys
1. Por que o `verify` quebrou? Porque o clique usou o nome acessível.
2. Por que o teste clicava pelo nome? Porque herdou esse clique do C104 original e o manteve no C134, que só trocou a data fixa por hoje + 1.
3. Por que o locator não usou o contrato estável? Porque `data-day` já existia no Calendar, mas não havia helper que o montasse — role + name parecia o locator "semântico" correto.
4. Por que passou nos runs anteriores? Porque a fragilidade é dependente do calendário (só dias 1–9) e o alvo caiu nessa faixa apenas em 2026-10-02.
5. Por que nenhuma camada a montante pegou? Porque unit/int não veem locators de DOM e o e2e de PR é `selected` (OPS72) — a classe só é pega no verify full do deploy.
## Correção
Sem mudança de schema/produto — o defeito era do locator do spec:
- `tests/e2e/helpers/agendaPeriodLabels.ts:46`: novo helper `calendarDaySelector(civilDate)` → `[data-day="DD/MM/YYYY"]` (o contrato do `Calendar.tsx:167`), aditivo (funções existentes intocadas).
- `tests/e2e/campaignActivity.e2e.spec.ts:357`: o C104 clica `endPicker.locator(calendarDaySelector(endCivilDate))`; comentário atualizado.
- `tests/unit/e2eNavigationConventions.unit.spec.ts:96`: guard miss #55 que varre todos os `tests/e2e/**/*.e2e.spec.ts` e falha o build em clique de célula de dia por nome (`*DayLabel` ou literais/templates com " de " e ano), + teste do contrato do helper (zero-padded) em `:119`.
- `docs/TESTING.md:17`: parágrafo "Calendar day-cell clicks (miss #55)", ao lado do miss #54.
Resolve a causa: o clique passa a mirar o atributo de contrato (inequívoco por construção) em vez do texto, e a suposição antiga ("clicar pelo nome é seguro") deixa de ser aceita sem falhar um teste.
## Verificação
- Sem o fix: repro local em dev (`env -u CI pnpm test:e2e --no-deps --project=campaign -g "Todo o dia"`) falhou exatamente com o strict mode violation (3 elementos: data-day 02/10, 12/10, 22/10) em 2026-10-01; no CI falhou nas 3 tentativas (runs `36816518055` e `36820002374`).
- Guard de regressão: com o spec antigo (stash) a suíte `tests/unit/e2eNavigationConventions.unit.spec.ts` falha com offender `tests/e2e/campaignActivity.e2e.spec.ts:355`; com o fix, 4 passed.
- Com o fix: e2e C104 1 passed (1.4m, dev); `pnpm gate:fast` verde (eslint `--max-warnings=0`, `tsc --noEmit`, unit 458 arquivos / 5.071 testes).
- Flaky do run `36820002374` (7, todos passaram no retry; sem relação causal com o diff): `frontendShareLink` (kill-switch, 404 pós-republish), `campaignAgendaGoogleSync` (500 `POST /campanha/agenda`), `campaignBottomNav` (clique não navegou), `campaignMunicipalities` (apoiador não visível), `campaignPeople` (filtro não aplicado na URL), `campaignTerritoriesHttp` (cleanup com transação abortada), `frontendJingles` (404 de mídia). O run `36816518055` teve 3 flaky.
- CI: a preencher (PR).
- Prod: não se aplica (bug de CI/teste).
## Prevenção
| Estratégia | Custo | Estado |
| ------------------------------------------------------------------------------------------------------ | ------ | ------------------------------------------ |
| Helper `calendarDaySelector` como contrato único para clicar célula de dia | barata | implementada neste PR |
| Guard miss #55 no `e2eNavigationConventions` (varre specs futuros e auto-testa o contrato zero-padded) | barata | implementada neste PR |
| Doc em `docs/TESTING.md` ("Calendar day-cell clicks (miss #55)") | barata | implementada neste PR |
| Page Object/helper de calendário completo | cara | documentada — não implementada neste fluxo |
| Lint AST para locators (hoje o guard é regex por linha) | cara | documentada — não implementada neste fluxo |
| `failOnFlakyTests`/quarentena (hoje `retries: 2` sem `failOnFlakyTests`, `playwright.config.ts:119`) | cara | documentada — não implementada neste fluxo |
| Estabilizar os flaky listados (causas próprias) | cara | documentada — não implementada neste fluxo |
**Estratégia implementada:** os cliques de célula de dia passam a usar um contrato único (`data-day` via helper), um guard de convenção varre todos os specs e falha o build no padrão antigo (e auto-testa o contrato), e a convenção está documentada no `docs/TESTING.md`. A varredura não achou outro spec com o mesmo padrão.
**Estratégia documentada (cara):** Page Object/helper de calendário completo; lint AST para locators; `failOnFlakyTests`/quarentena (`playwright.config.ts:119` roda `retries: 2` sem `failOnFlakyTests`); e estabilizar os flaky listados, que têm causas próprias e ficam como débito separado.
## Lições
- Locator "semântico" por texto pode ser menos estável que um atributo de contrato; `data-day` já existia e não foi usado.
- O C134 removeu o hardcode de data mas manteve o matcher dependente do calendário: remover a data fixa não basta se o locator continua ambíguo.
- O e2e de PR é `selected` (OPS72) e o verify full do deploy é o primeiro a pegar a classe — por isso o vermelho só apareceu no deploy.
- A falha de hoje degradou o CI por ~11h e bloqueou a lane: a segunda detecção (#1406) ocorreu porque a primeira (#1401) foi fechada sem PR de fix (a decisão da primeira sessão é não apurada).

View file

@ -1,7 +1,12 @@
import { formatBahiaCivilDate, parseBahiaDateTimeInput } from '../../src/lib/campaignTime.js'
import { hookFilledCreateData } from '../../src/utilities/hookFilledData.js'
import { campaignPageChrome, expect, test } from './fixtures/campaignE2EFixtures.js'
import { civilDatePlusDays, dayLabelFor, ptBrMonthNames } from './helpers/agendaPeriodLabels.js'
import {
calendarDaySelector,
civilDatePlusDays,
dayLabelFor,
ptBrMonthNames,
} from './helpers/agendaPeriodLabels.js'
/**
* Agenda prologue shared by every journey in this file: opens the agenda
@ -342,17 +347,14 @@ test.describe('Agenda — calendário operacional', () => {
// Multi-day: the end picker lands on a later day, always +1 from today:
// the target stays inside the picker's visible grid (an inside day, or
// day 1 of the next month as an outside day) — no hardcoded dates (C134).
// The button label is date-fns pt-BR "PPPP" ("sexta-feira, 14 de agosto
// de 2026"): day without leading zero, month in lowercase — the string
// name matcher is case-insensitive, the day number is not.
const [endYear, endMonth, endDay] = civilDatePlusDays(
formatBahiaCivilDate(new Date()),
1,
).split('-')
const endDayLabel = `${Number(endDay)} de ${ptBrMonthNames[Number(endMonth) - 1]} de ${endYear}`
// The cell is clicked by its `data-day` contract, not by the accessible
// name: "d de mês de ano" is a substring of "1d…"/"2d… de mês de ano"
// and name matching would resolve to several cells (C104 miss #55).
const endCivilDate = civilDatePlusDays(formatBahiaCivilDate(new Date()), 1)
const [endYear, endMonth, endDay] = endCivilDate.split('-')
await page.getByRole('button', { name: 'Término', exact: true }).click()
const endPicker = page.locator('[data-slot="popover-content"][data-state="open"]').last()
await endPicker.getByRole('button', { name: endDayLabel }).click()
await endPicker.locator(calendarDaySelector(endCivilDate)).click()
await page.keyboard.press('Escape')
await expect(page.getByRole('button', { name: 'Término', exact: true })).toHaveText(
`${endDay}/${endMonth}/${endYear}`,

View file

@ -36,6 +36,18 @@ const ptBrWeekdays = [
export const dayLabelFor = (civilDate: string): string =>
`${Number(civilDate.slice(8, 10))} ${ptBrMonthNames[Number(civilDate.slice(5, 7)) - 1]}`
/**
* "[data-day=\"DD/MM/YYYY\"]" — the Calendar day cell's stable contract
* (`Calendar.tsx` DayButton formats the civil date with the pt-BR locale).
* Click day cells through this selector, never through the accessible name:
* "2 de outubro de 2026" is a substring of "12…"/"22 de outubro de 2026",
* so name matching resolves to several cells (strict mode violation).
*/
export const calendarDaySelector = (civilDate: string): string => {
const [year, month, day] = civilDate.split('-')
return `[data-day="${day}/${month}/${year}"]`
}
export const civilDatePlusDays = (civilDate: string, days: number): string => {
const [year, month, day] = civilDate.split('-').map(Number)
const date = new Date(Date.UTC(year, month - 1, day + days))

View file

@ -2,6 +2,8 @@ import { readdirSync, readFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { calendarDaySelector } from '../e2e/helpers/agendaPeriodLabels.js'
// Miss #54 (2026-07-30): a `page.goto` fired while the previous heavy RSC
// navigation was still in flight aborted with `net::ERR_ABORTED`
// (campaignSavedFilters, prod build). `goto` stays legal for cold loads and
@ -84,3 +86,37 @@ describe('e2e WebAuthn virtual authenticator (miss #53)', () => {
).toEqual([])
})
})
// Miss #55 (2026-10-01): the C104 e2e clicked the calendar's end-day cell with
// `getByRole('button', { name: '2 de Outubro de 2026' })`. The accessible name
// ("sexta-feira, 2 de outubro de 2026") is a longer string and Playwright's
// name matcher is substring + case-insensitive by default, so days 12 and 22
// in the same grid matched too — a strict mode violation that only fired when
// "today + 1" landed on a single-digit day of the month (2026-10-02).
const AMBIGUOUS_DAY_NAME =
/getByRole\('button',\s*\{\s*name:\s*(?:[A-Za-z_]*DayLabel\b|`[^`]*\bde\b[^`]*\$\{|"[^"]*\bde\b[^"]*20\d\d[^"]*"|'[^']*\bde\b[^']*20\d\d[^']*')/
describe('e2e calendar day-cell locator (miss #55)', () => {
it('clicks calendar days through the data-day contract, never the date name', () => {
const offenders: string[] = []
for (const file of specFiles) {
const lines = readFileSync(file, 'utf8').split('\n')
for (const [index, line] of lines.entries()) {
if (AMBIGUOUS_DAY_NAME.test(line)) {
offenders.push(`${relative(process.cwd(), file)}:${index + 1}`)
}
}
}
expect(
offenders,
"click the day cell via calendarDaySelector ('[data-day=\"DD/MM/YYYY\"]') — the accessible name 'd de mês de ano' also matches 1d/2d (miss #55)",
).toEqual([])
})
it('builds the pt-BR data-day selector with a zero-padded day and month', () => {
expect(calendarDaySelector('2026-10-02')).toBe('[data-day="02/10/2026"]')
expect(calendarDaySelector('2026-01-31')).toBe('[data-day="31/01/2026"]')
})
})