teqo/tests/unit/municipalityList.unit.spec.ts
Francisco Solla 14bbee6e00 feat(B176): filtrar municípios por Dobradinha/Liderança/Partido + coluna Dobradinhas ao assessor
Omnibox de /campanha/municipios ganha as dimensões Dobradinha (nome do
contato), Liderança (nome do contato) e Partido (sigla), com recortes
"Sem dobradinha"/"Sem liderança"/"Sem partido" no precedente de
"Sem nível". Params aditivos stateDeputy/leadership/party na URL
congelada; buildMunicipalityListWhere ganha catálogo request-scoped
fail-closed (municipalityRelationSets) que vira as leituras reversa e
cruzada em where — sem migration, paginação nativa mantida; facets
estendidas e o mapa compartilha o catálogo. Coluna Dobradinhas liberada
ao staff (servidor já scoped B37/B157; só o gate de UI mudou).
Extrato do par sentinela+exists:false em campaignListUrl (5 call sites).
2026-08-09 03:19:54 -03:00

647 lines
26 KiB
TypeScript
Raw Permalink 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)),
)
})
})
describe('municipality list relation filters (B176)', () => {
it('canonicalizes stateDeputy/leadership ids ascending with the sentinel last', () => {
const state = parseMunicipalityListParams({
stateDeputy: ['5', 'sem_dobradinha', '3', '5', 'abc'],
leadership: ['sem_lideranca', '2', '8'],
})
expect(state.stateDeputies).toEqual([3, 5, 'sem_dobradinha'])
expect(state.leaderships).toEqual([2, 8, 'sem_lideranca'])
expect(buildMunicipalityListHref(state, 1)).toBe(
'/campanha/municipios?stateDeputy=3&stateDeputy=5&stateDeputy=sem_dobradinha&leadership=2&leadership=8&leadership=sem_lideranca',
)
})
it('parses parties by insertion order, deduped, with the sentinel last', () => {
const state = parseMunicipalityListParams({
party: ['PSD', 'PT', 'PSD', 'sem_partido'],
})
expect(state.parties).toEqual(['PSD', 'PT', 'sem_partido'])
expect(buildMunicipalityListHref(state, 1)).toBe(
'/campanha/municipios?party=PSD&party=PT&party=sem_partido',
)
// The sentinel canonicalizes LAST regardless of where it appeared, so one
// value set maps to one URL (same contract as the relationship dims).
const sentinelFirst = parseMunicipalityListParams({ party: ['sem_partido', 'PT', 'PT'] })
expect(sentinelFirst.parties).toEqual(['PT', 'sem_partido'])
expect(buildMunicipalityListHref(sentinelFirst, 1)).toBe(
'/campanha/municipios?party=PT&party=sem_partido',
)
})
it('serializes the three dimensions together with the rest of the state', () => {
const state = parseMunicipalityListParams({
region: 'Irecê',
stateDeputy: '4',
leadership: ['sem_lideranca', '9'],
party: 'PCdoB',
})
const href = buildMunicipalityListHref(state, 1)
expect(href).toContain('stateDeputy=4')
expect(href).toContain('leadership=9&leadership=sem_lideranca')
expect(href).toContain('party=PCdoB')
// URL → raw plain-record params → parse, mirroring what the page's
// `searchParams` object carries into `parseMunicipalityListParams`.
const query = new URLSearchParams(href.split('?')[1])
const raw: Record<string, string | string[] | undefined> = {}
for (const key of query.keys()) {
const values = query.getAll(key)
raw[key] = values.length === 1 ? values[0] : values
}
expect(parseMunicipalityListParams(raw)).toEqual(state)
})
it('builds the direct dobradinha where (in / exists), no catalog needed', () => {
expect(buildMunicipalityListWhere(parseMunicipalityListParams({ stateDeputy: '4' }))).toEqual({
and: [{ stateDeputies: { in: [4] } }],
})
expect(
buildMunicipalityListWhere(parseMunicipalityListParams({ stateDeputy: 'sem_dobradinha' })),
).toEqual({ and: [{ stateDeputies: { exists: false } }] })
expect(
buildMunicipalityListWhere(
parseMunicipalityListParams({ stateDeputy: ['4', 'sem_dobradinha'] }),
),
).toEqual({
and: [{ or: [{ stateDeputies: { in: [4] } }, { stateDeputies: { exists: false } }] }],
})
})
const catalog = {
leadershipMunicipalityIDsByLeadership: new Map([
[1, [10, 11]],
[2, [12]],
]),
allLeadershipMunicipalityIDs: new Set([10, 11, 12]),
stateDeputyIDsByParty: new Map([
['PT', [4, 5]],
['PSD', [6]],
]),
allPartyStateDeputyIDs: new Set([4, 5, 6]),
}
it('builds the reverse leadership where from the catalog (in / not_in)', () => {
expect(
buildMunicipalityListWhere(parseMunicipalityListParams({ leadership: ['1', '2'] }), catalog),
).toEqual({ and: [{ id: { in: [10, 11, 12] } }] })
expect(
buildMunicipalityListWhere(
parseMunicipalityListParams({ leadership: 'sem_lideranca' }),
catalog,
),
).toEqual({ and: [{ id: { not_in: [10, 11, 12] } }] })
})
it('builds the cross party where from the catalog (in / not_in)', () => {
expect(
buildMunicipalityListWhere(parseMunicipalityListParams({ party: ['PT'] }), catalog),
).toEqual({ and: [{ stateDeputies: { in: [4, 5] } }] })
expect(
buildMunicipalityListWhere(parseMunicipalityListParams({ party: 'sem_partido' }), catalog),
).toEqual({ and: [{ stateDeputies: { not_in: [4, 5, 6] } }] })
expect(
buildMunicipalityListWhere(
parseMunicipalityListParams({ party: ['PT', 'sem_partido'] }),
catalog,
),
).toEqual({
and: [{ or: [{ stateDeputies: { in: [4, 5] } }, { stateDeputies: { not_in: [4, 5, 6] } }] }],
})
})
it('fails closed: leadership/party filter without the catalog throws', () => {
expect(() =>
buildMunicipalityListWhere(parseMunicipalityListParams({ leadership: '1' })),
).toThrow(/missing relation catalog/)
expect(() => buildMunicipalityListWhere(parseMunicipalityListParams({ party: 'PT' }))).toThrow(
/missing relation catalog/,
)
// Dobradinha (direct) never needs the catalog.
expect(() =>
buildMunicipalityListWhere(parseMunicipalityListParams({ stateDeputy: '4' })),
).not.toThrow()
})
it('summarizes the three dimensions in the active-filters summary', () => {
expect(
formatMunicipalityActiveFiltersSummary(
parseMunicipalityListParams({ stateDeputy: ['1', '2'] }),
),
).toBe('2 dobradinhas')
expect(
formatMunicipalityActiveFiltersSummary(
parseMunicipalityListParams({ stateDeputy: ['1', 'sem_dobradinha'] }),
),
).toBe('1 dobradinha + sem dobradinha')
expect(
formatMunicipalityActiveFiltersSummary(
parseMunicipalityListParams({ leadership: 'sem_lideranca' }),
),
).toBe('Sem liderança')
expect(
formatMunicipalityActiveFiltersSummary(
parseMunicipalityListParams({ party: ['PT', 'PSD', 'sem_partido'] }),
),
).toBe('Partido PT, PSD + sem partido')
})
it('keeps the B176 toggles on the canonical fast-path href', () => {
const state = parseMunicipalityListParams({ party: 'PT' })
expect(buildMunicipalityFilterOptionHref(state, 'stateDeputy', '5')).toBe(
buildMunicipalityFilterHref(toggleMunicipalityMultiFilterValue(state, 'stateDeputy', '5')),
)
expect(buildMunicipalityFilterOptionHref(state, 'leadership', 'sem_lideranca')).toBe(
buildMunicipalityFilterHref(
toggleMunicipalityMultiFilterValue(state, 'leadership', 'sem_lideranca'),
),
)
expect(buildMunicipalityFilterOptionHref(state, 'party', 'PSD')).toBe(
buildMunicipalityFilterHref(toggleMunicipalityMultiFilterValue(state, 'party', 'PSD')),
)
})
})