From a740f1eb16fb874a4237b48d0c1724be04edcf64 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 11 Aug 2026 13:16:54 -0300 Subject: [PATCH] =?UTF-8?q?fix(e2e):=20first()=20no=20getByLabel('Atividad?= =?UTF-8?q?e=20relacionada')=20sob=20duplica=C3=A7=C3=A3o=20transit=C3=B3r?= =?UTF-8?q?ia=20(flake=20pr=C3=A9-existente,=20precedente=20cd469857)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D12 (#641): strict-mode violation (2 triggers idênticos) só em runs completos sob carga. Estado estável tem 1 match (verificado em browser); o 2º match é duplicação transitória RSC-pending, mesma classe de cd469857. Mesmo alvo em campaignActivity:52 (único outro uso do label em e2e). --- ...2-getbylabel-atividade-relacionada-impl.md | 73 +++++++++++++++++++ tests/e2e/campaignActivity.e2e.spec.ts | 4 +- tests/e2e/campaignRegisterDemand.e2e.spec.ts | 4 +- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 docs/plans/d12-getbylabel-atividade-relacionada-impl.md diff --git a/docs/plans/d12-getbylabel-atividade-relacionada-impl.md b/docs/plans/d12-getbylabel-atividade-relacionada-impl.md new file mode 100644 index 00000000..3d6656d0 --- /dev/null +++ b/docs/plans/d12-getbylabel-atividade-relacionada-impl.md @@ -0,0 +1,73 @@ +# Impl: E2E — flake do `getByLabel('Atividade relacionada')` (strict mode) sob carga + +Status: aprovado +Atualizado em: 2026-08-11 +Issue: #641 (D12) +Intenção: docs/plans/e2e-combobox-label-strict-mode-flake.md +Appetite restante: herdado (P3, kind: defect, sem nova superfície) + +## Leitura da intenção + +- **Outcome:** `campaignRegisterDemand` verde em run completo e isolado; sem mudança de DOM/labels no estado fechado que afete outros specs. +- **O que NÃO negociar:** copy visível e comportamento da combobox intactos; nenhuma regressão de a11y do dialog. +- **O que reavaliar:** a hipótese "o conteúdo do CommandDialog fica sempre montado" da intenção **não se sustenta** — ver caracterização abaixo. + +## Caracterização (completada na sessão, browser real + código) + +1. **Estado estável (fechado): exatamente 1 match.** O trigger (`aria-label="Atividade relacionada: Nenhuma atividade"`) é o único nó. O `CommandDialog` usa `DialogPortal` **sem** `forceMount` (src/components/ui/dialog.tsx): o conteúdo (incl. `CommandInput` com `aria-label={label}`) só monta com `open` ou na animação de saída (~200ms, `duration-200`). +2. **Dialog aberto: 2 matches** — trigger + `CommandInput` (`role="combobox"`, `aria-label="Atividade relacionada"`). Medido ao vivo. +3. **O sr-only `DialogHeader` (título + descrição) fica montado permanentemente** — é filho direto de `Dialog.Root` (render inline, fora da porta). O snapshot pós-falha do D11 ("título + descrição no DOM com o dialog fechado") é o **estado normal da página**, não evidência de dialog montado. +4. O teste que flakava (linha 38) **nunca abre o dialog** — logo o segundo match não pode ser o `CommandInput` por interação do próprio teste. +5. A intenção registra o segundo match como **"ambos botões"** com o mesmo `aria-label` do trigger → duplicação **transitória de instância** (2 comboboxes idênticos no DOM por um instante), a classe já tratada em cd469857 (2026-08-09): "transient RSC-pending duplication … strict-mode flake on loaded machines" → fix estabelecido no repo: `.first()` nos asserts. +6. Navegação dura (`page.goto`) medida ao vivo: 0→1 sem janela de 2 matches na máquina local; a duplicação é dependente de carga (máquina do pool, compiles de 6–27s). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[Flake strict-mode 2 matches] --> B{2º match é...} + B -->|"CommandInput do dialog"| C[Descartado: teste nunca abre o dialog] + B -->|"2º trigger idêntico"| D[Duplicação transitória de instância
classe cd469857] + D --> E["Fix: .first() no assert
precedente do repo"] +``` + +**Opções consideradas:** A (spec `.first()`) | B (spec `getByRole('button', …)`) | C (componente: remover label do `CommandInput` / `aria-hidden` no fechado) + +**Recomendação:** A — `.first()` nos asserts de `getByLabel('Atividade relacionada')` dos dois specs que usam o label (`campaignRegisterDemand.e2e.spec.ts:38` e `campaignActivity.e2e.spec.ts:52`), com comentário citando o precedente cd469857. + +**Rejeitadas:** + +- **B** — `getByRole('button', { name: /Atividade relacionada:/ })` resolve a duplicação trigger+input (input é `role=combobox`), mas **não** resolve o caso reportado ("ambos botões" = duplicação de instância): 2 triggers de mesmo `aria-label` → mesma strict violation. +- **C** — o portal **não está montado no fechado** (medido): remover `aria-label` do `CommandInput` não muda o estado estável e degradaria a a11y do input de busca (o dialog precisa de label próprio); `aria-hidden` no fechado não tem alvo (nada montado). Não atinge a duplicação transitória, que é artefato de render do framework (Next/React), não do componente. +- (descartada na análise) "esperar o dialog abrir/fechar": o teste não interage com o dialog. + +### Componentes / mudanças + +- **`tests/e2e/campaignRegisterDemand.e2e.spec.ts:38`**: `page.getByLabel(CAMPAIGN_DEMAND_ACTIVITY_LABEL)` → `.first()`. Semântica preservada: o teste quer provar que o campo está presente/visível; qualquer uma das instâncias transitórias é o mesmo trigger. +- **`tests/e2e/campaignActivity.e2e.spec.ts:52`**: idem (mesmo label, mesma classe de flake — página da atividade também monta `AsyncSearchCombobox` via `DemandFields`). +- **Migration:** sem migration. **Access/Consent:** não toca. **UI/componente:** nenhuma mudança (aceite "sem mudança de DOM/labels"). +- Comentário nos dois asserts no estilo cd469857 (classe + motivo, não reescrever o histórico). + +## Fases verificáveis + +1. **Baseline (reprodução)** — run completo do e2e em dev (em andamento, máquina com load real 11–22): registrar se o flake reproduz localmente. O D11 já provou pré-existência em main via stash (3/4 runs falharam no pool); a reprodução local é bônus, não gate. +2. **Fix** — `.first()` nos 2 asserts. +3. **Gates** — `campaignRegisterDemand` isolado (dev) + `campaignActivity` isolado; run completo do e2e verde; `pnpm gate:fast` (tsc, lint, format, unit+int) e `pnpm check:cycles`; knip. + +## Rabbit holes / Não escopo (engenharia) + +- Não investigar o mecanismo exato da duplicação transitória do Next/React (bug upstream, dependente de carga; o repo já decidiu a resposta pragmática em cd469857). +- Não mexer no `AsyncSearchCombobox`/`CommandDialog` (a11y do dialog é correta; portal não monta no fechado). +- Não trocar `getByLabel` por `getByRole` nos outros ~100 usos do arquivo (fora do escopo do label em questão). + +## Riscos e mitigação + +- **Flake não reproduz na máquina local** (condições do pool diferentes) → mitigação: validação = mecanismo caracterizado ao vivo (estado estável 1 match; 2 matches só em instância duplicada/open) + fix idêntico ao precedente aprovado cd469857 + runs completos verdes pós-fix (1+). +- **`.first()` mascarar regressão real** (ex.: 2 instâncias ESTÁVEIS) → mitigação: assert continua verificando visibilidade do campo; se um dia houver duplicação estável, os outros asserts do spec (contagem 0/1 em linhas vizinhas) pegam. +- **Aceite "grep getByLabel em e2e"** → verificado: os únicos usos do label `Atividade relacionada` são os 2 pontos alterados; nenhum outro spec depende do estado fechado da combobox. + +## Aceite de engenharia + +- [ ] Aceite da intenção coberto (spec verde isolado e em run completo; DOM/labels fechados intactos) +- [ ] Invariantes AGENTS/engineering-standards (sem migration/access/transação; identificadores em inglês; precedente cd469857) +- [ ] Testes: run completo e2e dev verde + specs alterados isolados; gates completos no fechamento diff --git a/tests/e2e/campaignActivity.e2e.spec.ts b/tests/e2e/campaignActivity.e2e.spec.ts index cdc88810..04e7c146 100644 --- a/tests/e2e/campaignActivity.e2e.spec.ts +++ b/tests/e2e/campaignActivity.e2e.spec.ts @@ -49,7 +49,9 @@ test.describe('Atividades — registro-fundação', () => { await page.getByRole('link', { name: 'Adicionar demanda' }).click() await expect(page.locator('input[name="activityId"]')).toHaveValue(/\d+/) - await expect(page.getByLabel('Atividade relacionada')).toContainText(activityTitle) + // `.first()`: transient RSC-pending duplication also copies the combobox + // trigger (same strict-mode flake as cd469857; observed on loaded machines). + await expect(page.getByLabel('Atividade relacionada').first()).toContainText(activityTitle) await expect(page.getByLabel('Município')).toHaveValue(String(municipality.id)) await page.goto(`${campaign.baseURL}/campanha/municipios/${municipality.slug}?tab=updates`) diff --git a/tests/e2e/campaignRegisterDemand.e2e.spec.ts b/tests/e2e/campaignRegisterDemand.e2e.spec.ts index 3d9b4407..4e0551f7 100644 --- a/tests/e2e/campaignRegisterDemand.e2e.spec.ts +++ b/tests/e2e/campaignRegisterDemand.e2e.spec.ts @@ -35,7 +35,9 @@ test.describe('Wizard "Registrar pedido" (A5/B195)', () => { // free-text field comes straight after Tipo and Atividade. await expect(page.getByRole('heading', { name: 'Registrar pedido' })).toHaveCount(0) await expect(page.getByLabel(CAMPAIGN_DEMAND_KIND_LABEL)).toBeVisible() - await expect(page.getByLabel(CAMPAIGN_DEMAND_ACTIVITY_LABEL)).toBeVisible() + // `.first()`: transient RSC-pending duplication also copies the combobox + // trigger (same strict-mode flake as cd469857; observed on loaded machines). + await expect(page.getByLabel(CAMPAIGN_DEMAND_ACTIVITY_LABEL).first()).toBeVisible() await expect(page.getByLabel(CAMPAIGN_DEMAND_BODY_LABEL)).toBeVisible() await expect(page.locator('input[name="title"]')).toHaveCount(0) await expect(page.locator('select[name="municipalityId"]')).toHaveCount(0)