teqo-backup/tests/unit/campaignNucleusSummaryLayout.unit.spec.ts
Francisco Ignácio Fontoura Solla b035cc4a2f Support multi-municipality and multi-neighborhood nucleus territory.
Replace scalar region/city/neighborhood with hasMany arrays so nuclei can span multiple Bahia municipalities while keeping neighborhoods scoped to a single city.
2026-07-18 18:31:51 -03:00

253 lines
9.3 KiB
TypeScript

import { createElement, type ReactNode } from 'react'
import { cleanup, render, screen, within } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
getCampaignUser: vi.fn(),
getPayload: vi.fn(),
resolveAccessibleNucleusContext: vi.fn(),
getNucleusDetailPageData: vi.fn(),
getNucleusCoordinatorAssignmentPageData: vi.fn(),
getNucleusLeadershipPageData: vi.fn(),
getNucleusUpdatesPageData: vi.fn(),
getNucleusPrimaryContactPageData: vi.fn(),
getCampaignInviteConsentState: vi.fn(),
notFound: vi.fn(() => {
throw new Error('not-found')
}),
NucleusNotFoundError: class NucleusNotFoundError extends Error {},
}))
vi.mock('@payload-config', () => ({ default: {} }))
vi.mock('payload', () => ({ getPayload: mocks.getPayload }))
vi.mock('next/navigation', () => ({
notFound: mocks.notFound,
redirect: vi.fn(),
}))
vi.mock('@/utilities/campaignAuth', () => ({ getCampaignUser: mocks.getCampaignUser }))
vi.mock('@/utilities/campaignInvitePageData', () => ({
getCampaignInviteConsentState: mocks.getCampaignInviteConsentState,
}))
vi.mock('@/utilities/nucleusPageData', () => ({
getNucleusDetailPageData: mocks.getNucleusDetailPageData,
resolveAccessibleNucleusContext: mocks.resolveAccessibleNucleusContext,
NucleusNotFoundError: mocks.NucleusNotFoundError,
}))
vi.mock('@/utilities/nucleusDetailPageData', () => ({
loadNucleusDetailPageData: async (
payload: unknown,
user: unknown,
slug: string,
activeTab: unknown,
) => {
const context = await mocks.resolveAccessibleNucleusContext(payload, user, slug, activeTab)
const coordinatorAssignment = await mocks.getNucleusCoordinatorAssignmentPageData(
payload,
user,
context,
)
return {
context,
view: mocks.getNucleusDetailPageData(context, user),
coordinatorAssignment,
}
},
}))
vi.mock('@/utilities/nucleusCoordinatorAssignmentPageData', () => ({
getNucleusCoordinatorAssignmentPageData: mocks.getNucleusCoordinatorAssignmentPageData,
}))
vi.mock('@/utilities/leadershipPageData', () => ({
getNucleusLeadershipPageData: mocks.getNucleusLeadershipPageData,
}))
vi.mock('@/utilities/nucleusUpdatePageData', () => ({
getNucleusUpdatesPageData: mocks.getNucleusUpdatesPageData,
}))
vi.mock('@/utilities/primaryContactPageData', () => ({
getNucleusPrimaryContactPageData: mocks.getNucleusPrimaryContactPageData,
}))
vi.mock('@/utilities/leadershipUi', () => ({
buildLeadershipPanelHref: vi.fn(() => '/campanha/nucleos/nucleo-centro?tab=leaderships'),
buildLeadershipFilterHref: vi.fn(),
nucleusDetailFocusFallbackId: 'nucleus-detail-heading',
parseLeadershipFilterState: vi.fn(() => ({ page: 1 })),
}))
vi.mock('@/utilities/nucleusUpdateUi', () => ({
buildNucleusUpdateHref: vi.fn(),
parseNucleusUpdateListState: vi.fn(() => ({ page: 1 })),
}))
vi.mock('@/utilities/nucleusUi', () => ({
organizationKindLabels: { territorial: 'Territorial' },
formatNucleusTerritoryLabel: () => 'Metropolitano de Salvador',
}))
vi.mock('@/utilities/phone', () => ({ buildWhatsAppUrl: vi.fn() }))
vi.mock('@/app/(campaign)/campanha/(app)/nucleos/[slug]/coordinatorAssignmentFormActions', () => ({
assignNucleusCoordinatorsFormAction: vi.fn(),
}))
vi.mock('@/app/(campaign)/campanha/(app)/nucleos/[slug]/voteEstimateFormActions', () => ({
confirmVoteEstimateFormAction: vi.fn(),
}))
vi.mock('@/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusUpdateFormActions', () => ({
createNucleusUpdateFormAction: vi.fn(),
}))
vi.mock('@/components/campaign/CoordinatorAssignmentCard', () => ({
CoordinatorAssignmentCard: () =>
createElement('article', { 'data-testid': 'coordinator-assignment-card' }),
}))
vi.mock('@/components/campaign/VoteEstimateCard', () => ({
VoteEstimateCard: () => createElement('article', { 'data-testid': 'vote-estimate-card' }),
}))
vi.mock('@/components/campaign/NucleusActiveTab', () => ({
NucleusActiveTab: () => createElement('div', { 'data-testid': 'nucleus-active-tab' }),
NucleusActiveTabLoading: () => createElement('div', { 'data-testid': 'nucleus-tab-loading' }),
}))
vi.mock('@/components/campaign/NucleusTabNav', () => ({
NucleusTabNav: () => createElement('nav', { 'data-testid': 'nucleus-tab-nav' }),
}))
vi.mock('@/components/campaign/CampaignScopeBadge', () => ({
CampaignScopeBadge: ({ children }: { children: ReactNode }) =>
createElement('div', null, children),
}))
vi.mock('@/components/campaign/TseZoneBadge', () => ({
TseZoneBadge: () => createElement('span'),
}))
vi.mock('@/components/campaign/ArchiveNucleusDialog', () => ({
ArchiveNucleusDialog: () => null,
}))
vi.mock('@/components/campaign/VoteEstimateDialog', () => ({
VoteEstimateDialog: () => null,
}))
vi.mock('@/components/ui/Badge', () => ({
Badge: ({ children }: { children: ReactNode }) => createElement('span', null, children),
}))
vi.mock('@/components/ui/button', () => ({
Button: ({ children }: { children: ReactNode }) => createElement('div', null, children),
}))
import NucleusDetailPage from '@/app/(campaign)/campanha/(app)/nucleos/[slug]/page'
describe('campaign nucleus summary layout', () => {
afterEach(cleanup)
beforeEach(() => {
vi.clearAllMocks()
mocks.getCampaignUser.mockResolvedValue({ id: 9, role: 'coordenador' })
mocks.getPayload.mockResolvedValue({})
mocks.resolveAccessibleNucleusContext.mockResolvedValue({
id: 1,
slug: 'nucleo-centro',
document: {},
})
mocks.getNucleusDetailPageData.mockReturnValue({
kind: 'staff',
id: 1,
slug: 'nucleo-centro',
name: 'Núcleo Centro',
status: 'ativo',
organizationKind: 'territorial',
organizationLabel: null,
neighborhoods: [],
locality: null,
cities: ['Salvador'],
regions: ['Metropolitano de Salvador'],
tseZones: [],
confirmedVoteEstimate: null,
proposedVoteEstimate: null,
proposedVoteEstimateVersion: null,
primaryContactId: null,
tabs: {},
})
mocks.getNucleusCoordinatorAssignmentPageData.mockResolvedValue({
canManage: false,
coordinators: [],
})
mocks.getNucleusLeadershipPageData.mockResolvedValue({
kind: 'staff',
leaderships: [],
totalPages: 0,
})
mocks.getNucleusUpdatesPageData.mockResolvedValue({ totalPages: 0 })
mocks.getNucleusPrimaryContactPageData.mockResolvedValue({ current: null, options: [] })
mocks.getCampaignInviteConsentState.mockResolvedValue({ configured: true })
})
it('renders the two summary cards in logical order before the tab navigation', async () => {
render(
await NucleusDetailPage({
params: Promise.resolve({ slug: 'nucleo-centro' }),
searchParams: Promise.resolve({}),
}),
)
const summary = screen.getByRole('region', { name: 'Resumo operacional' })
const directCards = Array.from(summary.children)
expect(directCards).toHaveLength(2)
expect(directCards[0]).toBe(screen.getByTestId('coordinator-assignment-card'))
expect(directCards[1]).toBe(screen.getByTestId('vote-estimate-card'))
const tabNav = screen.getByTestId('nucleus-tab-nav')
expect(within(summary).queryByTestId('nucleus-tab-nav')).toBeNull()
expect(summary.nextElementSibling).toBe(tabNav)
expect(tabNav.nextElementSibling).toBe(screen.getByTestId('nucleus-active-tab'))
})
it('keeps the responsive summary grid and child sizing constraints', async () => {
render(
await NucleusDetailPage({
params: Promise.resolve({ slug: 'nucleo-centro' }),
searchParams: Promise.resolve({}),
}),
)
const summary = screen.getByRole('region', { name: 'Resumo operacional' })
expect(summary.classList).toContain('grid')
expect(summary.classList).toContain('grid-cols-1')
expect(summary.classList).toContain('lg:grid-cols-2')
expect(summary.classList).toContain('items-stretch')
expect(summary.classList).toContain('min-w-0')
expect(summary.classList).toContain('*:min-w-0')
})
it('maps only typed inaccessible nucleus failures to not-found', async () => {
mocks.resolveAccessibleNucleusContext.mockRejectedValue(new mocks.NucleusNotFoundError())
await expect(
NucleusDetailPage({
params: Promise.resolve({ slug: 'nucleo-inacessivel' }),
searchParams: Promise.resolve({}),
}),
).rejects.toThrow('not-found')
expect(mocks.notFound).toHaveBeenCalledOnce()
expect(mocks.getNucleusLeadershipPageData).not.toHaveBeenCalled()
})
it('propagates resolver and child loader failures to the route error boundary', async () => {
const databaseError = new Error('database unavailable')
mocks.resolveAccessibleNucleusContext.mockRejectedValueOnce(databaseError)
await expect(
NucleusDetailPage({
params: Promise.resolve({ slug: 'nucleo-centro' }),
searchParams: Promise.resolve({}),
}),
).rejects.toBe(databaseError)
expect(mocks.notFound).not.toHaveBeenCalled()
mocks.resolveAccessibleNucleusContext.mockResolvedValue({
id: 1,
slug: 'nucleo-centro',
document: {},
})
mocks.getNucleusCoordinatorAssignmentPageData.mockRejectedValueOnce(databaseError)
await expect(
NucleusDetailPage({
params: Promise.resolve({ slug: 'nucleo-centro' }),
searchParams: Promise.resolve({}),
}),
).rejects.toBe(databaseError)
expect(mocks.notFound).not.toHaveBeenCalled()
})
})