teqo-backup/tests/unit/municipalityList.unit.spec.ts
Francisco Ignácio Fontoura Solla 03bf54dea2 Pass 2 W1-D1: sistema de listas (column defs + CampaignTable) + split do municipalityUi + B16+
- CampaignTable/CampaignTableHead: colunas como dado (id, head island, cell, seams B17 mandatory/defaultVisible); colSpan derivado
- CampaignListFooter: contagem + paginacao compartilhadas
- MunicipalityList reescrita sobre column defs; cards mobile inalterados
- municipalityUi.ts (765/59 exports) dividido: municipalityListUrl / municipalityListFilters / municipalityLabels / municipalitySignal; contrato de URL byte-identico (pins verdes)
- B16+ absorvido: useOptimistic no HeaderFilter (onNavigate dentro da transition), hrefs de opcao via serializador canonico confiavel (1 serializacao por opcao, pin de byte-igualdade), facet de municipio como slugs rotulados no cliente (-20KB), facets no mesmo Promise.all (seed por promise), catalogo DRY, clearMunicipalityListFilters
- timeout explicito no int test de facets (2 bundles full-scope sob suite 47-way)
2026-07-25 14:13:04 -03:00

349 lines
15 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,
shouldUpdateMunicipalitySearchUrl,
} from '@/utilities/municipalityListUrl'
import {
formatMunicipalitySignalAgeLabel,
isMunicipalitySignalCold,
MUNICIPALITY_COLD_SIGNAL_DAYS,
municipalitySignalAgeInDays,
resolveMunicipalityLastSignalAt,
} from '@/utilities/municipalitySignal'
describe('shouldUpdateMunicipalitySearchUrl', () => {
it('returns false when canonical q matches the current URL q', () => {
expect(shouldUpdateMunicipalitySearchUrl('salvador', 'salvador')).toBe(false)
expect(shouldUpdateMunicipalitySearchUrl(' salvador ', 'salvador')).toBe(false)
})
it('returns true when q differs or is newly set', () => {
expect(shouldUpdateMunicipalitySearchUrl('salv', 'salvador')).toBe(true)
expect(shouldUpdateMunicipalitySearchUrl('salvador', undefined)).toBe(true)
expect(shouldUpdateMunicipalitySearchUrl('', 'salvador')).toBe(true)
})
})
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 ↑')
})
})
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',
},
]
it.each(cases)('matches the canonical toggle+href path: $name', ({ state, param, value }) => {
expect(buildMunicipalityFilterOptionHref(state, param, value)).toBe(
buildMunicipalityFilterHref(toggleMunicipalityMultiFilterValue(state, param, value)),
)
})
})