import { cleanup, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import HomePage from '@/app/(frontend)/(home)/page' import { toContentPiecePublicItem, type ContentPiecePublicItem } from '@/lib/contentPieceCatalog' const pieceItem = toContentPiecePublicItem({ id: 1, slug: 'peca-da-home', title: 'Peça da home', type: 'foto', status: 'publicado', origin: 'arquivo', media: { id: 2, filename: 'peca.png', mimeType: 'image/png' }, }) if (!pieceItem) throw new Error('fixture should be public') type MockImageProps = React.ImgHTMLAttributes & { fill?: boolean priority?: boolean } vi.mock('next/image', () => ({ default: ({ alt, fill: _fill, priority: _priority, ...props }: MockImageProps) => ( // eslint-disable-next-line @next/next/no-img-element {alt} ), })) // The content section is an async server component that reads posts through // the Payload DB; the unit env has no database and an async child suspends the // whole tree under testing-library. Its empty/full behavior is covered by e2e // (frontend.e2e.spec.ts), so the home skeleton tests render it away. vi.mock('@/components/CampaignContentSection', () => ({ CampaignContentSection: () => null, })) // The S9 capture form is a client component (radix comboboxes/checkbox) whose // interactions are e2e-covered; the unit skeleton only pins the section shell. vi.mock('@/components/CampaignNewsletterForm', () => ({ CampaignNewsletterCapture: () => null, })) // S10 — the section reads the site-settings global (server-only, Payload DB); // the unit env has no database, and the pixel behavior is e2e-covered. vi.mock('@/utilities/campaignHomeTracking', () => ({ getCampaignHomeMetaPixelId: async () => null, })) // S21/S22 — the home reads the jingle listing through the Payload DB + // unstable_cache for the sound section and the footer discovery flag; the unit // env has neither, and the real listing/kill-switch behavior is e2e-covered // (frontendJingles.e2e.spec.ts). vi.mock('@/utilities/jingleReads', () => ({ getPublishedJingleItems: async () => [], })) // S27/S39 — the same for the Central de Conteúdos listing (cached read that // feeds the discovery flag and the home sample); the real kill-switch behavior // is e2e-covered (frontendConteudos.e2e.spec.ts). const contentPieces = vi.hoisted(() => ({ items: [] as ContentPiecePublicItem[] })) vi.mock('@/utilities/content/contentPieceReads', () => ({ getPublishedContentPieceItems: async () => contentPieces.items, })) // S14 — the card section renders the client studio island (next/font local // face + matchMedia + canvas); its behavior is e2e-covered, so the unit // skeleton mocks both the face module and the island. vi.mock('@/app/(frontend)/fonts', () => ({ brexterBold: { variable: 'font-brexter-mock', style: { fontFamily: 'Brexter' } }, })) vi.mock('@/components/cards/CardsStudio', () => ({ CardsStudio: () => null, })) afterEach(() => { contentPieces.items = [] cleanup() }) describe('Campaign home', () => { it('monta as seis seções previstas e o rodapé eleitoral', async () => { render(await HomePage()) expect(screen.getByRole('heading', { level: 1, name: 'MAIS SAÚDE MAIS FUTURO' })).toBeTruthy() expect(screen.getByText('3.333')).toBeTruthy() expect(screen.getByText('1.031')).toBeTruthy() expect(screen.getByText('3º')).toBeTruthy() expect(screen.getByRole('heading', { name: /Eleger deputado é coisa séria/i })).toBeTruthy() expect( screen.getByRole('heading', { name: /Junto com o trabalhador e do lado de quem mais precisa, sempre/i, }), ).toBeTruthy() // S22 — with zero published jingles the sound section shows only the radio. expect(screen.getByRole('heading', { name: 'Sintonize com a Rádio 1313' })).toBeTruthy() expect(screen.getByRole('heading', { name: /Mostre que você está com Solla/i })).toBeTruthy() expect(screen.getByRole('heading', { name: /Receba as novidades da campanha/i })).toBeTruthy() expect(screen.getByText(/CNPJ: 68\.430\.467\/0001-05/)).toBeTruthy() }) it('usa exatamente as seis bandeiras aprovadas', async () => { render(await HomePage()) // 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) } }) it('mostra a seção da Central com peça publicada e a esconde sem nenhuma (fail-closed)', async () => { contentPieces.items = [pieceItem] render(await HomePage()) expect(screen.getByRole('heading', { name: 'Peça voto pra Solla 1313' })).toBeTruthy() // The single-piece layout renders one CTA per viewport (split desktop + // stacked mobile); both hand off to the Central. const ctas = screen.getAllByRole('link', { name: /Ver todas as peças/ }) expect(ctas.length).toBeGreaterThan(0) for (const cta of ctas) expect(cta.getAttribute('href')).toBe('/conteudos') cleanup() contentPieces.items = [] render(await HomePage()) expect(screen.queryByRole('heading', { name: 'Peça voto pra Solla 1313' })).toBeNull() expect(screen.queryByRole('link', { name: /Ver todas as peças/ })).toBeNull() }) })