teqo/tests/unit/territoryOverview.unit.spec.ts
Francisco Solla e75bb1c2f3 feat(campaignTerritories): repintar a lista de Territórios no padrão da lista de Municípios (B175)
- Colunas responsivas ao painel via @container/territory-list (P0→P3), sem scroll horizontal
- Contagem de municípios no nome ("Irecê (20)"); % e votos absorvidos na célula 2022 (hover = válidos)
- Célula 2026 no detalhe de Municípios (valor central + %, faixa de cenários no hover, leitura)
- Colunas de rede só leitura: Assessor, Liderança, Dobradinha (para todo o staff)
- Cobertura oculta por padrão no seletor; labels curtos (2022/2026/Captura/Cobertura)
- loadTerritoryOverview devolve { rows, references }; rollup puro com soma por cenário, hasEstimate e uniões de rede
2026-08-08 23:58:54 -03:00

338 lines
11 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 {
METROPOLITANO_DEMAIS_SUB_ROW_LABEL,
METROPOLITANO_SALVADOR_SUB_ROW_LABEL,
} from '@/lib/metropolitanoTerritoryPeers'
import {
computeTerritoryE12Rollup,
computeTerritoryRollup,
filterTerritoryRows,
flattenTerritoryRows,
meanCaptureRateOfMunicipalities,
sortTerritoryRows,
type TerritoryMunicipalityInput,
} from '@/utilities/territory/territoryOverview'
const emptyGoalCoverage = {
goal: 0,
committed: 0,
coverageRatio: null as number | null,
deficit: 0,
}
const input = (
overrides: Partial<TerritoryMunicipalityInput> &
Pick<TerritoryMunicipalityInput, 'slug' | 'region' | 'city'>,
): TerritoryMunicipalityInput => ({
name: overrides.slug,
kind: 'municipio',
votesByYear: { 2022: 0 },
validVotesByYear: { 2022: 0 },
estimateByScenario: { pessimistic: 0, central: 0, optimistic: 0 },
hasEstimate: false,
advisorIDs: [],
leadershipIDs: [],
stateDeputyIDs: [],
ownVotes2022: 0,
fieldCeiling2022: 0,
goalCoverage: emptyGoalCoverage,
...overrides,
})
const fixture: TerritoryMunicipalityInput[] = [
// Irecê (TI 01) — 2 municípios, 1 com assessor
input({
slug: 'ibipeba',
region: 'Irecê',
city: 'Ibipeba',
votesByYear: { 2014: 10, 2018: 20, 2022: 30 },
validVotesByYear: { 2022: 1000 },
estimateByScenario: { pessimistic: 35, central: 40, optimistic: 45 },
hasEstimate: true,
advisorIDs: [7],
leadershipIDs: [11, 12],
stateDeputyIDs: [1, 2],
ownVotes2022: 30,
fieldCeiling2022: 300,
goalCoverage: { goal: 100, committed: 20, coverageRatio: 0.2, deficit: 80 },
}),
input({
slug: 'irece',
region: 'Irecê',
city: 'Irecê',
votesByYear: { 2014: 5, 2018: 15, 2022: 25 },
validVotesByYear: { 2022: 2000 },
estimateByScenario: { pessimistic: 33, central: 35, optimistic: 37 },
advisorIDs: [],
leadershipIDs: [11],
stateDeputyIDs: [2, 3],
ownVotes2022: 25,
fieldCeiling2022: 100,
goalCoverage: { goal: 50, committed: 10, coverageRatio: 0.2, deficit: 40 },
}),
// Velho Chico (TI 02) — 1 município, com assessor
input({
slug: 'barra',
region: 'Velho Chico',
city: 'Barra',
votesByYear: { 2014: 100, 2018: 110, 2022: 120 },
validVotesByYear: { 2022: 5000 },
estimateByScenario: { pessimistic: 140, central: 150, optimistic: 160 },
hasEstimate: true,
advisorIDs: [7, 8],
ownVotes2022: 120,
fieldCeiling2022: 1200,
}),
// Metropolitano de Salvador (TI 26) — Salvador (1 zona) + Camaçari (demais)
input({
slug: 'salvador-ze-1',
region: 'Metropolitano de Salvador',
city: 'Salvador',
kind: 'zona',
votesByYear: { 2014: 1000, 2018: 1100, 2022: 1200 },
validVotesByYear: { 2022: 40000 },
estimateByScenario: { pessimistic: 1200, central: 1300, optimistic: 1400 },
hasEstimate: true,
advisorIDs: [9],
stateDeputyIDs: [5],
ownVotes2022: 1200,
fieldCeiling2022: 12000,
}),
input({
slug: 'camacari',
region: 'Metropolitano de Salvador',
city: 'Camaçari',
votesByYear: { 2014: 200, 2018: 210, 2022: 220 },
validVotesByYear: { 2022: 8000 },
estimateByScenario: { pessimistic: 240, central: 250, optimistic: 260 },
advisorIDs: [10],
ownVotes2022: 220,
fieldCeiling2022: 2200,
}),
]
describe('computeTerritoryRollup', () => {
const rows = computeTerritoryRollup(fixture)
it('returns one row per TI (3 regions)', () => {
expect(rows.map((row) => row.region).sort()).toEqual(
['Irecê', 'Metropolitano de Salvador', 'Velho Chico'].sort(),
)
})
it('sums votes per year and valid votes 2022', () => {
const irece = rows.find((row) => row.region === 'Irecê')!
expect(irece.votesByYear).toEqual({ 2014: 15, 2018: 35, 2022: 55 })
expect(irece.validVotes2022).toBe(3000)
expect(irece.municipalityCount).toBe(2)
})
it('sums 2026 estimates per scenario', () => {
const irece = rows.find((row) => row.region === 'Irecê')!
expect(irece.estimateByScenario).toEqual({
pessimistic: 68,
central: 75,
optimistic: 82,
})
})
it('flags a territory as having an estimate when any municipality does', () => {
const irece = rows.find((row) => row.region === 'Irecê')!
const metro = rows.find((row) => row.region === 'Metropolitano de Salvador')!
expect(irece.hasEstimate).toBe(true)
expect(metro.hasEstimate).toBe(true)
const allEmpty = computeTerritoryRollup([
input({ slug: 'x', region: 'Velho Chico', city: 'X' }),
])
expect(allEmpty[0].hasEstimate).toBe(false)
})
it('unions advisors, leaderships and state deputies per territory (dedup)', () => {
const irece = rows.find((row) => row.region === 'Irecê')!
const velhoChico = rows.find((row) => row.region === 'Velho Chico')!
const metro = rows.find((row) => row.region === 'Metropolitano de Salvador')!
expect(irece.advisorIDs).toEqual([7])
expect(irece.leadershipIDs).toEqual([11, 12])
expect(irece.stateDeputyIDs).toEqual([1, 2, 3])
expect(velhoChico.advisorIDs).toEqual([7, 8])
expect(metro.advisorIDs).toEqual([9, 10])
})
it('counts municipalities with advisor', () => {
const irece = rows.find((row) => row.region === 'Irecê')!
expect(irece.withAdvisorCount).toBe(1)
})
it('computes % da própria votação as fraction of state total', () => {
const stateTotal2022 = 30 + 25 + 120 + 1200 + 220 // 1395
const velhoChico = rows.find((row) => row.region === 'Velho Chico')!
expect(velhoChico.pctPropriaVotacao).toBeCloseTo(120 / stateTotal2022, 6)
})
it('decomposes Metropolitano into Salvador + Demais RMS sub-rows', () => {
const metro = rows.find((row) => row.region === 'Metropolitano de Salvador')!
expect(metro.subRows).toHaveLength(2)
const salvador = metro.subRows![0]
const demais = metro.subRows![1]
expect(salvador.label).toBe(METROPOLITANO_SALVADOR_SUB_ROW_LABEL)
expect(demais.label).toBe(METROPOLITANO_DEMAIS_SUB_ROW_LABEL)
expect(salvador.municipalityCount).toBe(1)
expect(demais.municipalityCount).toBe(1)
// Sub-rows sum to the Metropolitano total.
expect(salvador.votesByYear[2022] + demais.votesByYear[2022]).toBe(metro.votesByYear[2022])
expect(salvador.estimateByScenario.central + demais.estimateByScenario.central).toBe(
metro.estimateByScenario.central,
)
// Sub-rows carry their own network sets (Salvador zones × Demais RMS).
expect(salvador.advisorIDs).toEqual([9])
expect(demais.advisorIDs).toEqual([10])
})
it('returns zero % when state total is zero', () => {
const empty = computeTerritoryRollup([input({ slug: 'x', region: 'Irecê', city: 'X' })])
expect(empty[0].pctPropriaVotacao).toBe(0)
})
})
describe('sortTerritoryRows', () => {
const rows = computeTerritoryRollup(fixture)
it('sorts by % da própria votação desc by default', () => {
const sorted = sortTerritoryRows(rows, 'pct')
expect(sorted[0].region).toBe('Metropolitano de Salvador')
})
it('keeps Metropolitano sub-rows attached to the parent', () => {
const sorted = sortTerritoryRows(rows, 'municipalities', 'asc')
const metro = sorted.find((row) => row.region === 'Metropolitano de Salvador')!
expect(metro.subRows).toHaveLength(2)
})
it('sorts ascending when dir=asc', () => {
const sorted = sortTerritoryRows(rows, 'votes2022', 'asc')
expect(sorted[0].votesByYear[2022] ?? 0).toBeLessThanOrEqual(
sorted[sorted.length - 1].votesByYear[2022] ?? 0,
)
})
})
describe('flattenTerritoryRows', () => {
it('places Metropolitano sub-rows immediately after their parent', () => {
const rows = sortTerritoryRows(computeTerritoryRollup(fixture), 'region', 'asc')
const flattened = flattenTerritoryRows(rows)
const metroIndex = flattened.findIndex(
(row) => row.variant === 'parent' && row.region === 'Metropolitano de Salvador',
)
expect(flattened.slice(metroIndex, metroIndex + 3).map((row) => row.variant)).toEqual([
'parent',
'sub',
'sub',
])
expect(flattened[metroIndex + 1]).toMatchObject({
variant: 'sub',
parentRegion: 'Metropolitano de Salvador',
label: METROPOLITANO_SALVADOR_SUB_ROW_LABEL,
})
})
})
describe('filterTerritoryRows', () => {
const rows = computeTerritoryRollup(fixture)
it('combines accent-insensitive search and selected regions', () => {
expect(
filterTerritoryRows(rows, {
q: 'irece',
regions: ['Irecê', 'Velho Chico'],
}).map((row) => row.region),
).toEqual(['Irecê'])
})
it('treats com assessor as complete coverage and sem assessor as any gap', () => {
// Velho Chico (1/1) and Metropolitano (Salvador + Camaçari both with advisor) are complete.
expect(
filterTerritoryRows(rows, { coverage: 'com_assessor' }).map((row) => row.region),
).toEqual(['Velho Chico', 'Metropolitano de Salvador'])
// Irecê has only ibipeba with an advisor → a gap.
expect(
filterTerritoryRows(rows, { coverage: 'sem_assessor' }).map((row) => row.region),
).toEqual(['Irecê'])
})
it('keeps Metropolitano sub-rows attached when the parent matches', () => {
const [metropolitano] = filterTerritoryRows(rows, { q: 'metropolitano' })
expect(metropolitano.subRows).toHaveLength(2)
})
})
describe('computeTerritoryE12Rollup (MAUP)', () => {
const rows = computeTerritoryRollup(fixture)
it('uses ratio of aggregates, not mean of ratios, when municipalities differ', () => {
const heterogeneous: TerritoryMunicipalityInput[] = [
input({
slug: 'a',
region: 'Irecê',
city: 'A',
ownVotes2022: 10,
fieldCeiling2022: 100,
goalCoverage: emptyGoalCoverage,
}),
input({
slug: 'b',
region: 'Irecê',
city: 'B',
ownVotes2022: 90,
fieldCeiling2022: 100,
goalCoverage: emptyGoalCoverage,
}),
]
const rollup = computeTerritoryE12Rollup(heterogeneous)
const mean = meanCaptureRateOfMunicipalities(heterogeneous)
expect(rollup.captureRate).toBeCloseTo(0.5, 6)
expect(mean).toBeCloseTo(0.5, 6)
// Same in this symmetric case — use skewed ceilings to diverge.
const skewed: TerritoryMunicipalityInput[] = [
input({
slug: 'low',
region: 'Irecê',
city: 'Low',
ownVotes2022: 10,
fieldCeiling2022: 1000,
goalCoverage: emptyGoalCoverage,
}),
input({
slug: 'high',
region: 'Irecê',
city: 'High',
ownVotes2022: 90,
fieldCeiling2022: 100,
goalCoverage: emptyGoalCoverage,
}),
]
const skewedRollup = computeTerritoryE12Rollup(skewed)
const skewedMean = meanCaptureRateOfMunicipalities(skewed)!
expect(skewedRollup.captureRate).toBeCloseTo(100 / 1100, 6)
expect(skewedMean).not.toBeCloseTo(skewedRollup.captureRate!, 4)
})
it('names the municipality with the largest goal deficit as critical', () => {
const irece = rows.find((row) => row.region === 'Irecê')!
expect(irece.criticalMunicipality).toEqual({
slug: 'ibipeba',
name: 'ibipeba',
deficit: 80,
})
expect(irece.goalCoverage.goal).toBe(150)
expect(irece.goalCoverage.committed).toBe(30)
})
it('computes capture stats on Metropolitano sub-rows', () => {
const metro = rows.find((row) => row.region === 'Metropolitano de Salvador')!
const salvador = metro.subRows![0]
expect(salvador.captureRate).not.toBeNull()
expect(salvador.medianCapture).not.toBeNull()
})
})