mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
273 lines
9.2 KiB
TypeScript
273 lines
9.2 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { bahiaMunicipalities } from '@/lib/bahiaTerritories'
|
|
import { computeIdentityKey, normalizeIdentityPart } from '@/lib/electionCandidateIdentity'
|
|
import type { CandidateVoteRow, TseDetalheApuracaoRow } from '@/lib/electionResults'
|
|
import {
|
|
assertAllCanonicalMunicipalitiesResolvable,
|
|
BASELINE_TICKET_2022,
|
|
canonicalizeMunicipalityName,
|
|
computeWinnersByScope,
|
|
mergeTallyWithWinners,
|
|
normalizeMunicipalityKey,
|
|
parseElectedStatus,
|
|
UnknownMunicipalityError,
|
|
winnerKey,
|
|
} from '@/lib/electionResults'
|
|
import { FEDERAL_ONLY_OFFICES } from '@/lib/electionResultsBuild'
|
|
import { mergeDuplicateTallyRows, mergeDuplicateVoteRows } from '@/lib/electionResultsParse'
|
|
import {
|
|
loadTseFixtureResults,
|
|
loadTseFixtureResultsForYear,
|
|
TSE_FIXTURE_EXPECTED,
|
|
} from '../helpers/tseFixtures'
|
|
|
|
describe('electionResults municipality mapping', () => {
|
|
it('resolves every canonical Bahia municipality through itself', () => {
|
|
expect(bahiaMunicipalities).toHaveLength(417)
|
|
expect(() => assertAllCanonicalMunicipalitiesResolvable()).not.toThrow()
|
|
})
|
|
|
|
it('maps TSE ALL-CAPS names to canonical names', () => {
|
|
expect(canonicalizeMunicipalityName('SALVADOR')).toBe('Salvador')
|
|
expect(canonicalizeMunicipalityName('FEIRA DE SANTANA')).toBe('Feira de Santana')
|
|
expect(canonicalizeMunicipalityName("DIAS D'AVILA")).toBe("Dias d'Ávila")
|
|
expect(canonicalizeMunicipalityName('XIQUE-XIQUE')).toBe('Xique-Xique')
|
|
})
|
|
|
|
it('maps known TSE spelling aliases to canonical names', () => {
|
|
expect(canonicalizeMunicipalityName('CAMACÃ')).toBe('Camacan')
|
|
expect(canonicalizeMunicipalityName('DIAS D ÁVILA')).toBe("Dias d'Ávila")
|
|
expect(canonicalizeMunicipalityName('MUQUÉM DO SÃO FRANCISCO')).toBe('Muquém de São Francisco')
|
|
expect(canonicalizeMunicipalityName('SANTA TEREZINHA')).toBe('Santa Teresinha')
|
|
})
|
|
|
|
it('fails closed on unknown municipality names', () => {
|
|
expect(() => canonicalizeMunicipalityName('CIDADE INEXISTENTE')).toThrow(
|
|
UnknownMunicipalityError,
|
|
)
|
|
})
|
|
|
|
it('normalizes accent and apostrophe variants to the same key', () => {
|
|
expect(normalizeMunicipalityKey("Dias d'Ávila")).toBe(normalizeMunicipalityKey("DIAS D'AVILA"))
|
|
})
|
|
})
|
|
|
|
describe('electionResults elected status', () => {
|
|
it('maps DS_SIT_TOT_TURNO variants', () => {
|
|
expect(parseElectedStatus('ELEITO')).toEqual({ elected: true, electedBy: 'QP' })
|
|
expect(parseElectedStatus('ELEITO POR MÉDIA')).toEqual({ elected: true, electedBy: 'média' })
|
|
expect(parseElectedStatus('ELEITO EM 2º TURNO')).toEqual({
|
|
elected: true,
|
|
electedBy: '2º turno',
|
|
})
|
|
expect(parseElectedStatus('2º TURNO')).toEqual({ elected: false, electedBy: null })
|
|
expect(parseElectedStatus('NÃO ELEITO')).toEqual({ elected: false, electedBy: null })
|
|
expect(parseElectedStatus('#NULO#')).toEqual({ elected: false, electedBy: null })
|
|
})
|
|
})
|
|
|
|
describe('electionCandidateIdentity', () => {
|
|
it('is stable for equivalent normalized inputs', () => {
|
|
const a = computeIdentityKey({
|
|
urnaName: 'Jorge Solla',
|
|
birthCity: 'Salvador',
|
|
birthState: 'BA',
|
|
party: 'PT',
|
|
})
|
|
const b = computeIdentityKey({
|
|
urnaName: 'JORGE SOLLA',
|
|
birthCity: 'SALVADOR',
|
|
birthState: 'ba',
|
|
party: 'pt',
|
|
})
|
|
expect(a).toBe(b)
|
|
expect(a).toHaveLength(64)
|
|
})
|
|
|
|
it('changes when party changes', () => {
|
|
const base = {
|
|
urnaName: 'LULA',
|
|
birthCity: 'GARANHUNS',
|
|
birthState: 'PE',
|
|
party: 'PT',
|
|
}
|
|
expect(computeIdentityKey(base)).not.toBe(computeIdentityKey({ ...base, party: 'PL' }))
|
|
})
|
|
|
|
it('strips accents in identity parts', () => {
|
|
expect(normalizeIdentityPart('Jerônimo')).toBe('JERONIMO')
|
|
})
|
|
})
|
|
|
|
describe('electionResults fixture parse + winners', () => {
|
|
const built = loadTseFixtureResults()
|
|
|
|
it('builds expected vote totals and skips zero-vote rows', () => {
|
|
expect(built.unknownMunicipalities).toEqual([])
|
|
expect(built.votes).toHaveLength(TSE_FIXTURE_EXPECTED.voteRowCount)
|
|
expect(built.tallies).toHaveLength(TSE_FIXTURE_EXPECTED.tallyRowCount)
|
|
|
|
const sollaVotes = built.votes
|
|
.filter(
|
|
(row) =>
|
|
row.office === 'deputado_federal' &&
|
|
row.candidateNumber === BASELINE_TICKET_2022.candidate.candidateNumber,
|
|
)
|
|
.reduce((sum, row) => sum + row.votes, 0)
|
|
expect(sollaVotes).toBe(TSE_FIXTURE_EXPECTED.sollaVotesTotal)
|
|
})
|
|
|
|
it('marks Solla, Lula and Jerônimo as elected', () => {
|
|
const solla = built.candidates.find(
|
|
(c) =>
|
|
c.office === 'deputado_federal' &&
|
|
c.candidateNumber === BASELINE_TICKET_2022.candidate.candidateNumber,
|
|
)
|
|
const lula = built.candidates.find(
|
|
(c) =>
|
|
c.office === 'presidente' &&
|
|
c.candidateNumber === BASELINE_TICKET_2022.president.candidateNumber &&
|
|
c.turn === '2',
|
|
)
|
|
const jeronimo = built.candidates.find(
|
|
(c) =>
|
|
c.office === 'governador' &&
|
|
c.candidateNumber === BASELINE_TICKET_2022.governor.candidateNumber &&
|
|
c.turn === '2',
|
|
)
|
|
|
|
expect(solla?.elected).toBe(true)
|
|
expect(lula?.elected).toBe(true)
|
|
expect(lula?.electedBy).toBe('2º turno')
|
|
expect(jeronimo?.elected).toBe(true)
|
|
expect(jeronimo?.electedBy).toBe('2º turno')
|
|
expect(lula?.state).toBe('BA')
|
|
})
|
|
|
|
it('computes local winners per city+zone', () => {
|
|
const winners = computeWinnersByScope(built.votes)
|
|
const z1 = winners.get(winnerKey('38490', 1, 'deputado_federal', '1'))
|
|
const z2 = winners.get(winnerKey('38490', 2, 'deputado_federal', '1'))
|
|
expect(z1?.winnerCandidateNumber).toBe(
|
|
TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ1.candidateNumber,
|
|
)
|
|
expect(z1?.winnerVotes).toBe(TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ1.votes)
|
|
expect(z2?.winnerCandidateNumber).toBe(
|
|
TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ2.candidateNumber,
|
|
)
|
|
expect(z2?.winnerVotes).toBe(TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ2.votes)
|
|
|
|
const merged = mergeTallyWithWinners(built.tallies, winners)
|
|
const federalZ1 = merged.find(
|
|
(t) => t.office === 'deputado_federal' && t.zoneNumber === 1 && t.cityCode === '38490',
|
|
)
|
|
expect(federalZ1?.winnerCandidateName).toBe('JORGE SOLLA')
|
|
})
|
|
|
|
it('filters to federal deputy only when offices are restricted (E2 historical seed)', () => {
|
|
const built = loadTseFixtureResultsForYear(2018, FEDERAL_ONLY_OFFICES)
|
|
|
|
expect(built.votes.every((row) => row.office === 'deputado_federal')).toBe(true)
|
|
expect(built.tallies.every((row) => row.office === 'deputado_federal')).toBe(true)
|
|
expect(built.candidates.every((row) => row.office === 'deputado_federal')).toBe(true)
|
|
expect(built.votes.every((row) => row.year === 2018)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('electionResults "voto em trânsito" merge (2014 presidente duplicate rows)', () => {
|
|
const baseVote: CandidateVoteRow = {
|
|
year: 2014,
|
|
office: 'presidente',
|
|
turn: '1',
|
|
state: 'BA',
|
|
cityCode: '99999',
|
|
cityName: 'Feira de Santana',
|
|
zoneNumber: 154,
|
|
candidateNumber: 13,
|
|
candidateName: 'DILMA',
|
|
coalition: null,
|
|
party: 'PT',
|
|
voteType: 'nominal',
|
|
votes: 100,
|
|
}
|
|
|
|
it('sums votes for rows sharing the same scope/zone/candidate key (the trânsito split)', () => {
|
|
const merged = mergeDuplicateVoteRows([baseVote, { ...baseVote, votes: 40 }])
|
|
|
|
expect(merged).toHaveLength(1)
|
|
expect(merged[0].votes).toBe(140)
|
|
})
|
|
|
|
it('keeps rows with a different key separate (no false merge across zones/candidates)', () => {
|
|
const otherZone = { ...baseVote, zoneNumber: 155, votes: 10 }
|
|
const otherCandidate = { ...baseVote, candidateNumber: 45, votes: 20 }
|
|
|
|
const merged = mergeDuplicateVoteRows([baseVote, otherZone, otherCandidate])
|
|
|
|
expect(merged).toHaveLength(3)
|
|
expect(merged.reduce((sum, row) => sum + row.votes, 0)).toBe(130)
|
|
})
|
|
|
|
it('is a no-op when there is nothing to merge (2018/2022 shape)', () => {
|
|
const rows = [baseVote, { ...baseVote, zoneNumber: 1 }]
|
|
expect(mergeDuplicateVoteRows(rows)).toEqual(rows)
|
|
})
|
|
|
|
const baseTally: TseDetalheApuracaoRow = {
|
|
year: 2014,
|
|
office: 'presidente',
|
|
turn: '1',
|
|
state: 'BA',
|
|
cityCode: '99999',
|
|
cityName: 'Feira de Santana',
|
|
zoneNumber: 154,
|
|
aptos: 300,
|
|
comparecimento: 200,
|
|
abstencoes: 100,
|
|
votosValidos: 180,
|
|
votosNominaisValidos: 175,
|
|
votosLegenda: 5,
|
|
votosBranco: 10,
|
|
votosNulo: 10,
|
|
votosAnulados: 0,
|
|
}
|
|
|
|
it('sums every count field for duplicate tally rows sharing the same scope key', () => {
|
|
const merged = mergeDuplicateTallyRows([
|
|
baseTally,
|
|
{
|
|
...baseTally,
|
|
aptos: 50,
|
|
comparecimento: 40,
|
|
abstencoes: 10,
|
|
votosValidos: 35,
|
|
votosNominaisValidos: 33,
|
|
votosLegenda: 2,
|
|
votosBranco: 3,
|
|
votosNulo: 2,
|
|
votosAnulados: 0,
|
|
},
|
|
])
|
|
|
|
expect(merged).toHaveLength(1)
|
|
expect(merged[0]).toMatchObject({
|
|
aptos: 350,
|
|
comparecimento: 240,
|
|
abstencoes: 110,
|
|
votosValidos: 215,
|
|
votosNominaisValidos: 208,
|
|
votosLegenda: 7,
|
|
votosBranco: 13,
|
|
votosNulo: 12,
|
|
votosAnulados: 0,
|
|
})
|
|
})
|
|
|
|
it('is a no-op when zones differ', () => {
|
|
const rows = [baseTally, { ...baseTally, zoneNumber: 155 }]
|
|
expect(mergeDuplicateTallyRows(rows)).toEqual(rows)
|
|
})
|
|
})
|