teqo-backup/tests/unit/municipalityList.unit.spec.ts
Francisco Ignácio Fontoura Solla ee44581ba4 Remove Tipo column and list kind filter from municipality list.
Drop the redundant type column, URL param, and filters while keeping municipality.kind for map and dossier; update tests and delivery docs.
2026-07-29 07:42:47 -03:00

481 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/municipality/municipalityLabels'
import {
buildMunicipalityFilterHref,
buildMunicipalityFilterOptionHref,
formatMunicipalityActiveFiltersSummary,
isMunicipalityColumnFilterActive,
toggleMunicipalityExclusiveFilterValue,
toggleMunicipalityMultiFilterValue,
toggleMunicipalityPriorityFilter,
type MunicipalityMultiFilterParam,
} from '@/utilities/municipality/municipalityListFilters'
import {
buildMunicipalityListHref,
buildMunicipalityListWhere,
buildMunicipalitySortHref,
formatMunicipalityListSortSummary,
municipalityListSortLabels,
parseMunicipalityListParams,
parseMunicipalitySortValue,
resolveMunicipalityListSort,
serializeMunicipalitySortValue,
} from '@/utilities/municipality/municipalityListUrl'
import {
formatMunicipalitySignalAgeLabel,
isMunicipalitySignalCold,
MUNICIPALITY_COLD_SIGNAL_DAYS,
municipalitySignalAgeInDays,
resolveMunicipalityLastSignalAt,
} from '@/utilities/municipality/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 Cobertura (maior déficit 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('disambiguates Assessores (coverage) from goal Cobertura (deficit)', () => {
expect(municipalityListSortLabels.coverage).toBe('Assessores')
expect(municipalityListSortLabels.deficit).toBe('Cobertura')
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',
priority: 'alta',
})
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&priority=alta&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(
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)),
)
})
})