teqo/tests/unit/municipalityPortfolioCell.unit.spec.ts
Francisco Ignácio Fontoura Solla 77023db99d B34+: scale and DRY after municipality chip lists.
Memoize access reads per req, cache portfolio index, trim chip revalidates, and shrink RSC payload via catalog-backed labels.
2026-07-28 19:49:29 -03:00

109 lines
3.8 KiB
TypeScript

import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { createElement } from 'react'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { MunicipalityPortfolioCell } from '@/components/campaign/shared/MunicipalityPortfolioCell'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
import type { MunicipalityPortfolioIndexEntry } from '@/lib/municipalityPortfolio'
type ToastAction = { label: string; onClick: () => void }
const toastActions: ToastAction[] = []
vi.mock('sonner', () => ({
toast: {
success: (_message: string, options?: { action?: ToastAction }) => {
if (options?.action) toastActions.push(options.action)
},
error: () => {},
},
}))
/**
* Real slugs, because the cell resolves name and território from the static
* catalog: Itaparica is the smallest TI (6 municípios), so assigning all of it
* collapses to one chip and removing that chip is a multi-id delta — which is
* what raises the undo toast. Ilhéus stands in for a later, unrelated edit;
* Itabuna is never assigned and is load-bearing, since without a second entry
* of Litoral Sul left over, Ilhéus alone would collapse into a território chip.
*/
const TERRITORY = 'Itaparica'
const TERRITORY_SLUGS = municipalityCatalog
.filter((entry) => entry.region === TERRITORY)
.map((entry) => entry.slug)
const INDEX: MunicipalityPortfolioIndexEntry[] = [
...TERRITORY_SLUGS.map((slug, position) => ({ id: position + 1, slug })),
{ id: 90, slug: 'ilheus' },
{ id: 91, slug: 'itabuna' },
]
const TERRITORY_IDS = INDEX.slice(0, TERRITORY_SLUGS.length).map((entry) => entry.id)
const ILHEUS_ID = 90
const cell = (municipalityIds: number[]) =>
createElement(MunicipalityPortfolioCell, {
ownerId: 7,
ownerName: 'Maria Souza',
municipalityIds,
municipalityIndex: INDEX,
commitAction: async () => ({ status: 'success' as const }),
drawerTitle: 'Editar municípios',
updateErrorMessage: 'Não foi possível atualizar os municípios.',
})
beforeAll(() => {
vi.stubGlobal(
'ResizeObserver',
class {
observe() {}
unobserve() {}
disconnect() {}
},
)
})
afterEach(() => {
toastActions.length = 0
cleanup()
})
afterAll(() => vi.unstubAllGlobals())
describe('município portfolio cell — optimistic baseline', () => {
/**
* Regression pin for a bug that has now been written twice: the delta itself
* was already applied functionally, but the fallback the updater takes when
* the optimistic value is null read the `municipalityIds` of the render that
* created the closure. The toast's "Desfazer" is the one caller that outlives
* its render, so pressing it after a later edit rebuilt the row from a set
* that predated that edit — and because the reconcile effect only clears on an
* exact match, the row stayed wrong through every later revalidation.
*/
it('undoes a removal against the current set, not the one its render captured', async () => {
const { rerender } = render(cell(TERRITORY_IDS))
// 1. Drop the whole território — a multi-id delta, so it toasts.
fireEvent.click(
screen.getByRole('button', {
name: `Remover ${TERRITORY} — ${TERRITORY_SLUGS.length} municípios`,
}),
)
await waitFor(() => expect(toastActions).toHaveLength(1))
// 2. The write landed and the server re-rendered the row empty.
rerender(cell([]))
// 3. A LATER edit the undo closure knows nothing about.
rerender(cell([ILHEUS_ID]))
expect(screen.getByText('Ilhéus')).toBeTruthy()
// 4. Undo. It must ADD the território back, never rewind the row to step 1.
await act(async () => {
toastActions[0]?.onClick()
})
await waitFor(() => expect(screen.getByText(TERRITORY)).toBeTruthy())
expect(screen.getByText('Ilhéus')).toBeTruthy()
})
})