teqo/tests/unit/municipalityProximity.unit.spec.ts
Francisco Ignácio Fontoura Solla 5ace1a04b2 simplify(B14): corrige o mecanismo da fronteira, estado unsupported e duplicatas
Terceira rodada de /simplify sobre o B14, com dois enunciados errados corrigidos:

- A propriedade da fronteira compartilhada NAO vem da ordem do `.find()`, e sim
  da convencao meio-aberta do ray casting (`point.lng < crossingLng`): a aresta
  pertence ao poligono cujo interior fica a leste (ao norte, se horizontal). Tres
  comentarios diziam o contrario, e o do unit test dizia "oeste" enquanto
  afirmava `east`.
- O plano dizia que o `Promise.all` esconde o decode da malha atras do fix de
  GPS. Nao esconde: o decode roda no topo do modulo, quando o chunk executa. O
  paralelo continua certo por outro motivo (decode ~2 ms; sequencial jogaria os
  42 KB no caminho critico quando o fix vem do cache).

Simplificacoes: `unsupported` virou estado proprio, entao `failureCopy` deixa de
ser anulavel e caem duas leituras defensivas inalcancaveis; "Voce esta em " era
escrito duas vezes (frase sr-only + JSX), o que anulava justamente a invariante
que o comentario alegava; `zoneCityHrefs` virou `Partial` porque a busca falha
para os 416 municipios sem zona e o guard existia sem o tipo exigir;
`MunicipalityForIbgeIndex` era duplicata campo-a-campo de `AccessibleMunicipality`;
`kmFormatter` duplicava `oneDecimalFormatter`; `RecentlyVisitedCard` carregava
tres props que nenhum caller passa (dez condicionais mortas); e o bbox de feature
estava escrito em dois specs (agora `tests/helpers/featureBounds.ts`, compartilhado
por vitest e Playwright).

Copy: fora da Bahia sem nada por perto dizia a mesma coisa em duas frases e nunca
dizia que voce estava fora do estado. Agora o headline afirma o fato e o detalhe
reusa "Nenhum municipio da sua carteira por perto.".

Tambem limpa um achado de knip herdado do main (`MunicipalityPoliticalTrendViewModel`
exportado sem uso) e registra no plano o caminho afiado do debito de payload RSC
mais dois adiados com gatilho.
2026-07-26 18:03:44 -03:00

300 lines
9.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { BahiaMunicipalityFeature } from '@/lib/bahiaGeometriesTypes'
import {
featureCentroid,
featureContainsPoint,
findContainingMunicipality,
formatDistanceKm,
haversineKm,
resolveNearbyMunicipality,
type AccessibleMunicipality,
} from '@/lib/municipalityProximity'
/** Axis-aligned square in degrees — small enough that planar reasoning holds. */
const square = (
codarea: string,
name: string,
{ west, south, size }: { west: number; south: number; size: number },
): BahiaMunicipalityFeature => ({
type: 'Feature',
properties: { codarea, name },
geometry: {
type: 'Polygon',
coordinates: [
[
[west, south],
[west + size, south],
[west + size, south + size],
[west, south + size],
[west, south],
],
],
},
})
const accessible = (slug: string, name: string, ibgeCode: string): AccessibleMunicipality => ({
slug,
name,
ibgeCode,
})
describe('municipalityProximity', () => {
describe('haversineKm', () => {
it('measures a known Bahia leg (Salvador → Feira de Santana ≈ 100 km)', () => {
const distance = haversineKm(
{ lat: -12.9714, lng: -38.5014 },
{ lat: -12.2664, lng: -38.9663 },
)
expect(distance).toBeGreaterThan(90)
expect(distance).toBeLessThan(110)
})
it('is zero for the same point', () => {
expect(haversineKm({ lat: -12, lng: -38 }, { lat: -12, lng: -38 })).toBe(0)
})
})
describe('featureContainsPoint', () => {
const cell = square('1', 'Quadrado', { west: -40, south: -13, size: 2 })
it('accepts an interior point and rejects an exterior one', () => {
expect(featureContainsPoint(cell, { lat: -12, lng: -39 })).toBe(true)
expect(featureContainsPoint(cell, { lat: -12, lng: -35 })).toBe(false)
})
it('treats a hole as outside', () => {
const donut: BahiaMunicipalityFeature = {
type: 'Feature',
properties: { codarea: '2', name: 'Rosca' },
geometry: {
type: 'Polygon',
coordinates: [
[
[-40, -13],
[-38, -13],
[-38, -11],
[-40, -11],
[-40, -13],
],
[
[-39.5, -12.5],
[-38.5, -12.5],
[-38.5, -11.5],
[-39.5, -11.5],
[-39.5, -12.5],
],
],
},
}
expect(featureContainsPoint(donut, { lat: -12, lng: -39 })).toBe(false)
expect(featureContainsPoint(donut, { lat: -12.8, lng: -39.8 })).toBe(true)
})
it('accepts a point in any part of a multipolygon', () => {
const island: BahiaMunicipalityFeature = {
type: 'Feature',
properties: { codarea: '3', name: 'Ilhas' },
geometry: {
type: 'MultiPolygon',
coordinates: [
[
[
[-40, -13],
[-39, -13],
[-39, -12],
[-40, -12],
[-40, -13],
],
],
[
[
[-38, -13],
[-37, -13],
[-37, -12],
[-38, -12],
[-38, -13],
],
],
],
},
}
expect(featureContainsPoint(island, { lat: -12.5, lng: -39.5 })).toBe(true)
expect(featureContainsPoint(island, { lat: -12.5, lng: -37.5 })).toBe(true)
expect(featureContainsPoint(island, { lat: -12.5, lng: -38.5 })).toBe(false)
})
})
describe('findContainingMunicipality', () => {
const west = square('1', 'Oeste', { west: -40, south: -13, size: 1 })
const east = square('2', 'Leste', { west: -39, south: -13, size: 1 })
it('gives a shared border to exactly one município', () => {
const features = [west, east]
const onBorder = { lat: -12.5, lng: -39 }
const matches = features.filter((feature) => featureContainsPoint(feature, onBorder))
// Exactly one, and it is the eastern feature: the half-open ray test counts
// only crossings strictly east of the point, so a shared edge belongs to the
// polygon whose interior lies east of it. Scan order never gets a say.
expect(matches).toHaveLength(1)
expect(findContainingMunicipality(features, onBorder)).toBe(east)
})
it('returns undefined outside every feature', () => {
expect(findContainingMunicipality([west, east], { lat: 0, lng: 0 })).toBeUndefined()
})
})
describe('featureCentroid', () => {
it('returns the centre of a square', () => {
const centroid = featureCentroid(square('1', 'Quadrado', { west: -40, south: -13, size: 2 }))
expect(centroid.lng).toBeCloseTo(-39, 6)
expect(centroid.lat).toBeCloseTo(-12, 6)
})
it('weights multipolygon parts by area', () => {
const lopsided: BahiaMunicipalityFeature = {
type: 'Feature',
properties: { codarea: '1', name: 'Desigual' },
geometry: {
type: 'MultiPolygon',
coordinates: [
[
[
[-40, -13],
[-36, -13],
[-36, -9],
[-40, -9],
[-40, -13],
],
],
[
[
[-35, -13],
[-34, -13],
[-34, -12],
[-35, -12],
[-35, -13],
],
],
],
},
}
// Big part centred at -38, small part at -34.5: area 16 vs 1.
expect(featureCentroid(lopsided).lng).toBeCloseTo((-38 * 16 + -34.5) / 17, 6)
})
})
describe('resolveNearbyMunicipality', () => {
const seabra = square('2929206', 'Seabra', { west: -42, south: -13, size: 1 })
const salvador = square('2927408', 'Salvador', { west: -38.6, south: -13, size: 0.4 })
const jequie = square('2918001', 'Jequié', { west: -41, south: -13.4, size: 0.6 })
const features = [seabra, salvador, jequie]
// The real caller hands over the loaded mesh module, index included.
const geometry = {
features,
getMunicipalityFeature: (codarea: string) =>
features.find((feature) => feature.properties.codarea === codarea),
}
it('resolves a município in scope', () => {
const seabraEntry = accessible('seabra', 'Seabra', '2929206')
expect(
resolveNearbyMunicipality({
point: { lat: -12.5, lng: -41.5 },
geometry,
accessible: [seabraEntry],
}),
).toEqual({ kind: 'inScope', municipality: seabraEntry })
})
it('falls back to the filtered list for a multi-zone city', () => {
expect(
resolveNearbyMunicipality({
point: { lat: -12.9, lng: -38.5 },
geometry,
accessible: [
accessible('salvador-ze-1', 'Salvador — ZE 1', '2927408'),
accessible('salvador-ze-2', 'Salvador — ZE 2', '2927408'),
],
}),
).toEqual({ kind: 'zoneCity', city: 'Salvador', ibgeCode: '2927408', zoneCount: 2 })
})
it('names where the actor is and offers the closest município in scope', () => {
const jequieEntry = accessible('jequie', 'Jequié', '2918001')
const resolution = resolveNearbyMunicipality({
point: { lat: -12.5, lng: -41.5 },
geometry,
accessible: [jequieEntry],
})
expect(resolution.kind).toBe('outOfScope')
if (resolution.kind !== 'outOfScope') return
expect(resolution.city).toBe('Seabra')
expect(resolution.nearestInScope?.municipality).toEqual(jequieEntry)
expect(resolution.nearestInScope?.distanceKm).toBeGreaterThan(0)
})
it('omits the suggestion when the portfolio is too far to be a shortcut', () => {
const resolution = resolveNearbyMunicipality({
point: { lat: -12.9, lng: -38.5 },
geometry,
accessible: [accessible('seabra', 'Seabra', '2929206')],
})
expect(resolution).toEqual({ kind: 'outOfScope', city: 'Salvador', nearestInScope: null })
})
it('reports a position outside Bahia, still suggesting a near portfolio', () => {
const salvadorEntry = accessible('salvador-ze-1', 'Salvador — ZE 1', '2927408')
const resolution = resolveNearbyMunicipality({
// Just offshore from the Salvador square above.
point: { lat: -12.9, lng: -38.1 },
geometry,
accessible: [salvadorEntry],
})
expect(resolution.kind).toBe('outsideBahia')
if (resolution.kind !== 'outsideBahia') return
expect(resolution.nearestInScope?.municipality).toEqual(salvadorEntry)
})
it('reports a distant position outside Bahia without a suggestion', () => {
expect(
resolveNearbyMunicipality({
point: { lat: -23.55, lng: -46.63 },
geometry,
accessible: [accessible('seabra', 'Seabra', '2929206')],
}),
).toEqual({ kind: 'outsideBahia', nearestInScope: null })
})
})
describe('formatDistanceKm', () => {
it('collapses sub-kilometre precision', () => {
expect(formatDistanceKm(0.42)).toBe('menos de 1 km')
})
it('keeps one decimal while it is felt', () => {
expect(formatDistanceKm(8.34)).toBe('8,3 km')
})
it('does not force a decimal that carries no information', () => {
expect(formatDistanceKm(8)).toBe('8 km')
})
it('rounds once the decimal stops mattering', () => {
expect(formatDistanceKm(38.6)).toBe('39 km')
})
})
})