From 19c14ef90c5405bea124df82fb96f82a0835484a Mon Sep 17 00:00:00 2001 From: fsolla Date: Thu, 1 Oct 2026 16:33:33 +0000 Subject: [PATCH] =?UTF-8?q?fix(e2e):=20clique=20no=20dia=20do=20calend?= =?UTF-8?q?=C3=A1rio=20pelo=20contrato=20data-day=20=E2=80=94=20desbloquei?= =?UTF-8?q?a=20o=20verify=20(C104,=20miss=20#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/TESTING.md | 2 + ...026-10-01-e2e-c104-calendar-day-locator.md | 1 + ...026-10-01-e2e-c104-calendar-day-locator.md | 89 +++++++++++++++++++ tests/e2e/campaignActivity.e2e.spec.ts | 22 ++--- tests/e2e/helpers/agendaPeriodLabels.ts | 12 +++ .../e2eNavigationConventions.unit.spec.ts | 36 ++++++++ 6 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 docs/changelog/2026-10-01-e2e-c104-calendar-day-locator.md create mode 100644 docs/postmortems/2026-10-01-e2e-c104-calendar-day-locator.md diff --git a/docs/TESTING.md b/docs/TESTING.md index cacd835d..c7254dba 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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`) diff --git a/docs/changelog/2026-10-01-e2e-c104-calendar-day-locator.md b/docs/changelog/2026-10-01-e2e-c104-calendar-day-locator.md new file mode 100644 index 00000000..41bd052f --- /dev/null +++ b/docs/changelog/2026-10-01-e2e-c104-calendar-day-locator.md @@ -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). diff --git a/docs/postmortems/2026-10-01-e2e-c104-calendar-day-locator.md b/docs/postmortems/2026-10-01-e2e-c104-calendar-day-locator.md new file mode 100644 index 00000000..cd57820f --- /dev/null +++ b/docs/postmortems/2026-10-01-e2e-c104-calendar-day-locator.md @@ -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). diff --git a/tests/e2e/campaignActivity.e2e.spec.ts b/tests/e2e/campaignActivity.e2e.spec.ts index 969d338e..db915efc 100644 --- a/tests/e2e/campaignActivity.e2e.spec.ts +++ b/tests/e2e/campaignActivity.e2e.spec.ts @@ -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}`, diff --git a/tests/e2e/helpers/agendaPeriodLabels.ts b/tests/e2e/helpers/agendaPeriodLabels.ts index d8c613ab..7257e521 100644 --- a/tests/e2e/helpers/agendaPeriodLabels.ts +++ b/tests/e2e/helpers/agendaPeriodLabels.ts @@ -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)) diff --git a/tests/unit/e2eNavigationConventions.unit.spec.ts b/tests/unit/e2eNavigationConventions.unit.spec.ts index 07182184..d6c5e700 100644 --- a/tests/unit/e2eNavigationConventions.unit.spec.ts +++ b/tests/unit/e2eNavigationConventions.unit.spec.ts @@ -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"]') + }) +})