diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 7fb3128b..29283b15 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**S6** — home de campanha (jorgesolla1313.com.br) sem carrossel no desktop: as seções "Por que essa eleição importa" (3 cards) e "Nossa caminhada" (6 cards) mostram agora todo o conteúdo de uma vez em ≥1024px — grade estática 3 colunas (problem: 3×437px, bandeiras: 3×2 com altura por conteúdo, padding `p-4`), sem auto-avanço, sem chips/indicadores e sem `aria-current`. Mobile (<1024px) 100% preservado: carrossel com auto-avanço ~4s, chips sincronizados, swipe — componente e geometria mobile intocados. Padrão S1 (renderização-irmã com toggle CSS `hidden lg:grid`/`lg:hidden`): grade server-rendered (zero JS no desktop) + carrossel client embrulhado; os dois cards extraídos do `CampaignCarousel` para `CampaignProblemCard`/`CampaignFlagCard` (presentacionais, fonte única do markup, `size='spacious'` só na grade) e o contrato da grade (data-grid + aria-label + 3 colunas) vive num `CampaignCardGrid` local do page. O carrossel ficou byte-equivalente (só re-roteia os cards; unit tests intocados) e ganhou guard de `display:none` no auto-avanço — o timer nunca roda no desktop (chain morto numa subárvore oculta). Geometria: a seção flags recalibrada no desktop (`height: clamp(418px, calc(807.7px - 28.13vw), 520px)`, âncoras medidas em 1024/1280/1440+, respiro ≥30px, `overflow: hidden` para zoom de texto ≥125% não pintar sobre o footer); a seção problem manteve a geometria (respiro ~31px). Zero schema/migration/Consent; mesma copy/imagens/ordem; S1 e o resto da home intocados. Verificação: unit 2318 + int 791 verdes, e2e frontend 11/11 (novo teste de grade desktop com invariantes 3×1/3×2, ausência de regiões/chips/`aria-current`, respiro ≥24 e `scrollHeight ≤ clientHeight` em 1024/1280/1440/1920; teste de carrossel migrado para viewport mobile 390px com auto-avanço + chips; swipe/mobileSpacing intactos), lint/format/typecheck/knip/cycles/build verdes. `/simplify` 3 reviewers: `lg:` órfãos removidos do carrossel, grade extraída em `CampaignCardGrid`, `overflow: hidden` (zoom ≥125%), guard do timer, `document.fonts.ready` + eixo x + pino de clip no e2e, âncoras no comentário do clamp. Débitos: Issue #58 registrada (S6-FOLLOWUP — CLI do Playwright 1.58.2 ignora `--grep`/posicionais e o `test:e2e:affected` roda a suíte completa; `depends: [S6]`); defer com gatilho para os literais de gap da grade (recalibração Penpot do carrossel); descartados contratos de tipo dos cards (armadilha knip), aria-labels duplicados (copy pinada no e2e) e pino de tablet 768–1023. Impl plan: `docs/plans/home-campanha-sem-carrossel-desktop-impl.md`. + **S1** — seção "Acompanhe de perto" na home de campanha (jorgesolla1313.com.br), na 3ª posição entre a prova social e "Por que essa eleição importa": bento de 5 artigos do `post` collection (1 card grande + 4 pequenos) no desktop e carrossel de 1 conteúdo por tela com dots e contador "N de M" no mobile (~390px), badge de tipo (Notícia/Campanha/Artigo/Evento), capa, título e meta "há X · categoria" (relativa < 48h, data absoluta além — nunca mente congelada na ISR); card inteiro é link para a rota canônica do artigo. Fail-closed: sem posts visíveis (`isPostVisible` — tag `hidden` eleitoral nunca vaza) a seção some inteira e Prova → Problema voltam a ser contíguos. Zero schema/migration/Consent; leitura via `getVisiblePosts` sob a tag `posts` (home estática revalidada pelo mecanismo `posts` existente). Primeira fatia do board aprovado (S2/S3 = YouTube/Instagram). Verificação: unit 2282 + int 791 verdes, e2e frontend 10/10 em dev e prod (`E2E_PROD=1`, `.next-e2e`), suíte completa prod 169-170 passed com flakes ambientais de campanha (máquina compartilhada; testes diferentes a cada run, nenhum no código tocado); criação via REST com hooks reais; polling positivo do estado da home pós-revalidate (ISR serve stale uma vez sob carga) e navegação para about:blank antes do cleanup (prefetch de links apagados); lint/format/typecheck/cycles/knip/build verdes. `/simplify` 3 reviewers: 2 P1 de a11y (foco+setas no track, `inert` em slides inativos) corrigidos; o `revalidate = 300` proposto contra o congelamento da label relativa foi TESTADO e REVERTIDO — no Next 15.4 em prod ele trava o bust por tag (`revalidateTag('posts')` deixava de regenerar a home; medido em probe manual) — a label relativa ganhou threshold de 48h com fallback absoluto; P2 absorvidos (dots ≥3:1, aria-live no contador, motion-reduce na capa, fidelidade de gaps, secret do e2e espelhado da env); débitos deferidos documentados no impl plan (refusal do `formatRelativeAge` por contrato, gutter flow-centered, link `/artigos` sancionado pela intenção). **S1** — seção "Acompanhe de perto" na home de campanha (jorgesolla1313.com.br), na 3ª posição entre a prova social e "Por que essa eleição importa": bento de 5 artigos do `post` collection (1 card grande + 4 pequenos) no desktop e carrossel de 1 conteúdo por tela com dots e contador "N de M" no mobile (~390px), badge de tipo (Notícia/Campanha/Artigo/Evento), capa, título e meta relativa "há X · categoria"; card inteiro é link para a rota canônica do artigo. Fail-closed: sem posts visíveis (`isPostVisible` — tag `hidden` eleitoral nunca vaza) a seção some inteira e Prova → Problema voltam a ser contíguos. Zero schema/migration/Consent; leitura via `getVisiblePosts` sob a tag `posts`; a home ganhou `revalidate = 300` para a label relativa não congelar na ISR. Primeira fatia do board aprovado (S2/S3 = YouTube/Instagram). Verificação: unit 2282 + int 791 verdes, e2e frontend 10/10 (empty-state + full-state desktop/mobile, fail-closed com tag oculta, click-through, teclado/setas, swipe, dots; criação via REST com hooks reais; cleanup convergindo a cache de dev), lint/format/typecheck/cycles/knip/build verdes. `/simplify` 3 reviewers: 2 P1 de a11y (foco+setas no track, `inert` em slides inativos) e o congelamento da data relativa na ISR corrigidos; P2 absorvidos (dots ≥3:1, aria-live no contador, motion-reduce na capa, fidelidade de gaps, secret do e2e espelhado da env); débitos deferidos documentados no impl plan (refusal do `formatRelativeAge` por contrato, gutter flow-centered, link `/artigos` sancionado pela intenção). diff --git a/docs/changelog/2026-08-18-s6.md b/docs/changelog/2026-08-18-s6.md new file mode 100644 index 00000000..9356bd68 --- /dev/null +++ b/docs/changelog/2026-08-18-s6.md @@ -0,0 +1 @@ +**S6** — home de campanha (jorgesolla1313.com.br) sem carrossel no desktop: as seções "Por que essa eleição importa" (3 cards) e "Nossa caminhada" (6 cards) mostram agora todo o conteúdo de uma vez em ≥1024px — grade estática 3 colunas (problem: 3×437px, bandeiras: 3×2 com altura por conteúdo, padding `p-4`), sem auto-avanço, sem chips/indicadores e sem `aria-current`. Mobile (<1024px) 100% preservado: carrossel com auto-avanço ~4s, chips sincronizados, swipe — componente e geometria mobile intocados. Padrão S1 (renderização-irmã com toggle CSS `hidden lg:grid`/`lg:hidden`): grade server-rendered (zero JS no desktop) + carrossel client embrulhado; os dois cards extraídos do `CampaignCarousel` para `CampaignProblemCard`/`CampaignFlagCard` (presentacionais, fonte única do markup, `size='spacious'` só na grade) e o contrato da grade (data-grid + aria-label + 3 colunas) vive num `CampaignCardGrid` local do page. O carrossel ficou byte-equivalente (só re-roteia os cards; unit tests intocados) e ganhou guard de `display:none` no auto-avanço — o timer nunca roda no desktop (chain morto numa subárvore oculta). Geometria: a seção flags recalibrada no desktop (`height: clamp(418px, calc(807.7px - 28.13vw), 520px)`, âncoras medidas em 1024/1280/1440+, respiro ≥30px, `overflow: hidden` para zoom de texto ≥125% não pintar sobre o footer); a seção problem manteve a geometria (respiro ~31px). Zero schema/migration/Consent; mesma copy/imagens/ordem; S1 e o resto da home intocados. Verificação: unit 2318 + int 791 verdes, e2e frontend 11/11 (novo teste de grade desktop com invariantes 3×1/3×2, ausência de regiões/chips/`aria-current`, respiro ≥24 e `scrollHeight ≤ clientHeight` em 1024/1280/1440/1920; teste de carrossel migrado para viewport mobile 390px com auto-avanço + chips; swipe/mobileSpacing intactos), lint/format/typecheck/knip/cycles/build verdes. `/simplify` 3 reviewers: `lg:` órfãos removidos do carrossel, grade extraída em `CampaignCardGrid`, `overflow: hidden` (zoom ≥125%), guard do timer, `document.fonts.ready` + eixo x + pino de clip no e2e, âncoras no comentário do clamp. Débitos: Issue #58 registrada (S6-FOLLOWUP — CLI do Playwright 1.58.2 ignora `--grep`/posicionais e o `test:e2e:affected` roda a suíte completa; `depends: [S6]`); defer com gatilho para os literais de gap da grade (recalibração Penpot do carrossel); descartados contratos de tipo dos cards (armadilha knip), aria-labels duplicados (copy pinada no e2e) e pino de tablet 768–1023. Impl plan: `docs/plans/home-campanha-sem-carrossel-desktop-impl.md`. diff --git a/docs/plans/home-campanha-sem-carrossel-desktop-impl.md b/docs/plans/home-campanha-sem-carrossel-desktop-impl.md new file mode 100644 index 00000000..3ceba19c --- /dev/null +++ b/docs/plans/home-campanha-sem-carrossel-desktop-impl.md @@ -0,0 +1,162 @@ +# Impl: S6 — Home de campanha sem carrossel no desktop + +Status: aprovado +Atualizado em: 2026-08-18 +Issue: #34 +Intenção: docs/plans/home-campanha-sem-carrossel-desktop.md +Appetite restante: herdado (~0,5–1 dia eng; só layout, sem dados novos) + +## Leitura da intenção + +- **Outcome:** no desktop (≥1024px) as seções "Por que essa eleição importa" (3 cards) e + "Nossa caminhada" (6 cards) mostram todo o conteúdo de uma vez — grade estática, sem + auto-avanço, sem rolagem oculta, sem indicadores/chips; mobile (<1024px) 100% preservado. +- **O que NÃO negociar:** mobile intocado (comportamento e geometria); mesma copy/imagens/ordem; + sem redesenho de cards; seção "Acompanhe de perto" (S1) fora do escopo; zero schema/migration/ + Consent/dados. +- **Questões em aberto da intenção (assumidas, validar no gate):** chips somem no desktop (A); + cards de bandeira com altura por conteúdo e respiro (A); breakpoint `lg` = 1024px (A); a seção + dos 6 cards é "Nossa caminhada" (confirmado — eyebrow atual no site). +- **O que reavaliar:** a hipótese "variante estática em `lg` dentro do `CampaignCarousel`" — + **rejeitada**: o `CampaignCarousel` é o módulo pinado à geometria Penpot de auto-avanço + (precedente S1: "not a variant of `CampaignCarousel`"); o padrão consagrado no repo para + "bento/grid no desktop + carrossel no mobile" é renderização-irmã com toggle CSS (`hidden md:grid` + - `md:hidden` em `CampaignContentSection`). Ver Decisão 1. + +## Abordagem recomendada + +```mermaid +flowchart LR + subgraph page[page.tsx — server] + A[problemItems/flagItems] --> B[Grade estática desktop
hidden lg:grid] + A --> C[Carrossel mobile existente
lg:hidden] + end + B --> D[CampaignProblemCard / CampaignFlagCard
components presentacionais compartilhados] + C --> D + E[styles.css
@media min-width 64rem
.campaign-flags height recalibrada] --> B +``` + +**Opções consideradas:** A | B | C | D +**Recomendação:** **A — renderizações-irmãs no `page.tsx`** (grade estática server-rendered +`hidden lg:grid` + carrossel atual embrulhado em `lg:hidden`), com os dois cards extraídos para +components presentacionais compartilhados — exatamente o padrão S1 (`CampaignContentSection` + +`CampaignArticleCard`). +**Rejeitadas:** + +- **B) track único vira grade via CSS dentro do `CampaignCarousel`** (`lg:grid lg:overflow-visible` + - matchMedia para suprimir JS/a11y no desktop). O S1 já rejeitou estender esse módulo para um + layout diferente ("pinned to the Penpot auto-advancing geometry"); a troca de semântica a11y + (`aria-roledescription="carrossel"`, "N de M") não é expressável em CSS — exigiria estado de + matchMedia com flip pós-hidratação — e o componente é pinado por unit + e2e de swipe. +- **C) desmontar o carrossel no desktop** (wrapper client com matchMedia). Custo: componente + wrapper novo + dupla renderização pós-hidratação para matar um timer invisível de ~1 re-render/ + 4s numa subárvore `display:none` — o mesmo custo que o S1 já aceita (carrossel montado oculto + no desktop, sem timer por não ter auto-avanço). Não vale o movimento. +- **D) duplicar o JSX dos cards inline no `page.tsx`** (grade copiada, carrossel intocado). + Cria duas fontes de verdade do markup do card (~50 linhas) com risco de drift em qualquer + mudança visual futura. O S1 extraiu o card (`CampaignArticleCard`) — a extração é o dono do + concern. + +### Componentes / mudanças + +- **`CampaignProblemCard`** (`src/components/CampaignProblemCard.tsx`, server/presentacional): + o artigo do card `problem` (imagem `fill`/`imageFrame`, gradiente, título+corpo overlay) — + extraído verbatim do `li` do carrossel; recebe `item` (`CampaignCarouselItem`). +- **`CampaignFlagCard`** (`src/components/CampaignFlagCard.tsx`, server/presentacional): o artigo + do card `flags` (título+corpo) — extraído verbatim; recebe `item` + prop `size: 'compact' | +'spacious'` (padding `px-3.5 py-2.5` no carrossel — mobile intocado — e `p-4` na grade, + conforme o rascunho UI aprovado). +- **`CampaignCarousel`** (`src/components/CampaignCarousel.tsx`): SÓ re-roteia o markup dos cards + para os dois components extraídos; classes, estado, timers e a11y byte-idênticos. Unit tests + existentes permanecem sem mudança. +- **`page.tsx`** (`src/app/(frontend)/(home)/page.tsx`): dentro de cada wrapper absoluto existente + (`.campaign-problem-carousel` / `.campaign-flags-carousel`), renderiza: + - grade desktop: ` diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css index ec6f6f6b..1400734d 100644 --- a/src/app/(frontend)/styles.css +++ b/src/app/(frontend)/styles.css @@ -460,7 +460,18 @@ body, } } +/* Desktop grid recalibration (S6): the flags section grows so both grid rows + of content-height cards fit with breathing room. Fluid clamp between the + measured anchors: ~520px at 1024px (narrow columns, 3-line card bodies) → + ~418px from ~1385px up (columns saturate at the content max width). + `overflow: hidden` keeps text-zoom growth (≥125%) inside the band instead + of painting the cards over the footer. */ @media (min-width: 64rem) { + .campaign-flags { + height: clamp(418px, calc(807.7px - 28.13vw), 520px); + overflow: hidden; + } + [data-theme='campaign-site'] { --campaign-desktop-portrait-width: clamp(465px, calc(26.45vw + 194.15px), 702px); --campaign-desktop-portrait-height: clamp(517px, calc(29.41vw + 215.84px), 781px); diff --git a/src/components/CampaignCarousel.tsx b/src/components/CampaignCarousel.tsx index 8c17af1c..25ae74a4 100644 --- a/src/components/CampaignCarousel.tsx +++ b/src/components/CampaignCarousel.tsx @@ -1,6 +1,7 @@ 'use client' -import Image from 'next/image' +import { CampaignFlagCard } from '@/components/CampaignFlagCard' +import { CampaignProblemCard } from '@/components/CampaignProblemCard' import { useCallback, useEffect, useRef, useState } from 'react' const AUTO_ADVANCE_MS = 4_000 @@ -83,6 +84,9 @@ export const CampaignCarousel = ({ ariaLabel, items, variant }: CampaignCarousel useEffect(() => { if (prefersReducedMotion || temporarilyPaused || items.length < 2) return + // S6: on desktop the carousel lives inside an `lg:hidden` wrapper — its + // auto-advance chain must not keep re-rendering a display:none subtree. + if (trackRef.current && window.getComputedStyle(trackRef.current).display === 'none') return const timer = window.setTimeout(() => { scrollToItem((activeIndex + 1) % items.length) @@ -142,7 +146,7 @@ export const CampaignCarousel = ({ ariaLabel, items, variant }: CampaignCarousel
{items.map((item, index) => { const isActive = index === activeIndex @@ -176,7 +180,7 @@ export const CampaignCarousel = ({ ariaLabel, items, variant }: CampaignCarousel onScroll={syncActiveItem} data-carousel-track className={`scrollbar-hide m-0 flex list-none snap-x snap-mandatory overflow-x-auto scroll-smooth p-0 ${ - variant === 'problem' ? 'gap-2.5 lg:gap-[21px]' : 'mt-5 gap-2.5 lg:gap-[14px]' + variant === 'problem' ? 'gap-2.5' : 'mt-5 gap-2.5' }`} > {items.map((item, index) => ( @@ -187,45 +191,14 @@ export const CampaignCarousel = ({ ariaLabel, items, variant }: CampaignCarousel aria-current={index === activeIndex ? 'true' : undefined} className={`m-0 shrink-0 snap-start ${ variant === 'problem' - ? 'h-[437px] w-[calc(100vw_-_38px)] max-w-[355px] lg:w-[355px]' - : 'h-[100px] w-[calc(100vw_-_66px)] max-w-[327px] lg:w-[327px]' + ? 'h-[437px] w-[calc(100vw_-_38px)] max-w-[355px]' + : 'h-[100px] w-[calc(100vw_-_66px)] max-w-[327px]' }`} > {variant === 'problem' ? ( -
- {item.image && item.imageFrame ? ( - {item.imageAlt - ) : item.image ? ( - {item.imageAlt - ) : null} -
-
-

- {item.title} -

-

{item.body}

-
-
+ ) : ( -
-

- {item.title} -

-

{item.body}

-
+ )} ))} diff --git a/src/components/CampaignFlagCard.tsx b/src/components/CampaignFlagCard.tsx new file mode 100644 index 00000000..1e7ad306 --- /dev/null +++ b/src/components/CampaignFlagCard.tsx @@ -0,0 +1,24 @@ +type CampaignFlagCardItem = { + id: string + title: string + body: string +} + +type CampaignFlagCardProps = { + item: CampaignFlagCardItem + size?: 'compact' | 'spacious' +} + +export const CampaignFlagCard = ({ item, size = 'compact' }: CampaignFlagCardProps) => { + const padding = size === 'spacious' ? 'p-4' : 'px-3.5 py-2.5' + return ( +
+

+ {item.title} +

+

{item.body}

+
+ ) +} diff --git a/src/components/CampaignProblemCard.tsx b/src/components/CampaignProblemCard.tsx new file mode 100644 index 00000000..3ff602f5 --- /dev/null +++ b/src/components/CampaignProblemCard.tsx @@ -0,0 +1,44 @@ +import Image from 'next/image' + +export type CampaignProblemCardItem = { + id: string + title: string + body: string + image?: string + imageAlt?: string + imageFrame?: { + width: number + height: number + className: string + } +} + +export const CampaignProblemCard = ({ item }: { item: CampaignProblemCardItem }) => ( +
+ {item.image && item.imageFrame ? ( + {item.imageAlt + ) : item.image ? ( + {item.imageAlt + ) : null} +
+
+

+ {item.title} +

+

{item.body}

+
+
+) diff --git a/tests/e2e/frontend.e2e.spec.ts b/tests/e2e/frontend.e2e.spec.ts index d220af3c..a9a1ca53 100644 --- a/tests/e2e/frontend.e2e.spec.ts +++ b/tests/e2e/frontend.e2e.spec.ts @@ -350,9 +350,93 @@ test.describe('Frontend', () => { } }) - test('auto-advances and keeps carousel controls synchronized', async ({ page }) => { + test('shows the campaign message sections as static grids on desktop', async ({ page }) => { await page.goto('/') + const problemGrid = page.locator('[data-home-section="problem"] [data-grid="problem"]') + await expect(problemGrid).toBeVisible() + await expect(problemGrid.getByRole('listitem')).toHaveCount(3) + const problemBoxes = await problemGrid + .getByRole('listitem') + .evaluateAll((items) => items.map((item) => item.getBoundingClientRect())) + expect(new Set(problemBoxes.map((box) => Math.round(box.y))).size).toBe(1) + expect(new Set(problemBoxes.map((box) => Math.round(box.x))).size).toBe(3) + expect(problemBoxes.every((box) => box.width > 0 && box.height > 0)).toBe(true) + + const flagsGrid = page.locator('[data-home-section="flags"] [data-grid="flags"]') + await expect(flagsGrid).toBeVisible() + await expect(flagsGrid.getByRole('listitem')).toHaveCount(6) + const flagsBoxes = await flagsGrid + .getByRole('listitem') + .evaluateAll((items) => items.map((item) => item.getBoundingClientRect())) + expect(new Set(flagsBoxes.map((box) => Math.round(box.x))).size).toBe(3) + expect(new Set(flagsBoxes.map((box) => Math.round(box.y))).size).toBe(2) + + // No carousel chrome on desktop: regions and chips are display:none (out + // of the a11y tree) and the hidden carousel never starts its auto-advance + // chain, so no aria-current state exists anywhere in the grids. + await expect( + page.getByRole('region', { name: 'Bandeiras que tornam esta eleição decisiva' }), + ).toHaveCount(0) + await expect(page.getByRole('region', { name: 'Bandeiras da campanha' })).toHaveCount(0) + await expect(page.locator('[data-carousel-chips]')).toBeHidden() + await expect(problemGrid.locator('[aria-current]')).toHaveCount(0) + await expect(flagsGrid.locator('[aria-current]')).toHaveCount(0) + + // The grids stay inside their sections with breathing room at every + // desktop width, no card is clipped (no hidden scroll) and nothing + // overflows the page horizontally. Fonts settle first: the flags cards + // are content-height, so a fallback-font measurement could differ from + // the real layout. + await page.evaluate(() => document.fonts.ready) + for (const width of [1024, 1280, 1440, 1920]) { + await page.setViewportSize({ width, height: 900 }) + const spacing = await page.evaluate(() => { + const section = (selector: string) => { + const element = document.querySelector(selector) + if (!element) throw new Error(`Seção ausente: ${selector}`) + return element.getBoundingClientRect() + } + const gridBottom = (selector: string) => { + const grid = document.querySelector(selector) + if (!grid) throw new Error(`Grade ausente: ${selector}`) + const cards = Array.from(grid.querySelectorAll('li')) + return Math.max(...cards.map((card) => card.getBoundingClientRect().bottom)) + } + const gridClipped = (selector: string) => { + const grid = document.querySelector(selector) + if (!grid) throw new Error(`Grade ausente: ${selector}`) + return grid.scrollHeight - grid.clientHeight + } + const problem = section('[data-home-section="problem"]') + const flags = section('[data-home-section="flags"]') + const scrollContainer = document.querySelector('[data-theme="campaign-site"]') + return { + problemBottomGap: + problem.bottom - gridBottom('[data-home-section="problem"] [data-grid]'), + flagsBottomGap: flags.bottom - gridBottom('[data-home-section="flags"] [data-grid]'), + problemClipped: gridClipped('[data-home-section="problem"] [data-grid]'), + flagsClipped: gridClipped('[data-home-section="flags"] [data-grid]'), + horizontalOverflow: scrollContainer + ? scrollContainer.scrollWidth - scrollContainer.clientWidth + : 0, + } + }) + expect(spacing.problemBottomGap).toBeGreaterThanOrEqual(24) + expect(spacing.flagsBottomGap).toBeGreaterThanOrEqual(24) + expect(spacing.problemClipped).toBeLessThanOrEqual(1) + expect(spacing.flagsClipped).toBeLessThanOrEqual(1) + expect(spacing.horizontalOverflow).toBeLessThanOrEqual(1) + } + }) + + test('keeps carousel auto-advance and chips on mobile', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }) + await page.goto('/') + + await expect(page.locator('[data-home-section="problem"] [data-grid]')).toBeHidden() + await expect(page.locator('[data-home-section="flags"] [data-grid]')).toBeHidden() + const problemCarousel = page.getByRole('region', { name: 'Bandeiras que tornam esta eleição decisiva', }) diff --git a/tests/unit/campaignHome.unit.spec.tsx b/tests/unit/campaignHome.unit.spec.tsx index 1b6036c5..e2952dcf 100644 --- a/tests/unit/campaignHome.unit.spec.tsx +++ b/tests/unit/campaignHome.unit.spec.tsx @@ -45,11 +45,18 @@ describe('Campaign home', () => { it('usa exatamente as seis bandeiras aprovadas', async () => { render(await HomePage()) - expect(screen.getByText('Defender o SUS e valorizar quem cuida')).toBeTruthy() - expect(screen.getByText('Fim da escala 6×1 e jornada de 40h')).toBeTruthy() - expect(screen.getByText('Educação em tempo integral e federais no interior')).toBeTruthy() - expect(screen.getByText('Recomprar a Refinaria de Mataripe')).toBeTruthy() - expect(screen.getByText('Salário mínimo forte, emprego e moradia')).toBeTruthy() - expect(screen.getByText('Defesa intransigente da democracia')).toBeTruthy() + // S6: cada bandeira aparece em duas renderizações irmãs (grade desktop e + // carrossel mobile); o teste garante que as duas seguem a mesma copy. + const expectedTitles = [ + 'Defender o SUS e valorizar quem cuida', + 'Fim da escala 6×1 e jornada de 40h', + 'Educação em tempo integral e federais no interior', + 'Recomprar a Refinaria de Mataripe', + 'Salário mínimo forte, emprego e moradia', + 'Defesa intransigente da democracia', + ] + for (const title of expectedTitles) { + expect(screen.getAllByText(title)).toHaveLength(2) + } }) })