teqo-backup/tests/unit/campaignMobileTopBar.unit.spec.tsx
Francisco Solla f0dff330bd feat(C102): sem hamburger/sheet de sidebar no mobile staff — navegação pela barra + Mais
No mobile (<768px) staff perde o botão de abrir sidebar na top bar
(CampaignMobileTopBar role-gated via useCampaignPageChromeRole +
isStaffCampaignRole; leader mantém) e o sheet de navegação deixa de
montar (CampaignSidebar early-return quando isMobile && staff).
Filtros salvos de Municípios (B18) migram para o drawer Mais
(MunicipalityNavSavedFilters variant 'overflow', mesma store,
apagar/desfazer/sucessão de foco; drawer ganha overflow-y-auto).

Correção de bug pré-existente (D4): o Toaster vivia dentro do wrapper
[contain:layout_paint] do painel da AI, que vira containing block para
position:fixed e stacking context próprio — toasts com ação (ex.
Desfazer) ficavam inalcançáveis sob os portais z-50 de drawer/sheet.
Movido para o root layout (campaign).

Testes: unit top bar (staff sem trigger / leader com trigger) + e2e
(staff sem trigger/dialog, leader com sheet, B18 no drawer completo).
2026-08-09 19:13:43 -03:00

228 lines
8.1 KiB
TypeScript

import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const pathnameState = vi.hoisted(() => ({ value: '/campanha/municipios' }))
vi.mock('next/navigation', () => ({
usePathname: () => pathnameState.value,
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
}),
}))
import {
CampaignHomeSearchChromeProvider,
useSetCampaignHomeSearchChrome,
type CampaignHomeSearchChromeState,
} from '@/components/campaign/shell/CampaignHomeSearchChromeContext'
import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar'
import { CampaignPageChromeProvider } from '@/components/campaign/shell/CampaignPageChromeContext'
import {
CampaignWizardChromeProvider,
useSetCampaignWizardChrome,
type CampaignWizardChromeState,
} from '@/components/campaign/shell/CampaignWizardChromeContext'
import { SidebarProvider } from '@/components/ui/Sidebar'
import { stubMatchMedia } from '../helpers/matchMedia'
const WizardChromeProbe = ({ chrome }: { chrome: CampaignWizardChromeState | null }) => {
useSetCampaignWizardChrome(chrome)
return null
}
const HomeSearchChromeProbe = ({ chrome }: { chrome: CampaignHomeSearchChromeState | null }) => {
useSetCampaignHomeSearchChrome(chrome)
return null
}
const renderTopBar = (input: {
wizardChrome?: CampaignWizardChromeState | null
homeSearchChrome?: CampaignHomeSearchChromeState | null
pathname?: string
}) => {
pathnameState.value = input.pathname ?? '/campanha/municipios'
return render(
<SidebarProvider>
<CampaignPageChromeProvider role="coordinator">
<CampaignWizardChromeProvider>
<CampaignHomeSearchChromeProvider>
<WizardChromeProbe chrome={input.wizardChrome ?? null} />
<HomeSearchChromeProbe chrome={input.homeSearchChrome ?? null} />
<CampaignMobileTopBar />
</CampaignHomeSearchChromeProvider>
</CampaignWizardChromeProvider>
</CampaignPageChromeProvider>
</SidebarProvider>,
)
}
describe('CampaignMobileTopBar', () => {
beforeEach(() => {
stubMatchMedia()
})
afterEach(() => {
cleanup()
vi.unstubAllGlobals()
})
it('renders page chrome from pathname in app mode', () => {
const { container } = renderTopBar({})
const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]')
expect(topBar?.getAttribute('data-mode')).toBe('app')
expect(screen.getByText('Municípios')).toBeTruthy()
expect(screen.queryByRole('link', { name: /Voltar/ })).toBeNull()
expect(screen.queryByRole('button', { name: 'Fechar busca' })).toBeNull()
expect(screen.queryByRole('link', { name: 'Sair da ação' })).toBeNull()
})
it('renders empty chrome on home', () => {
const { container } = renderTopBar({ pathname: '/campanha' })
const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]')
expect(topBar?.getAttribute('data-mode')).toBe('app')
expect(container.querySelector('[data-slot="campaign-page-chrome"]')).toBeNull()
})
it('renders app mode with Voltar when home search is focused (B106)', () => {
const collapse = vi.fn()
const { container } = renderTopBar({
pathname: '/campanha',
homeSearchChrome: { focused: true, collapse },
})
const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]')
expect(topBar?.getAttribute('data-home-search-focused')).toBe('true')
expect(screen.queryByRole('button', { name: 'Abrir ou fechar menu da campanha' })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Fechar busca' }))
expect(collapse).toHaveBeenCalledTimes(1)
})
it('does not render the sidebar trigger for staff in app mode (C102)', () => {
const { container } = renderTopBar({})
const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]')
expect(topBar?.getAttribute('data-mode')).toBe('app')
expect(screen.queryByRole('button', { name: 'Abrir ou fechar menu da campanha' })).toBeNull()
})
it('renders the sidebar trigger for the leader in app mode (C102)', () => {
const { container } = render(
<SidebarProvider>
<CampaignPageChromeProvider role="leader">
<CampaignWizardChromeProvider>
<CampaignHomeSearchChromeProvider>
<CampaignMobileTopBar />
</CampaignHomeSearchChromeProvider>
</CampaignWizardChromeProvider>
</CampaignPageChromeProvider>
</SidebarProvider>,
)
const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]')
expect(topBar?.getAttribute('data-mode')).toBe('app')
expect(screen.getByRole('button', { name: 'Abrir ou fechar menu da campanha' })).toBeTruthy()
})
it('renders wizard entry mode with dismiss control and flow title', () => {
renderTopBar({
wizardChrome: {
flowTitle: 'Ajustar votos',
stepKind: 'entry',
dismissHref: '/campanha',
},
})
expect(screen.getByText('Ajustar votos')).toBeTruthy()
expect(screen.getByRole('link', { name: 'Sair da ação' }).getAttribute('href')).toBe(
'/campanha',
)
expect(screen.queryByRole('link', { name: /Voltar/ })).toBeNull()
})
it('renders wizard continue mode with back link and municipality subtitle', () => {
renderTopBar({
wizardChrome: {
flowTitle: 'Ajustar votos',
municipalityLabel: 'Cairu',
stepKind: 'continue',
previousHref: '/campanha/acoes/atualizar-votos',
dismissHref: '/campanha',
},
})
expect(screen.getByRole('link', { name: /Voltar/ }).getAttribute('href')).toBe(
'/campanha/acoes/atualizar-votos',
)
expect(screen.getByLabelText('Município em atualização: Cairu').textContent).toBe('Cairu')
expect(screen.getByRole('link', { name: 'Sair da ação' })).toBeTruthy()
})
it('renders dismiss on every wizard step and never a skip link (B168)', () => {
renderTopBar({
wizardChrome: {
flowTitle: 'Registrar atualização',
stepKind: 'continue',
previousHref: '/campanha/acoes/atualizar-votos?municipio=cairu',
dismissHref: '/campanha',
},
})
expect(screen.getByRole('link', { name: 'Sair da ação' }).getAttribute('href')).toBe(
'/campanha',
)
expect(screen.queryByRole('link', { name: 'Pular' })).toBeNull()
})
it('keeps wizard mode when chrome updates between steps', () => {
const { rerender } = render(
<SidebarProvider>
<CampaignPageChromeProvider role="coordinator">
<CampaignWizardChromeProvider>
<CampaignHomeSearchChromeProvider>
<WizardChromeProbe
chrome={{
flowTitle: 'Ajustar votos',
stepKind: 'entry',
dismissHref: '/campanha',
}}
/>
<CampaignMobileTopBar />
</CampaignHomeSearchChromeProvider>
</CampaignWizardChromeProvider>
</CampaignPageChromeProvider>
</SidebarProvider>,
)
expect(screen.queryByText('Municípios')).toBeNull()
rerender(
<SidebarProvider>
<CampaignPageChromeProvider role="coordinator">
<CampaignWizardChromeProvider>
<CampaignHomeSearchChromeProvider>
<WizardChromeProbe
chrome={{
flowTitle: 'Ajustar votos',
municipalityLabel: 'Cairu',
stepKind: 'continue',
previousHref: '/campanha/acoes/atualizar-votos',
dismissHref: '/campanha',
}}
/>
<CampaignMobileTopBar />
</CampaignHomeSearchChromeProvider>
</CampaignWizardChromeProvider>
</CampaignPageChromeProvider>
</SidebarProvider>,
)
const topBar = document.querySelector('[data-slot="campaign-mobile-top-bar"]')
expect(topBar?.getAttribute('data-mode')).toBe('wizard')
expect(screen.queryByText('Municípios')).toBeNull()
expect(screen.getByLabelText('Município em atualização: Cairu')).toBeTruthy()
})
})