teqo/tests/unit/municipalityUi.unit.spec.ts
Francisco Ignácio Fontoura Solla 51c13153fc E9: Fila de alocação (déficit como default do staff, frescor, coluna da vergonha)
A lista de municípios vira a fila de decisão, sem rota nova, sem coluna nova
e sem migration.

Fase 0 corrige a meta sintética do E8, que repartia a meta estadual
proporcionalmente só ao teto do campo e ignorava a votação própria (meta 2.911
para Vitória da Conquista, de 5.005 votos em 2022; 813 para Campo Formoso, de
47). `deriveSuggestedGoalsByScenario` substitui `decomposeStateGoal` e ancora a
meta na votação de 2022 por cenário — pessimista = base × (1 − margin), central
= base, otimista = base × (stateGoal ÷ Σ base), com clamp. `goalCoverage` passa
a receber `SuggestedGoalByScenario`, deixando `expectedVotes[S] ??
suggestedGoal[S]` simétrico. Só descrições do global mudaram.

Fase 1 adiciona frescor e ordenações: `declaredAt`/`estimatedAt` no select do
agregado de pledges (`lastPledgeAt`), `lastSignalAt` no view model via
`resolveMunicipalityLastSignalAt` (mesmo helper para o comparador do servidor e
a célula), e as sort keys `deficit` (novo default do staff) e `frescor`,
derivadas em memória no cenário central.

Fase 2 dobra os sinais nas células existentes: "há N dias" na coluna de última
atualização (frio ≥ 21 dias, em âmbar — vermelho já é da prioridade), badge
"sem responsável" em prioridade alta sem assessor, coluna da vergonha no
detalhe da métrica de assessoria, tooltip de cobertura nomeando o cenário e
copy Praças→Municípios.

Cortes: votos em jogo → B13, LQ/captura → E10, coluna dedicada de déficit
desnecessária.
2026-07-25 02:24:17 -03:00

160 lines
7.2 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 {
buildMunicipalityListHref,
buildMunicipalitySortHref,
formatMunicipalityConcentrationHint,
formatMunicipalityListSortSummary,
formatMunicipalitySignalAgeLabel,
isMunicipalitySignalCold,
MUNICIPALITY_COLD_SIGNAL_DAYS,
municipalitySignalAgeInDays,
parseMunicipalityListParams,
parseMunicipalitySortValue,
resolveMunicipalityLastSignalAt,
resolveMunicipalityListSort,
serializeMunicipalitySortValue,
shouldUpdateMunicipalitySearchUrl,
} from '@/utilities/municipalityUi'
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 de identidade ↑',
)
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/)
})
})
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')
})
})