166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { openAction, routerMock } = vi.hoisted(() => ({
|
|
openAction: vi.fn(),
|
|
routerMock: { refresh: vi.fn() },
|
|
}))
|
|
|
|
vi.mock('next/navigation', () => ({
|
|
useRouter: () => routerMock,
|
|
}))
|
|
|
|
vi.mock('@/app/(campaign)/campanha/actions/notifications', () => ({
|
|
openCampaignNotifications: openAction,
|
|
}))
|
|
|
|
import { CampaignNotificationBell } from '@/components/campaign/shell/CampaignNotificationBell'
|
|
import type { NotificationListItem } from '@/lib/notificationContract'
|
|
|
|
const now = new Date().toISOString()
|
|
|
|
const items: NotificationListItem[] = [
|
|
{
|
|
id: 1,
|
|
type: 'municipality_update',
|
|
payload: {
|
|
title: 'Nova atualização — Salvador ZE 01',
|
|
detail: 'Fato de campo registrado',
|
|
href: '/campanha/municipios/salvador-ze-01',
|
|
},
|
|
createdAt: now,
|
|
},
|
|
{
|
|
id: 2,
|
|
type: 'new_supporter',
|
|
payload: {
|
|
title: 'Convite aceito — Maria Souza',
|
|
detail: 'Novo apoiador',
|
|
href: '/campanha/apoiadores',
|
|
},
|
|
createdAt: now,
|
|
},
|
|
]
|
|
|
|
const renderBell = (unreadCount = 2) =>
|
|
render(<CampaignNotificationBell unreadCount={unreadCount} vapidPublicKey={null} />)
|
|
|
|
const matchMediaMock = vi.fn()
|
|
|
|
const stubMatchMedia = (matches: boolean) => {
|
|
matchMediaMock.mockImplementation((query: string) => ({
|
|
matches,
|
|
media: query,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
}))
|
|
vi.stubGlobal('matchMedia', matchMediaMock)
|
|
}
|
|
|
|
beforeAll(() => {
|
|
vi.stubGlobal(
|
|
'ResizeObserver',
|
|
class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
},
|
|
)
|
|
Element.prototype.scrollIntoView = () => {}
|
|
})
|
|
|
|
beforeEach(() => {
|
|
stubMatchMedia(false)
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterAll(() => {
|
|
vi.unstubAllGlobals()
|
|
Reflect.deleteProperty(Element.prototype, 'scrollIntoView')
|
|
})
|
|
|
|
describe('CampaignNotificationBell (C108 — read on open, clean panel, desktop dialog)', () => {
|
|
it('opens the panel, marks everything read in one call and clears the badge', async () => {
|
|
openAction.mockResolvedValue({ status: 'success', items, markedCount: 2 })
|
|
renderBell()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '2 notificações não lidas' }))
|
|
|
|
const dialog = await screen.findByRole('dialog', { name: 'Notificações' })
|
|
expect(await within(dialog).findByText('Nova atualização — Salvador ZE 01')).toBeTruthy()
|
|
expect(await within(dialog).findByText('Convite aceito — Maria Souza')).toBeTruthy()
|
|
expect(openAction).toHaveBeenCalledTimes(1)
|
|
|
|
// Badge zeroes as soon as the open action lands — no extra click. The bell
|
|
// button sits outside the modal, so Radix marks it aria-hidden while the
|
|
// dialog is open and role queries cannot see it — assert on the badge text.
|
|
await waitFor(() => expect(screen.queryByText('2', { exact: true })).toBeNull())
|
|
})
|
|
|
|
it('re-syncs the sibling bell only when something was marked', async () => {
|
|
openAction.mockResolvedValue({ status: 'success', items, markedCount: 2 })
|
|
renderBell()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '2 notificações não lidas' }))
|
|
await screen.findByRole('dialog', { name: 'Notificações' })
|
|
await waitFor(() => expect(routerMock.refresh).toHaveBeenCalledTimes(1))
|
|
})
|
|
|
|
it('renders no title bar, counter, close button or mark-all button', async () => {
|
|
openAction.mockResolvedValue({ status: 'success', items, markedCount: 2 })
|
|
renderBell()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '2 notificações não lidas' }))
|
|
|
|
const dialog = await screen.findByRole('dialog', { name: 'Notificações' })
|
|
expect(within(dialog).queryByRole('button', { name: 'Marcar todas como lidas' })).toBeNull()
|
|
expect(within(dialog).queryByText(/não lidas|Tudo em dia/)).toBeNull()
|
|
// The only heading is the visually-hidden a11y title — the panel has no
|
|
// visible title bar.
|
|
expect(within(dialog).queryAllByRole('heading')).toHaveLength(1)
|
|
})
|
|
|
|
it('shows the empty state when there are no notifications', async () => {
|
|
openAction.mockResolvedValue({ status: 'success', items: [], markedCount: 0 })
|
|
renderBell(0)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Notificações' }))
|
|
|
|
const dialog = await screen.findByRole('dialog', { name: 'Notificações' })
|
|
await within(dialog).findByText('Nenhuma notificação por aqui.')
|
|
expect(routerMock.refresh).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('surfaces the load error instead of marking anything', async () => {
|
|
openAction.mockResolvedValue({ message: 'Não foi possível carregar as notificações.' })
|
|
renderBell()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '2 notificações não lidas' }))
|
|
|
|
const dialog = await screen.findByRole('dialog', { name: 'Notificações' })
|
|
await within(dialog).findByText('Não foi possível carregar as notificações.')
|
|
expect(routerMock.refresh).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('CampaignNotificationBell (C108 — mobile bottom sheet)', () => {
|
|
it('renders the sheet with the same clean panel', async () => {
|
|
stubMatchMedia(true)
|
|
openAction.mockResolvedValue({ status: 'success', items, markedCount: 0 })
|
|
renderBell()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '2 notificações não lidas' }))
|
|
|
|
const sheet = await screen.findByRole('dialog', { name: 'Notificações' })
|
|
expect(await within(sheet).findByText('Nova atualização — Salvador ZE 01')).toBeTruthy()
|
|
expect(within(sheet).queryByRole('button', { name: 'Marcar todas como lidas' })).toBeNull()
|
|
expect(within(sheet).queryAllByRole('heading')).toHaveLength(1)
|
|
})
|
|
})
|