mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
O dropdown de adição de municípios (pessoas ×3, assessores, lideranças, dobradinhas) oferece 'Salvador — Todas as zonas' como primeiro hit da busca: um toque adiciona as 19 ZEs num lote, via máquina existente do RelationChipCell (delta otimista, cap/floor, undo, colapso 'Salvador (19)' B178, exact-match C116). - searchMunicipalityPortfolio ganha kind 'city' (NORMALIZED_ZONE_CITIES, derivado do catálogo) com gate de escopo por contagem: o agregado só aparece quando a cidade inteira está no escopo addableIds do ator — a label 'Todas as zonas' nunca mente para assessor com carteira parcial. - hitDescription ganha o branch 'Todas as zonas'; sem migration/access/schema. - Testes: unit busca pura (19 ids, soma, escopo parcial, tudo atribuído, posição), cell render, e2e em campaignPeople (agregado → chip → 19 zonas no DB).
137 lines
5 KiB
TypeScript
137 lines
5 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[], index: MunicipalityPortfolioIndexEntry[] = INDEX) =>
|
|
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()
|
|
})
|
|
})
|
|
|
|
describe('município portfolio cell — Salvador aggregate suggestion (C131)', () => {
|
|
// The default INDEX has no Salvador zones, so this test builds its own: the 19
|
|
// zone entries (the aggregate's search target) plus Ilhéus, so a non-matching
|
|
// sibling proves the suggestion is scoped to the query.
|
|
const SALVADOR_INDEX: MunicipalityPortfolioIndexEntry[] = [
|
|
...municipalityCatalog
|
|
.filter((entry) => entry.city === 'Salvador')
|
|
.map((entry, position) => ({ id: position + 1, slug: entry.slug })),
|
|
{ id: 90, slug: 'ilheus' },
|
|
]
|
|
|
|
it('offers "Salvador — Todas as zonas" as the first suggestion for "salvador"', async () => {
|
|
render(cell([], SALVADOR_INDEX))
|
|
|
|
fireEvent.change(
|
|
screen.getByRole('combobox', {
|
|
name: 'Buscar município, território de identidade ou zona eleitoral',
|
|
}),
|
|
{ target: { value: 'salvador' } },
|
|
)
|
|
|
|
const options = await screen.findAllByRole('option')
|
|
const first = options[0]
|
|
expect(first?.textContent).toContain('Salvador')
|
|
expect(first?.textContent).toContain('Todas as zonas')
|
|
})
|
|
})
|