teqo-backup/tests/unit/municipalityList.unit.spec.ts
Francisco Ignácio Fontoura Solla c7001b75f0 E14: engagement levels N0-N4 and the first allocationDecision writer.
`municipality.engagementLevel` (staff-only, no default) records what the
campaign decided to invest in a município, next to the E8/E10 metrics that
describe what it knows. Moving it is a reallocation decision, so only
unrestricted staff may, every move carries a rationale plus the signals that
would reverse it, and the write files an `allocationDecision` in the same
transaction under an advisory lock taken before the current level is read —
otherwise the snapshot's `from` is a guess. The enum gains `movimento`: a level
move is neither `aceita` nor `descarta`.

No backfill. Null is "Sem nível", and filtering by it is the triage queue.
That is also why the list sorts `nivel` in memory rather than in the query:
Postgres puts NULLs first on DESC, so a native sort would open "N4 primeiro"
on a page of municípios nobody has decided about yet. The filter stays native.

The hysteresis rules from research §6.8 are pure and client-safe, so the
Popover explains a block before submitting and the server re-runs them anyway;
a blocked move answers 409 with typed violations and the override is recorded
with exactly the rules it contradicted.
2026-07-27 21:19:33 -03:00

482 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from 'vitest'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
import { formatMunicipalityConcentrationHint } from '@/utilities/municipalityLabels'
import {
applyMunicipalityKindFilter,
buildMunicipalityFilterHref,
buildMunicipalityFilterOptionHref,
formatMunicipalityActiveFiltersSummary,
isMunicipalityColumnFilterActive,
toggleMunicipalityExclusiveFilterValue,
toggleMunicipalityMultiFilterValue,
toggleMunicipalityPriorityFilter,
type MunicipalityMultiFilterParam,
} from '@/utilities/municipalityListFilters'
import {
buildMunicipalityListHref,
buildMunicipalityListWhere,
buildMunicipalitySortHref,
formatMunicipalityListSortSummary,
municipalityListSortLabels,
parseMunicipalityListParams,
parseMunicipalitySortValue,
resolveMunicipalityListSort,
serializeMunicipalitySortValue,
} from '@/utilities/municipalityListUrl'
import {
formatMunicipalitySignalAgeLabel,
isMunicipalitySignalCold,
MUNICIPALITY_COLD_SIGNAL_DAYS,
municipalitySignalAgeInDays,
resolveMunicipalityLastSignalAt,
} from '@/utilities/municipalitySignal'
describe('municipality list sort params (A11 + B15 + E9)', () => {
it('omits the default deficit/desc from the URL (E9 allocation queue order)', () => {
const state = parseMunicipalityListParams({ sort: 'deficit', dir: 'desc' })
expect(state).toMatchObject({ sort: 'deficit', dir: 'desc' })
expect(resolveMunicipalityListSort(state)).toEqual({ sort: 'deficit', dir: 'desc' })
expect(buildMunicipalityListHref(state, 1)).toBe('/campanha/municipios')
})
it('treats an empty query as deficit desc (biggest uncovered goal first)', () => {
const state = parseMunicipalityListParams({})
expect(resolveMunicipalityListSort(state)).toEqual({ sort: 'deficit', dir: 'desc' })
expect(buildMunicipalityListHref(state, 1)).toBe('/campanha/municipios')
})
it('keeps sort=votos in the URL now that it is no longer the default', () => {
const state = parseMunicipalityListParams({ sort: 'votos', dir: 'desc' })
expect(buildMunicipalityListHref(state, 1)).toBe('/campanha/municipios?sort=votos')
})
it('opens frescor on the coldest signal (desc default) and keeps asc explicit', () => {
const base = parseMunicipalityListParams({})
expect(buildMunicipalitySortHref(base, 'frescor')).toBe('/campanha/municipios?sort=frescor')
const cold = parseMunicipalityListParams({ sort: 'frescor' })
expect(resolveMunicipalityListSort(cold)).toEqual({ sort: 'frescor', dir: 'desc' })
expect(buildMunicipalitySortHref(cold, 'frescor')).toBe(
'/campanha/municipios?sort=frescor&dir=asc',
)
})
it('keeps sort=name when sorting alphabetically (dir=asc is the name default)', () => {
const state = parseMunicipalityListParams({ sort: 'name', dir: 'asc' })
expect(state.sort).toBe('name')
expect(state.dir).toBe('asc')
expect(buildMunicipalityListHref(state, 1)).toBe('/campanha/municipios?sort=name')
})
it('keeps sort=votos&dir=asc when ascending', () => {
const state = parseMunicipalityListParams({ sort: 'votos', dir: 'asc' })
expect(state).toMatchObject({ sort: 'votos', dir: 'asc' })
expect(buildMunicipalityListHref(state, 1)).toBe('/campanha/municipios?sort=votos&dir=asc')
})
it('toggles deficit from the default desc to asc', () => {
const base = parseMunicipalityListParams({})
const href = buildMunicipalitySortHref(base, 'deficit')
expect(href).toBe('/campanha/municipios?sort=deficit&dir=asc')
})
it('switches from name back to the default deficit desc (omitted URL)', () => {
const base = parseMunicipalityListParams({ sort: 'name' })
expect(buildMunicipalitySortHref(base, 'deficit')).toBe('/campanha/municipios')
})
it('parses mobile Ordenar values with | and rejects junk', () => {
expect(parseMunicipalitySortValue('votos|desc')).toEqual({ key: 'votos', dir: 'desc' })
expect(parseMunicipalitySortValue('name|asc')).toEqual({ key: 'name', dir: 'asc' })
expect(parseMunicipalitySortValue('priority|asc')).toBeNull()
expect(parseMunicipalitySortValue('votos')).toBeNull()
expect(serializeMunicipalitySortValue('votos', 'desc')).toBe('votos|desc')
})
it('summarizes the active sort in plain language', () => {
expect(formatMunicipalityListSortSummary('votos', 'desc')).toBe('Ordenado por 2022 ↓')
expect(formatMunicipalityListSortSummary('votos', 'asc')).toBe('Ordenado por 2022 ↑')
expect(formatMunicipalityListSortSummary('name', 'asc')).toBe('Ordenado por nome (A–Z)')
expect(formatMunicipalityListSortSummary('region', 'asc')).toBe('Ordenado por Território ↑')
expect(formatMunicipalityListSortSummary('deficit', 'desc')).toBe(
'Ordenado por déficit da meta (maior primeiro)',
)
expect(formatMunicipalityListSortSummary('frescor', 'desc')).toBe(
'Ordenado por frescor (sinal mais frio primeiro)',
)
})
it('formats the concentração header hint once for tooltip/caption', () => {
expect(formatMunicipalityConcentrationHint()).toMatch(/votação estadual/)
expect(formatMunicipalityConcentrationHint()).toMatch(/435/)
expect(formatMunicipalityConcentrationHint()).not.toMatch(/^%/)
expect(formatMunicipalityConcentrationHint(12)).toMatch(/12/)
})
it('labels the advisor-count sort as Assessores (not Cobertura da meta)', () => {
expect(municipalityListSortLabels.coverage).toBe('Assessores')
expect(formatMunicipalityListSortSummary('coverage', 'asc')).toBe('Ordenado por Assessores ↑')
})
it('opens the classe sort on reduto (desc default) and names the direction', () => {
const base = parseMunicipalityListParams({})
expect(buildMunicipalitySortHref(base, 'classe')).toBe('/campanha/municipios?sort=classe')
const byClass = parseMunicipalityListParams({ sort: 'classe' })
expect(resolveMunicipalityListSort(byClass)).toEqual({ sort: 'classe', dir: 'desc' })
expect(formatMunicipalityListSortSummary('classe', 'desc')).toBe(
'Ordenado por classe (reduto primeiro)',
)
expect(formatMunicipalityListSortSummary('classe', 'asc')).toBe(
'Ordenado por classe (marginal primeiro)',
)
})
})
describe('E10 classe territorial filter (derived, not a Payload constraint)', () => {
it('parses repeated ?class= params and keeps them out of the where clause', () => {
const state = parseMunicipalityListParams({ class: ['reduto', 'expansao', 'reduto'] })
expect(state.classes).toEqual(['reduto', 'expansao'])
expect(isMunicipalityColumnFilterActive(state, 'class')).toBe(true)
// The class lives in the committed TSE artifact, so `where` must stay clean —
// `municipalityPageData` applies it in memory over the unpaginated scope.
expect(buildMunicipalityListWhere(state)).toEqual({})
expect(buildMunicipalityFilterHref(state)).toBe(
'/campanha/municipios?class=reduto&class=expansao',
)
})
it('discards unknown classes and treats "todas" as absent', () => {
expect(parseMunicipalityListParams({ class: 'perdida' }).classes).toBeUndefined()
const all = parseMunicipalityListParams({
class: ['reduto', 'expansao', 'manutencao', 'marginal', 'sem_base'],
})
expect(all.classes).toBeUndefined()
expect(isMunicipalityColumnFilterActive(all, 'class')).toBe(false)
expect(formatMunicipalityActiveFiltersSummary(all)).toBeNull()
// Ticking the last unchecked box lands on that same canonical state.
const allButOne = parseMunicipalityListParams({
class: ['reduto', 'expansao', 'manutencao', 'marginal'],
})
expect(
toggleMunicipalityMultiFilterValue(allButOne, 'class', 'sem_base').classes,
).toBeUndefined()
})
it('names the class in the active-filters summary', () => {
expect(
formatMunicipalityActiveFiltersSummary(
parseMunicipalityListParams({ class: ['reduto', 'expansao'] }),
),
).toBe('Classe reduto, expansão')
})
it('toggles a class in and out while preserving the other filters', () => {
const state = parseMunicipalityListParams({ region: 'Irecê', sort: 'votos' })
const on = toggleMunicipalityMultiFilterValue(state, 'class', 'reduto')
expect(on.classes).toEqual(['reduto'])
expect(buildMunicipalityFilterHref(on)).toBe(
'/campanha/municipios?region=Irec%C3%AA&class=reduto&sort=votos',
)
expect(toggleMunicipalityMultiFilterValue(on, 'class', 'reduto').classes).toBeUndefined()
})
})
describe('E14 nível de envolvimento (stored: sort and filter both reach the query)', () => {
it('opens the nivel sort on N4 (desc default) and names the direction', () => {
const base = parseMunicipalityListParams({})
expect(buildMunicipalitySortHref(base, 'nivel')).toBe('/campanha/municipios?sort=nivel')
const byLevel = parseMunicipalityListParams({ sort: 'nivel' })
expect(resolveMunicipalityListSort(byLevel)).toEqual({ sort: 'nivel', dir: 'desc' })
expect(formatMunicipalityListSortSummary('nivel', 'desc')).toBe(
'Ordenado por nível (N4 primeiro)',
)
expect(formatMunicipalityListSortSummary('nivel', 'asc')).toBe(
'Ordenado por nível (N0 primeiro)',
)
})
it('constrains the query by the selected levels', () => {
const state = parseMunicipalityListParams({ level: ['n3', 'n4', 'n3'] })
expect(state.levels).toEqual(['n3', 'n4'])
expect(isMunicipalityColumnFilterActive(state, 'level')).toBe(true)
expect(buildMunicipalityListWhere(state)).toEqual({
and: [{ engagementLevel: { in: ['n3', 'n4'] } }],
})
expect(buildMunicipalityFilterHref(state)).toBe('/campanha/municipios?level=n3&level=n4')
})
/**
* "Sem nível" is the triage queue, so it must be selectable — and absence
* can't ride inside an `in`, which is why it becomes its own OR branch.
*/
it('expresses "sem nível" as absence, alone or beside real levels', () => {
expect(buildMunicipalityListWhere(parseMunicipalityListParams({ level: 'sem_nivel' }))).toEqual(
{ and: [{ engagementLevel: { exists: false } }] },
)
expect(
buildMunicipalityListWhere(parseMunicipalityListParams({ level: ['n0', 'sem_nivel'] })),
).toEqual({
and: [{ or: [{ engagementLevel: { in: ['n0'] } }, { engagementLevel: { exists: false } }] }],
})
})
it('discards unknown levels and treats "todos" as absent', () => {
expect(parseMunicipalityListParams({ level: 'n9' }).levels).toBeUndefined()
const all = parseMunicipalityListParams({
level: ['n0', 'n1', 'n2', 'n3', 'n4', 'sem_nivel'],
})
expect(all.levels).toBeUndefined()
expect(buildMunicipalityListWhere(all)).toEqual({})
expect(isMunicipalityColumnFilterActive(all, 'level')).toBe(false)
const allButOne = parseMunicipalityListParams({
level: ['n0', 'n1', 'n2', 'n3', 'n4'],
})
expect(
toggleMunicipalityMultiFilterValue(allButOne, 'level', 'sem_nivel').levels,
).toBeUndefined()
})
it('names the level in the active-filters summary', () => {
expect(
formatMunicipalityActiveFiltersSummary(
parseMunicipalityListParams({ level: ['n4', 'sem_nivel'] }),
),
).toBe('Nível N4, sem nível')
})
})
describe('municipality list header filters (B16+)', () => {
it('toggles multi region OR filters and preserves sort/dir', () => {
const state = parseMunicipalityListParams({
page: '3',
sort: 'votos',
dir: 'asc',
kind: 'zona',
})
const withRegion = toggleMunicipalityMultiFilterValue(state, 'region', 'Irecê')
expect(withRegion.regions).toEqual(['Irecê'])
expect(withRegion.page).toBe(1)
expect(buildMunicipalityFilterHref(withRegion)).toBe(
'/campanha/municipios?region=Irec%C3%AA&kind=zona&sort=votos&dir=asc',
)
const withTwo = toggleMunicipalityMultiFilterValue(withRegion, 'region', 'Recôncavo')
expect(withTwo.regions).toEqual(['Irecê', 'Recôncavo'])
expect(buildMunicipalityListHref(withTwo, 1)).toContain('region=Irec')
expect(buildMunicipalityListHref(withTwo, 1)).toContain('region=Rec')
const cleared = toggleMunicipalityMultiFilterValue(withRegion, 'region', 'Irecê')
expect(cleared.regions).toBeUndefined()
})
it('toggles the Prioritária filter in and out of the URL, keeping other filters', () => {
const state = parseMunicipalityListParams({ region: 'Irecê' })
const on = toggleMunicipalityPriorityFilter(state)
expect(on.priority).toBe('alta')
expect(buildMunicipalityFilterHref(on)).toBe(
'/campanha/municipios?region=Irec%C3%AA&priority=alta',
)
const off = toggleMunicipalityPriorityFilter(on)
expect(off.priority).toBeUndefined()
expect(buildMunicipalityFilterHref(off)).toBe('/campanha/municipios?region=Irec%C3%AA')
})
it('treats every trend selected as "todas" (no narrowing constraint)', () => {
const one = parseMunicipalityListParams({ trend: 'favoravel' })
expect(one.trends).toEqual(['favoravel'])
expect(isMunicipalityColumnFilterActive(one, 'trend')).toBe(true)
expect(buildMunicipalityListWhere(one)).toEqual({
and: [{ 'politicalTrend.status': { in: ['favoravel'] } }],
})
// "Todas" has a single encoding — absent — so the URL never carries params
// that filter nothing while the funnel reports itself inactive.
const all = parseMunicipalityListParams({ trend: ['favoravel', 'neutra', 'desfavoravel'] })
expect(all.trends).toBeUndefined()
expect(isMunicipalityColumnFilterActive(all, 'trend')).toBe(false)
expect(buildMunicipalityListWhere(all)).toEqual({})
expect(formatMunicipalityActiveFiltersSummary(all)).toBeNull()
expect(buildMunicipalityFilterHref(all)).toBe('/campanha/municipios')
// Ticking the last unchecked box lands on that same canonical state.
const twoSelected = parseMunicipalityListParams({ trend: ['favoravel', 'neutra'] })
expect(
toggleMunicipalityMultiFilterValue(twoSelected, 'trend', 'desfavoravel').trends,
).toBeUndefined()
const none = toggleMunicipalityMultiFilterValue(one, 'trend', 'favoravel')
expect(none.trends).toBeUndefined()
expect(buildMunicipalityFilterHref(none)).toBe('/campanha/municipios')
})
it('toggles coverage by re-clicking the active value', () => {
const state = parseMunicipalityListParams({})
const on = toggleMunicipalityExclusiveFilterValue(state, 'coverage', 'com_assessor')
expect(on.coverage).toBe('com_assessor')
const off = toggleMunicipalityExclusiveFilterValue(on, 'coverage', 'com_assessor')
expect(off.coverage).toBeUndefined()
})
it('discards invalid filter values', () => {
const state = parseMunicipalityListParams({})
expect(
toggleMunicipalityMultiFilterValue(state, 'region', 'NotATerritory').regions,
).toBeUndefined()
expect(applyMunicipalityKindFilter(state, 'bairro').kind).toBeUndefined()
expect(
toggleMunicipalityExclusiveFilterValue(state, 'coverage', 'maybe').coverage,
).toBeUndefined()
expect(toggleMunicipalityMultiFilterValue(state, 'trend', 'alta').trends).toBeUndefined()
expect(parseMunicipalityListParams({ slug: 'not-a-slug', advisor: 'x' }).slugs).toBeUndefined()
expect(parseMunicipalityListParams({ advisor: 'x' }).advisors).toBeUndefined()
})
it('parses repeated advisor and slug params as OR filters', () => {
const slug = municipalityCatalog[0]?.slug
expect(slug).toBeTruthy()
const state = parseMunicipalityListParams({
slug: [slug!, 'also-invalid'],
advisor: ['12', '12', '7'],
})
expect(state.slugs).toEqual([slug])
expect(state.advisors).toEqual([7, 12])
})
it('summarizes active filters for the slim bar', () => {
expect(formatMunicipalityActiveFiltersSummary(parseMunicipalityListParams({}))).toBeNull()
expect(
formatMunicipalityActiveFiltersSummary(
parseMunicipalityListParams({ region: 'Irecê', priority: 'alta', q: 'feira' }),
),
).toBe('Prioritária · Irecê · Busca "feira"')
})
})
describe('E9 frescor do sinal', () => {
const now = new Date('2026-07-24T12:00:00.000Z')
it('takes the latest of the staff update and the pledge dates', () => {
expect(
resolveMunicipalityLastSignalAt('2026-07-01T00:00:00.000Z', '2026-07-10T00:00:00.000Z'),
).toBe('2026-07-10T00:00:00.000Z')
expect(
resolveMunicipalityLastSignalAt('2026-07-20T00:00:00.000Z', '2026-07-10T00:00:00.000Z'),
).toBe('2026-07-20T00:00:00.000Z')
})
it('falls back to whichever side exists, and stays null when neither does', () => {
expect(resolveMunicipalityLastSignalAt(null, '2026-07-10T00:00:00.000Z')).toBe(
'2026-07-10T00:00:00.000Z',
)
expect(resolveMunicipalityLastSignalAt('2026-07-10T00:00:00.000Z', null)).toBe(
'2026-07-10T00:00:00.000Z',
)
expect(resolveMunicipalityLastSignalAt(null, null)).toBeNull()
})
it('floors the age in days and never goes negative on a future timestamp', () => {
expect(municipalitySignalAgeInDays('2026-07-24T00:00:00.000Z', now)).toBe(0)
expect(municipalitySignalAgeInDays('2026-07-23T00:00:00.000Z', now)).toBe(1)
expect(municipalitySignalAgeInDays('2026-07-01T00:00:00.000Z', now)).toBe(23)
expect(municipalitySignalAgeInDays('2026-08-01T00:00:00.000Z', now)).toBe(0)
expect(municipalitySignalAgeInDays(null, now)).toBeNull()
})
it('treats "no signal at all" as cold, alongside anything past the threshold', () => {
expect(isMunicipalitySignalCold(null)).toBe(true)
expect(isMunicipalitySignalCold(MUNICIPALITY_COLD_SIGNAL_DAYS)).toBe(true)
expect(isMunicipalitySignalCold(MUNICIPALITY_COLD_SIGNAL_DAYS - 1)).toBe(false)
expect(isMunicipalitySignalCold(0)).toBe(false)
})
it('labels the age in dense pt-BR copy', () => {
expect(formatMunicipalitySignalAgeLabel(null)).toBe('Sem sinal')
expect(formatMunicipalitySignalAgeLabel(0)).toBe('hoje')
expect(formatMunicipalitySignalAgeLabel(1)).toBe('há 1 dia')
expect(formatMunicipalitySignalAgeLabel(30)).toBe('há 30 dias')
})
})
describe('buildMunicipalityFilterOptionHref (B16+ fast path)', () => {
const [firstSlug, secondSlug, thirdSlug] = municipalityCatalog.map((entry) => entry.slug)
const cases: Array<{
name: string
state: Parameters<typeof buildMunicipalityFilterOptionHref>[0]
param: MunicipalityMultiFilterParam
value: string
}> = [
{
name: 'adds a slug',
state: parseMunicipalityListParams({}),
param: 'slug',
value: firstSlug!,
},
{
name: 'removes a selected slug',
state: parseMunicipalityListParams({ slug: [firstSlug!, secondSlug!] }),
param: 'slug',
value: secondSlug!,
},
{
name: 'keeps other filters and resets the page',
state: parseMunicipalityListParams({
q: 'santa',
page: '3',
sort: 'votos',
slug: [thirdSlug!],
priority: 'alta',
}),
param: 'slug',
value: firstSlug!,
},
{
name: 'sorts advisor ids numerically',
state: parseMunicipalityListParams({ advisor: ['20'] }),
param: 'advisor',
value: '5',
},
{
name: 'adds a region',
state: parseMunicipalityListParams({ region: ['Chapada Diamantina'] }),
param: 'region',
value: 'Sertão do São Francisco',
},
{
name: 'collapses the full trend set to "todas"',
state: parseMunicipalityListParams({ trend: ['favoravel', 'neutra'] }),
param: 'trend',
value: 'desfavoravel',
},
{
name: 'removes a selected trend',
state: parseMunicipalityListParams({ trend: ['favoravel', 'neutra'] }),
param: 'trend',
value: 'neutra',
},
{
name: 'adds "sem nível" beside a level',
state: parseMunicipalityListParams({ level: ['n0'] }),
param: 'level',
value: 'sem_nivel',
},
{
name: 'collapses the full level set to "todos"',
state: parseMunicipalityListParams({ level: ['n0', 'n1', 'n2', 'n3', 'n4'] }),
param: 'level',
value: 'sem_nivel',
},
]
it.each(cases)('matches the canonical toggle+href path: $name', ({ state, param, value }) => {
expect(buildMunicipalityFilterOptionHref(state, param, value)).toBe(
buildMunicipalityFilterHref(toggleMunicipalityMultiFilterValue(state, param, value)),
)
})
})