From 355e72c35d2572cbdc330b4e396875c771d8633e Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 26 Sep 2026 05:21:19 -0300 Subject: [PATCH] =?UTF-8?q?test(e2e):=20escopa=20locators=20do=20card=20de?= =?UTF-8?q?=20respons=C3=A1veis=20ao=20form=20vis=C3=ADvel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Falha pré-existente reproduzida em origin/main: a página de demanda monta o form de responsáveis duas vezes no DOM (visível + cópia oculta) e campaignDemandVisibility batia nos dois com locator estrito — classe "locator não-escopado" do plano OPS83/#824. O CI curado do PR roda a spec (diff high-risk) e ficaria vermelho; os locators agora miram o elemento visível. Registro no impl plan do C231 e no changelog. --- docs/changelog/2026-09-26-c231.md | 2 +- .../acervo-fotos-flickr-ingestao-impl.md | 1 + .../e2e/campaignDemandVisibility.e2e.spec.ts | 35 ++++++++++++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/changelog/2026-09-26-c231.md b/docs/changelog/2026-09-26-c231.md index 3445d245..d021f048 100644 --- a/docs/changelog/2026-09-26-c231.md +++ b/docs/changelog/2026-09-26-c231.md @@ -1 +1 @@ -**C231 — acervo de fotos do Flickr: ingestão completa para o storage privado (2026-09-26):** as 6.577 fotos da conta própria `depjorgesolla` passam a ter casa no Teqo — collection privada `archivePhoto` (upload, grupo Comunicação) com `flickrId` unique+index como identidade e metadados do Flickr estruturados (`title`/`description`/`tags`/`takenAt`/`postedAt`/`albums`/`geo` + `exif` json, `sourceUrl`/`owner`/`license` de proveniência; `alt` derivado), leitura pelos papéis de comunicação e mutação só do admin do Payload, com entrada no mapa S3 do plugin (sem ela o original cairia em disco efêmero em produção). A migration `add_archive_photo` é aditiva e o snapshot que a acompanha **repara a base da cadeia**: o `.json` anterior (`20260924_123940…`) estava dessincronizado e a migration gerada re-criava objetos já existentes (`internet_speech_media`, `recording_rels/topics/scopes`, colunas de `speech`/`content_piece`/`recording`); a migration foi aparada para só o DDL do acervo e o snapshot completo (106 tabelas) volta a ser o base das próximas `migrate:create`. A ingestão é idempotente por `flickrId` (`src/utilities/flickr/archivePhotoIngest.ts`: re-checa, grava linha+upload numa transação e devolve `created`/`existing`/`failed` com motivo) e o CLI é `pnpm flickr:import`: **plano/dry-run por padrão**, `--apply` (exige `FLICKR_IMPORT_CONFIRM=1` fora de dev local, `TEQO_ENV` casando o nome exato do banco e as 4 `S3_*`), `--verify` (inventário read-only: contagem, bytes, cobertura por álbum, metadados e ficheiros faltantes), `--limit`/`--page`/`--out`, recibos JSON datados em `data/flickr/reports/`. O client (`scripts/lib/flickrApi.mjs`) usa `fetch` nativo com `User-Agent`, pacing ~1 req/s, retry 429/5xx, `stat=fail` nomeado, EXIF tolerante e fallback para o maior tamanho quando não há `url_o`; o plano/recibo puro (`scripts/lib/flickrPlan.mjs`) conta vídeos pulados e itens sem id, resolve o mapa foto→álbuns antes de qualquer escrita e classifica novas/já existiam/fallback/inválidas; `downloadUrlToFile` ganha `headers` (UA do Flickr); e o guard de alvo dos imports saiu de `reel-ingest.mjs` para `scripts/lib/cli.mjs` (`assertEnvironmentDatabaseTarget` + `TEQO_ENV_DATABASE_BY_ENV`, com o C195 delegando sem mudar contrato). Envs `FLICKR_API_KEY`/`FLICKR_USER_ID` documentadas no `.env.example` (só env, nunca repo) e runbook do homeserver com staging-primeiro. Testes: unit novos de mapeamento/geo (`archivePhoto.unit`), client Flickr, plano/parser (`flickrPlan.unit`) e guardas em subprocesso (`flickrImportCli.unit`), int `archivePhotoIngest` (criação+objeto, convergência sem duplicar, falha honesta sem linha, access por papel e mutação admin); `pnpm gate:fast` verde (unit 431 files) e int full 105 files. Canary/backfill ficam para quando a chave da API for provisionada (Q1). Impl plan: `docs/plans/acervo-fotos-flickr-ingestao-impl.md`. +**C231 — acervo de fotos do Flickr: ingestão completa para o storage privado (2026-09-26):** as 6.577 fotos da conta própria `depjorgesolla` passam a ter casa no Teqo — collection privada `archivePhoto` (upload, grupo Comunicação) com `flickrId` unique+index como identidade e metadados do Flickr estruturados (`title`/`description`/`tags`/`takenAt`/`postedAt`/`albums`/`geo` + `exif` json, `sourceUrl`/`owner`/`license` de proveniência; `alt` derivado), leitura pelos papéis de comunicação e mutação só do admin do Payload, com entrada no mapa S3 do plugin (sem ela o original cairia em disco efêmero em produção). A migration `add_archive_photo` é aditiva e o snapshot que a acompanha **repara a base da cadeia**: o `.json` anterior (`20260924_123940…`) estava dessincronizado e a migration gerada re-criava objetos já existentes (`internet_speech_media`, `recording_rels/topics/scopes`, colunas de `speech`/`content_piece`/`recording`); a migration foi aparada para só o DDL do acervo e o snapshot completo (106 tabelas) volta a ser o base das próximas `migrate:create`. A ingestão é idempotente por `flickrId` (`src/utilities/flickr/archivePhotoIngest.ts`: re-checa, grava linha+upload numa transação e devolve `created`/`existing`/`failed` com motivo) e o CLI é `pnpm flickr:import`: **plano/dry-run por padrão**, `--apply` (exige `FLICKR_IMPORT_CONFIRM=1` fora de dev local, `TEQO_ENV` casando o nome exato do banco e as 4 `S3_*`), `--verify` (inventário read-only: contagem, bytes, cobertura por álbum, metadados e ficheiros faltantes), `--limit`/`--page`/`--out`, recibos JSON datados em `data/flickr/reports/`. O client (`scripts/lib/flickrApi.mjs`) usa `fetch` nativo com `User-Agent`, pacing ~1 req/s, retry 429/5xx, `stat=fail` nomeado, EXIF tolerante e fallback para o maior tamanho quando não há `url_o`; o plano/recibo puro (`scripts/lib/flickrPlan.mjs`) conta vídeos pulados e itens sem id, resolve o mapa foto→álbuns antes de qualquer escrita e classifica novas/já existiam/fallback/inválidas; `downloadUrlToFile` ganha `headers` (UA do Flickr); e o guard de alvo dos imports saiu de `reel-ingest.mjs` para `scripts/lib/cli.mjs` (`assertEnvironmentDatabaseTarget` + `TEQO_ENV_DATABASE_BY_ENV`, com o C195 delegando sem mudar contrato). Envs `FLICKR_API_KEY`/`FLICKR_USER_ID` documentadas no `.env.example` (só env, nunca repo) e runbook do homeserver com staging-primeiro. Testes: unit novos de mapeamento/geo (`archivePhoto.unit`), client Flickr, plano/parser (`flickrPlan.unit`) e guardas em subprocesso (`flickrImportCli.unit`), int `archivePhotoIngest` (criação+objeto, convergência sem duplicar, falha honesta sem linha, access por papel e mutação admin); `pnpm gate:fast` verde (unit 431 files), int full 105 files, e2e curado do PR 95/95 no espelho local (modo CI: build `NEXT_PUBLIC_SITE_URL=https://feed.e2e.teqo.test`) — no caminho, uma falha **pré-existente** reproduzida em `origin/main` foi destravada: `campaignDemandVisibility` batia com locator estrito nos dois mounts do form de responsáveis (visível + cópia oculta), classe "locator não-escopado" do plano OPS83/#824; os locators agora miram o elemento visível. Canary/backfill ficam para quando a chave da API for provisionada (Q1). Impl plan: `docs/plans/acervo-fotos-flickr-ingestao-impl.md`. diff --git a/docs/plans/acervo-fotos-flickr-ingestao-impl.md b/docs/plans/acervo-fotos-flickr-ingestao-impl.md index daaeabc7..0a8d5327 100644 --- a/docs/plans/acervo-fotos-flickr-ingestao-impl.md +++ b/docs/plans/acervo-fotos-flickr-ingestao-impl.md @@ -169,6 +169,7 @@ N/A — operação/ingestão; o recibo é arquivo JSON operacional (conferência - **Factory dos predicados de leitura da Central (S1).** `canReadArchivePhoto` segue o padrão intencional de predicado próprio por superfície (o comentário "never an alias" em `contentPieces.ts`; cada vertical pode ampliar seu gate sem conceder escrita alheia). Reabrir só se uma ampliação de read precisar valer para todas as verticais de uma vez. - **Tipagem do bag do recibo (S3)** e **`--out` absoluto (S4).** Doc-only/paridade com o irmão `import-web-speeches.mjs`; tipar se o formatter for tocado de novo; o recibo é arquivo operacional do operador. +- **Duplicata oculta no DOM do card de responsáveis (OPS83/#824).** O e2e curado destravou uma falha pré-existente (reproduzida em `origin/main`): a página de demanda monta o form de responsáveis duas vezes (visível + cópia oculta) e `campaignDemandVisibility` batia nos dois com locator estrito. Esta entrega escopa os locators ao elemento visível (classe "locator não-escopado" do plano OPS83/#824, que segue dono da causa-raiz); sem Issue nova. - **Drift pré-existente `activity.tags`.** Coluna sobra no banco (a config não a declara; o snapshot de base também não) e é invisível ao diff — fora do escopo. O reparo do snapshot desta entrega (106 tabelas como novo base) elimina a re-geração de DDL alheia nas próximas `migrate:create`. ## Aceite de engenharia diff --git a/tests/e2e/campaignDemandVisibility.e2e.spec.ts b/tests/e2e/campaignDemandVisibility.e2e.spec.ts index 6d836461..e6c9c6c7 100644 --- a/tests/e2e/campaignDemandVisibility.e2e.spec.ts +++ b/tests/e2e/campaignDemandVisibility.e2e.spec.ts @@ -51,7 +51,10 @@ test.describe('C143 — demand visibility (explicit responsibles)', () => { // The list rendered for him: the empty state proves the page settled // (and that the demand is absent — the link assertion below is the // fail-closed complement). - await expect(page.getByText('Nenhuma demanda por aqui')).toBeVisible() + // The list mounts the desktop table and the mobile cards together (one + // hidden) — assert the rendered empty state, not every DOM copy + // (OPS83/#824, same class). + await expect(page.getByText('Nenhuma demanda por aqui').filter({ visible: true })).toBeVisible() await expect(page.getByRole('link', { name: demand.title })).toHaveCount(0) // The URL does not open either — the demand simply does not exist for him @@ -69,20 +72,32 @@ test.describe('C143 — demand visibility (explicit responsibles)', () => { const { peer, responsible, demand } = await setupDemand(campaign, fixtures) const demandURL = `${campaign.baseURL}/campanha/demandas/${demand.slug}` + // Pre-existing duplication (OPS83/#824 — the "locator não-escopado" class): + // the detail page mounts the responsibles form twice in the DOM — the + // visible one plus a hidden copy with the same markup. Scope every card + // interaction to the visible form; the search dialog stays page-level (it + // portals outside). + const responsiblesCard = page + .locator('form') + .filter({ hasText: 'Responsáveis pela demanda' }) + .filter({ visible: true }) + // The responsible manages the list: add the peer through the search dialog. await campaign.sessionFor(context, responsible) await page.goto(demandURL) - await expect(page.getByText('Só quem é responsável vê esta demanda.')).toBeVisible() + await expect(responsiblesCard.getByText('Só quem é responsável vê esta demanda.')).toBeVisible() // The trigger announces the current selection (e.g. "…: 1 responsável"). - await page.getByRole('button', { name: /Responsáveis pela demanda/ }).click() + await responsiblesCard.getByRole('button', { name: /Responsáveis pela demanda/ }).click() await page.getByPlaceholder('Buscar assessor…').fill(peer.name) await page.getByRole('option', { name: peer.name }).click() // The multi-add dialog stays open (modal) — close it before saving. await page.keyboard.press('Escape') - await page.getByRole('button', { name: 'Salvar responsáveis' }).click() - await expect(page.getByText('Responsáveis atualizados.')).toBeVisible() - await expect(page.getByRole('button', { name: `Remover ${peer.name}` })).toBeVisible() + await responsiblesCard.getByRole('button', { name: 'Salvar responsáveis' }).click() + await expect(responsiblesCard.getByText('Responsáveis atualizados.')).toBeVisible() + await expect( + responsiblesCard.getByRole('button', { name: `Remover ${peer.name}` }), + ).toBeVisible() // The peer now opens the demand. await campaign.sessionFor(context, peer) @@ -92,15 +107,17 @@ test.describe('C143 — demand visibility (explicit responsibles)', () => { // Back as the responsible, remove the peer and revoke the access again. await campaign.sessionFor(context, responsible) await page.goto(demandURL) - await page.getByRole('button', { name: `Remover ${peer.name}` }).click() + await responsiblesCard.getByRole('button', { name: `Remover ${peer.name}` }).click() // The responsibles save is an optimistic server-action write — wait for the // POST to land BEFORE swapping to the peer, or the peer's navigation can // race the revoke (OPS83 run #16: the accessible demand still served). await Promise.all([ expectPostResponse(page, demandURL), - page.getByRole('button', { name: 'Salvar responsáveis' }).click(), + responsiblesCard.getByRole('button', { name: 'Salvar responsáveis' }).click(), ]) - await expect(page.getByRole('button', { name: `Remover ${peer.name}` })).toHaveCount(0) + await expect( + responsiblesCard.getByRole('button', { name: `Remover ${peer.name}` }), + ).toHaveCount(0) // The peer already rendered THIS URL (granted, line 88), so the Next RSC // cache can serve the previous payload on a plain goto even after the