teqo-backup/docs/TESTING.md
fsolla 90768be1cf 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
2026-10-01 14:14:37 -03:00

58 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Testing
## Test Strategy
Three layers, all local-database-only (see `local-database` skill; tests refuse any database not ending in `_test`):
- **Unit** (`tests/unit/**/*.unit.spec.ts`, 117 specs as of 2026-07-31, Pass 4 — +6 convention guards land with the Pass 4 remediation PRs): pure logic; runs with a deliberately invalid `DATABASE_URL` so nothing can touch a database.
- **Integration** (`tests/int/**/*.int.spec.ts`, 71 specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Since the miss #73 guardrail (Pass 4), specs that mutate municipalities must use the allocator (no pinned slugs) — enforced by `testMunicipalityAllocatorConventions`. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned.
- **E2E** (`tests/e2e`, Playwright, 17 specs / 51 cases (Pass 5, 2026-08-03; was 48 at Pass 4)): smoke-level; most campaign behavior is pinned at the int layer.
**Level preference (pyramid):** unit first (pure logic) → integration for Payload/DB boundaries → e2e only when the flow has real benefit the lower layers don't cover (critical user path, UX/navigation, URL contract). Canonical definition: `test-driven-development` skill.
CI (`ci-pr.yml`) is a **single sequential job `checks`** (OPS62 X1): guards → lint (zero warnings) → format → typecheck → knip → cycles → unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths) → build → e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; **only in mode `selected` — OPS72: the PR never runs full e2e**; production build + `list`/`github` reporters in CI) — against ONE Postgres 17 service, with a deterministic **`pnpm db:reset`** (drop schema → migrate → seed:minimal) between the int phase and the build/e2e phase (OPS88). Fail-fast is structural: a failed step stops the run; e2e runs with `PLAYWRIGHT_WORKERS=4`. There is no main verifier (`ci.yml` eliminated in OPS71): the full net including e2e runs in the deploy `verify` job before publishing — the deploy run now starts on merge to `main` (OPS104), and production still waits for the `production` environment approval.
**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`)
The integration suite and any agent/Cloud environment boot from a **minimal synthetic database**: schema from `pnpm migrate` (which seeds the 435-município catalog) plus `pnpm db:seed:minimal` ([`scripts/seed-minimal.mjs`](../scripts/seed-minimal.mjs)), an idempotent upsert of the rows the app refuses to run without:
- the four fail-closed `Consent` keys (`lideranca-autopreenchimento`, `apoiador-cadastro`, `apoiador-intencao-voto`, `whatsapp-inscricao`);
- one `campaignUser` per role (synthetic `@teqo.invalid` emails / `719999999Xx` phones);
- deterministic strategy pins (`priority` + `expectedVotes`) on 5 municípios (`salvador-ze-1`, `camacari`, `feira-de-santana`, `vitoria-da-conquista`, `itabuna`) — rows themselves come from the migration-seeded catalog;
- 1 `organization`, 1 `stateDeputy`, 2 `leadership`, 3 `supporter` (all synthetic);
- the `campaignGoals` global default.
The content contract is data in [`scripts/lib/seed-minimal-manifest.mjs`](../scripts/lib/seed-minimal-manifest.mjs), pinned by `tests/unit/seedMinimalManifest.unit.spec.ts`. **Contract: a delivery that adds a migration, collection, Consent key or required boot data must update the manifest in the same PR** — PR CI runs migrate → seed → int, so a broken seed fails the gate. The seed refuses non-local `DATABASE_URL` (same `assertLocalDatabase` guard family as the other seeds); agents and Cloud environments never set `ALLOW_REMOTE_DB`.
## Safety Net Map
Modules the 2026-07 hardening phases touch, and where their behavior is pinned:
| Module | Pinned behaviors | Test files | Gaps |
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| RBAC — `src/utilities/access/*` (split 2026-07-23; `campaignAccess.ts` is the re-export surface) | per-role municipality/leadership/pledge/supporter/demand/plan scoping; leader lockdown; staff field asymmetry | `campaignMunicipalityAccess`, `campaignVotePledge`, `campaignSupporter`, `campaignUserPhone`, `campaignActionPlan`, `campaignDemandWorkflow`, `collectionAccessLockdown` (int) | no dedicated per-export suite; covered through collection behavior |
| Collection access lockdown | campaign JWT denied on `users`/PII/CMS writes; anonymous read of public content; admin unaffected | `collectionAccessLockdown.int.spec.ts` | REST-transport variant untested (Local API with `overrideAccess: false` is equivalent) |
| `municipalityMapData.ts` | bundle scoping by URL filters + advisor access; `validVotesByYear` incl. 2026 reuse of 2022; null on empty scope; `hasZoneMunicipalities` + per-zone map keys | `municipalityMapData.int.spec.ts` | |
| `votePledgeData.ts` | `estimated[scenario] ?? declared` aggregation; role scoping; leader denial | `campaignVotePledge.int.spec.ts` | |
| Supporter import (token/bulk) | preview→confirm flow, dedup by phone, consent fail-closed | `campaignSupporter.int.spec.ts` | `supporterImportBulk`/`supporterImportToken` internals only via flow |
| Leader supporter create | linked-municipality scope; out-of-scope rejection; staff rejection | `collectionAccessLockdown.int.spec.ts` | |
| Posts visibility/caching | `isPostVisible` fail-closed; cached listing | (indirect via routes) | no dedicated `posts` spec — backlog |
| List URL contracts (Pass 2 W0, pinned 2026-07-25) | `campaignListUrl` canonicalize/clamp/redirect; supporter + actionPlan parse/serialize/href; 5 entity parsers | `campaignListUrl`, `supporterUi`, `actionPlanUi`, `campaignEntityListParsers` (unit) | municipality list URL pinned separately in `municipalityUi.unit.spec.ts` |
| Form-action ladders (Pass 2 W4d) | `runCampaignFormAction`: success spread, boundary/zod/safe/generic mapping, values+revision echo | `campaignFormActions.unit.spec.ts` | per-route ladders are thin shells; behavior pinned at the wrapper |
| WhatsApp subscription consent (Pass 2 D3) | fail-closed without `whatsapp-inscricao`; happy path links subscription → keyed consent | `submitWhatsapp.int.spec.ts` | |
| Vote trend + election number format (Pass 2 W4a survivors) | `computeVoteTrend` pair priority/±10% band; `formatElectionNumber` E18 fraction defect | `voteTrend.unit.spec.ts` | |
| Entity list loaders (Pass 2 W0) | scope (leader empty/denied), marker-narrowed q, kind/status filters, pagination shape | `campaignEntityListData.int.spec.ts` | advisor loader runs post-gate with `overrideAccess: true` (gate asserted upstream) |
| Codebase conventions (tooling guards, 2026-07-25) | `server-only` on Payload-coupled utilities; Praça/Núcleo copy ban; eslint legacy-ignore existence; formActions wrapper usage | `codebaseConventions.unit.spec.ts` | boundary rules themselves live in ESLint (`no-restricted-imports`) + madge in CI |
## Characterization Backlog
- [ ] `src/utilities/posts.ts` — dedicated spec for `isPostVisible` fail-closed matrix and cache wrappers (confirmed 2026-07-28, Pass 3 audit).
- [ ] `activityViewModels.ts` (ex-`actionPlanViewModels`, renamed by C13) — `activityUi.ts` pinned 2026-07-25 (Pass 2 W0); view models still untested directly (confirmed 2026-07-28, Pass 3 audit).
- [x] `campaignDemandData.ts` / `leadershipData.ts` / `organizationData.ts` / `stateDeputyData.ts` list loaders — smoked 2026-07-25 (`campaignEntityListData.int.spec.ts`, Pass 2 W0).
- [x] Election aggregate artifact (Phase 2): snapshot test pinning artifact ↔ `municipalityCatalog` key consistency — delivered as `bahiaElectionAggregates.unit.spec.ts`.