diff --git a/docs/plans/c134-e2e-agenda-dia-hardcoded-impl.md b/docs/plans/c134-e2e-agenda-dia-hardcoded-impl.md new file mode 100644 index 00000000..f8b4935a --- /dev/null +++ b/docs/plans/c134-e2e-agenda-dia-hardcoded-impl.md @@ -0,0 +1,101 @@ +# Impl: C134 — e2e agenda: dia hardcoded "17 de agosto de 2026" no teste C104 expira + +Status: aprovado +Atualizado em: 2026-08-13 +Issue: #712 +Intenção: body da Issue (sem plano de intenção linkado — body é a spec) +Appetite restante: herdado (correção de teste, escopo mínimo) + +## Leitura da intenção + +- **Outcome:** o teste e2e "cria compromisso 'Todo o dia'" (`campaignActivity.e2e.spec.ts`) + não quebra mais deterministicamente ~2026-09: o dia do picker de término é derivado + de "hoje" (`formatBahiaCivilDate(new Date()) + delta`), no mesmo padrão do resto do spec. +- **O que NÃO negociar:** nada de produto; mudança restrita ao spec de e2e. +- **O que reavaliar:** o delta. A intenção pede "+ delta (mesmo padrão do resto do spec)". + Hoje o teste não usa `civilDatePlusDays` no C104; o helper existe em + `tests/e2e/helpers/agendaPeriodLabels.ts` (usado pelo C95/mobile). A questão real é: + o delta precisa manter o dia **dentro do mesmo mês** (comentário atual: "a later day of + the same month") ou basta ser **posterior a hoje**? + +## Abordagem recomendada + +**Opções consideradas:** + +- **A — delta fixo +1 derivado de hoje (recomendada):** `endCivil = civilDatePlusDays(formatBahiaCivilDate(new Date()), 1)`, + label do botão montado de `ptBrMonthNames` + partes do civil date. Sem navegação de mês no picker. +- **B — delta +1 com navegação de mês condicional:** se `endCivil` cai em mês diferente, + clicar o botão de próximo mês antes do dia. Mais passos, cobre o caso em que o dia alvo + não está no grid — mas o grid do react-day-picker (`showOutsideDays`) **sempre** renderiza + o dia 1 do mês seguinte como célula "outside" clicável (seleciona e navega), então B é + complexidade sem cobertura adicional. +- **C — manter hardcode mas avançar a data:** adia a quebra; não resolve o problema de classe. + +**Recomendação:** A. O picker é react-day-picker v9 (`Calendar` em `ActivityDateTimeField`, +`defaultMonth` = mês de hoje; `showOutsideDays=true` renderiza o primeiro dia do mês +seguinte como botão clicável no próprio grid — verificado no source do node_modules: +`CalendarDay.outside` só marca `data-outside`, não desabilita). Com delta +1 o alvo está +**sempre** no grid visível: se hoje não é o último dia do mês, é um dia do próprio mês; se é, +vira o dia 1 do mês seguinte na primeira posição da última semana — ainda visível e clicável. +O label do botão é date-fns `PPPP` com pt-BR ("segunda-feira, 17 de agosto de 2026") — o +regex não-ancorado `/17 de agosto de 2026/` do teste casa contra o subconjunto +"dia de mês de ano"; derivamos exatamente esse subconjunto (dia sem zero à esquerda, +nome do mês capitalizado de `ptBrMonthNames`, ano). A asserção do trigger +(`toHaveText(/17\/08\/2026/)`) vira `dd/mm/aaaa` derivado do mesmo `endCivil`. + +**Rejeitadas:** B (navegação extra sem ganho: o dia alvo sempre está no grid), C (adieta). + +### Componentes / mudanças + +- **`campaignActivity.e2e.spec.ts`** (teste C104, ~linhas 308–315): derivar + `endCivil = civilDatePlusDays(formatBahiaCivilDate(new Date()), 1)`; clicar + `endPicker.getByRole('button', { name: new RegExp(pickerDayLabel) })` com o label + montado de `ptBrMonthNames`; asserir o trigger com o `dd/mm/aaaa` derivado. + Atualizar o comentário "same month" → "later day" (pode cruzar mês no último dia). +- **Helpers:** nenhum helper novo — `civilDatePlusDays` e `ptBrMonthNames` já existem e + estão importados/exportados; a montagem do label é 3 linhas locais no spec (mesmo + padrão de `dayLabelFor` do helper — não criar helper novo para 1 uso). +- **Migration:** sem migration. +- **Access / Consent:** sem mudança. +- **UI:** sem mudança. + +## Fases verificáveis + +1. **Spec** — editar o teste C104 no `campaignActivity.e2e.spec.ts`. +2. **Verificação** — `pnpm test:e2e campaignActivity` (e2e com db de teste do worktree). +3. **Gates** — `tsc --noEmit`, `lint`, `format:check`, `knip` (nenhum símbolo orfanado). + +## Rabbit holes / Não escopo (engenharia) + +- Não tocar nos outros hardcodes de data de e2e (`campaignAiChatFollowUps` são strings + de conteúdo, não datas de calendário — verificados, sem trabalho). +- Não generalizar para um helper de "label de dia do picker": 1 uso não justifica + camada nova (`civilDatePlusDays` + `ptBrMonthNames` já cobrem). + +## Riscos e mitigação + +- **Fuso:** `formatBahiaCivilDate` usa hora civil da Bahia (padrão do spec inteiro); + `civilDatePlusDays` opera em UTC puro sobre o civil date — imune a DST. Mesmo + contrato do resto do spec. +- **Último dia do mês:** o dia alvo vira "outside day" — o react-day-picker v9 renderiza + e seleciona outside days normalmente (verificado no source; o unit spec de + `activityOverlay` já clica dias do mês corrente sem restrição de "inside"). + Risco residual baixíssimo; se um dia o RDP mudar esse contrato, o teste falha com + "button não encontrado" — mensagem clara, não determinística de data. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (teste deriva o dia de hoje + delta) +- [x] Invariantes AGENTS/engineering-standards (mudança só em spec; sem DB, sem schema) +- [x] Testes de domínio previstos (e2e C104 roda no worktree; sem unit — sem lógica de runtime) + +## Absorvido durante a execução (débito pré-existente, mesmo arquivo) + +- **Flake `?tab=overview` (3 sítios):** `toHaveURL(/\/campanha\/atividades\/[^/?]+$/)` nas + linhas 86/344/397 corria contra o redirect canônico da página de detalhe + (`getActivityDetailTabRedirect` → sempre `?tab=overview` quando o query está vazio): + só passava quando o poll pegava o URL intermediário pré-redirect — quebrou 5× nos runs + de verificação, inclusive no próprio C104 (linha 344). O precedente da correção já + existia na linha 219 do mesmo arquivo (`(?:\?tab=overview)?$`); apliquei o mesmo + regex nos 3 sítios restantes. Débito absorvido (determinismo, mesma classe do C134; + sem nova Issue). diff --git a/tests/e2e/campaignActivity.e2e.spec.ts b/tests/e2e/campaignActivity.e2e.spec.ts index dfb986f4..5d9149db 100644 --- a/tests/e2e/campaignActivity.e2e.spec.ts +++ b/tests/e2e/campaignActivity.e2e.spec.ts @@ -1,7 +1,7 @@ import { formatBahiaCivilDate, parseBahiaDateTimeInput } from '../../src/lib/campaignTime.js' import { hookFilledCreateData } from '../../src/utilities/hookFilledData.js' import { campaignPageChrome, expect, test } from './fixtures/campaignE2EFixtures.js' -import { dayLabelFor, ptBrMonthNames } from './helpers/agendaPeriodLabels.js' +import { civilDatePlusDays, dayLabelFor, ptBrMonthNames } from './helpers/agendaPeriodLabels.js' /** * Agenda prologue shared by every journey in this file: opens the agenda @@ -83,7 +83,9 @@ test.describe('Atividades — registro-fundação', () => { await expect(editModal).toBeVisible() await expect(editModal.getByLabel('Título *')).toBeVisible({ timeout: 30_000 }) await editModal.getByRole('link', { name: 'Ver detalhes' }).click() - await expect(page).toHaveURL(/\/campanha\/atividades\/[^/?]+$/, { timeout: 30_000 }) + await expect(page).toHaveURL(/\/campanha\/atividades\/[^/?]+(?:\?tab=overview)?$/, { + timeout: 30_000, + }) await expect(campaignPageChrome(page, activityTitle)).toBeVisible({ timeout: 15_000, }) @@ -305,13 +307,23 @@ test.describe('Agenda — calendário operacional', () => { ).toHaveCount(0) await page.keyboard.press('Escape') - // Multi-day: the end picker lands on a later day of the same month. + // 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}` 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: /17 de agosto de 2026/ }).click() + await endPicker.getByRole('button', { name: endDayLabel }).click() await page.keyboard.press('Escape') await expect(page.getByRole('button', { name: 'Término', exact: true })).toHaveText( - /17\/08\/2026/, + new RegExp(`${endDay}/${endMonth}/${endYear}`), ) await page.getByLabel('Título *').fill(title) @@ -331,7 +343,9 @@ test.describe('Agenda — calendário operacional', () => { await expect(editModal).toBeVisible() await expect(editModal.getByLabel('Título *')).toBeVisible({ timeout: 30_000 }) await editModal.getByRole('link', { name: 'Ver detalhes' }).click() - await expect(page).toHaveURL(/\/campanha\/atividades\/[^/?]+$/, { timeout: 30_000 }) + await expect(page).toHaveURL(/\/campanha\/atividades\/[^/?]+(?:\?tab=overview)?$/, { + timeout: 30_000, + }) await expect(page.getByText(/^\d{2}\/\d{2}\/\d{4}$/).first()).toBeVisible() await expect(page.getByText(/\d{2}\/\d{2}\/\d{4} às /)).toHaveCount(0) }) @@ -384,7 +398,9 @@ test.describe('Agenda — calendário operacional', () => { const detailModal = page.getByRole('dialog', { name: 'Editar atividade' }) await expect(detailModal.getByLabel('Título *')).toBeVisible({ timeout: 30_000 }) await detailModal.getByRole('link', { name: 'Ver detalhes' }).click() - await expect(page).toHaveURL(/\/campanha\/atividades\/[^/?]+$/, { timeout: 30_000 }) + await expect(page).toHaveURL(/\/campanha\/atividades\/[^/?]+(?:\?tab=overview)?$/, { + timeout: 30_000, + }) // The location shows in the overview's own
(exact); the chrome // subtitle carries "município · local" as one string. await expect(page.getByText(newLocality, { exact: true })).toBeVisible()