mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
414 lines
14 KiB
TypeScript
414 lines
14 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
filterPeopleRows,
|
|
mergePeopleSources,
|
|
peopleFilterFacetsFromRows,
|
|
peopleNameSubline,
|
|
scopePeopleRows,
|
|
sortPeopleRows,
|
|
type MergedPerson,
|
|
type PeopleMergeSources,
|
|
} from '@/utilities/people/peopleData'
|
|
|
|
const source: PeopleMergeSources = {
|
|
leaderships: [
|
|
{
|
|
id: 10,
|
|
contactID: 100,
|
|
name: 'Maria Souza',
|
|
phone: '71999990001',
|
|
email: 'maria@example.com',
|
|
city: 'Feira de Santana',
|
|
municipalityIDs: [3, 4],
|
|
supportStatus: 'engajado',
|
|
hasAppAccess: true,
|
|
advisorIDs: [21],
|
|
},
|
|
],
|
|
deputies: [
|
|
{
|
|
id: 20,
|
|
contactID: 101,
|
|
name: 'Ana Lima',
|
|
phone: null,
|
|
email: 'ana@example.com',
|
|
city: 'Camaçari',
|
|
party: 'PCdoB',
|
|
ballotName: 'Ana do Povo',
|
|
municipalityIDs: [5],
|
|
advisorIDs: [22, 23],
|
|
},
|
|
],
|
|
staff: [
|
|
{
|
|
id: 31,
|
|
contactID: 100,
|
|
name: 'Maria Souza',
|
|
phone: '71999990001',
|
|
email: 'maria@example.com',
|
|
city: 'Feira de Santana',
|
|
role: 'advisor',
|
|
municipalityIDs: [3, 6],
|
|
},
|
|
{
|
|
id: 32,
|
|
contactID: 102,
|
|
name: 'João Pereira',
|
|
phone: null,
|
|
email: null,
|
|
city: null,
|
|
role: 'advisor',
|
|
municipalityIDs: [],
|
|
},
|
|
{
|
|
id: 33,
|
|
contactID: 103,
|
|
name: 'Jorge Solla',
|
|
phone: null,
|
|
email: null,
|
|
city: null,
|
|
role: 'candidate',
|
|
municipalityIDs: [1],
|
|
},
|
|
],
|
|
}
|
|
|
|
const byContact = (rows: readonly MergedPerson[], contactID: number): MergedPerson => {
|
|
const row = rows.find((person) => person.contactID === contactID)
|
|
if (!row) throw new Error(`Missing merged person ${contactID}`)
|
|
return row
|
|
}
|
|
|
|
describe('mergePeopleSources (C100)', () => {
|
|
it('merges one row per contact, accumulating capacities', () => {
|
|
const rows = mergePeopleSources(source)
|
|
|
|
expect(rows.map((row) => row.contactID).sort()).toEqual([100, 101, 102, 103])
|
|
|
|
const maria = byContact(rows, 100)
|
|
expect(maria.name).toBe('Maria Souza')
|
|
expect(maria.phone).toBe('71999990001')
|
|
expect(maria.email).toBe('maria@example.com')
|
|
expect(maria.city).toBe('Feira de Santana')
|
|
expect(maria.leadershipID).toBe(10)
|
|
expect(maria.leadershipMunicipalityIDs).toEqual([3, 4])
|
|
expect(maria.supportStatus).toBe('engajado')
|
|
expect(maria.hasAppAccess).toBe(true)
|
|
expect(maria.staff.map((account) => account.id)).toEqual([31])
|
|
expect(maria.assessoraMunicipalityIDs).toEqual([3, 6])
|
|
expect(maria.capacityMunicipalityIDs).toEqual([3, 4, 6])
|
|
})
|
|
|
|
it('carries the party from the dobradinha and keeps its municipalities', () => {
|
|
const ana = byContact(mergePeopleSources(source), 101)
|
|
expect(ana.party).toBe('PCdoB')
|
|
expect(ana.deputyID).toBe(20)
|
|
expect(ana.deputyMunicipalityIDs).toEqual([5])
|
|
expect(ana.capacityMunicipalityIDs).toEqual([5])
|
|
})
|
|
|
|
it('carries the ballot name from the dobradinha (C129)', () => {
|
|
const ana = byContact(mergePeopleSources(source), 101)
|
|
expect(ana.ballotName).toBe('Ana do Povo')
|
|
const maria = byContact(mergePeopleSources(source), 100)
|
|
expect(maria.ballotName).toBeNull()
|
|
})
|
|
|
|
it('keeps a staff account with an EMPTY carteira as a capacity (gate 2026-08-09)', () => {
|
|
const joao = byContact(mergePeopleSources(source), 102)
|
|
expect(joao.staff).toHaveLength(1)
|
|
expect(joao.assessoraMunicipalityIDs).toEqual([])
|
|
expect(joao.capacityMunicipalityIDs).toEqual([])
|
|
})
|
|
|
|
it('sorts and dedupes the capacity municipality unions', () => {
|
|
const maria = byContact(mergePeopleSources(source), 100)
|
|
expect(maria.assessoraMunicipalityIDs).toEqual([3, 6])
|
|
expect(maria.capacityMunicipalityIDs).toEqual([3, 4, 6])
|
|
})
|
|
|
|
it('merges multiple staff accounts sharing one ficha into one row', () => {
|
|
const rows = mergePeopleSources({
|
|
leaderships: [],
|
|
deputies: [],
|
|
staff: [
|
|
{
|
|
id: 41,
|
|
contactID: 200,
|
|
name: 'Dupla',
|
|
phone: null,
|
|
email: null,
|
|
city: null,
|
|
role: 'advisor',
|
|
municipalityIDs: [1],
|
|
},
|
|
{
|
|
id: 42,
|
|
contactID: 200,
|
|
name: 'Dupla',
|
|
phone: null,
|
|
email: null,
|
|
city: null,
|
|
role: 'coordinator',
|
|
municipalityIDs: [2],
|
|
},
|
|
],
|
|
})
|
|
const person = byContact(rows, 200)
|
|
expect(person.staff.map((account) => account.id).sort()).toEqual([41, 42])
|
|
expect(person.assessoraMunicipalityIDs).toEqual([1, 2])
|
|
expect(rows).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('scopePeopleRows (advisor viewer)', () => {
|
|
it('keeps every row for unrestricted actors', () => {
|
|
const rows = mergePeopleSources(source)
|
|
expect(scopePeopleRows(rows, null)).toHaveLength(4)
|
|
})
|
|
|
|
it('keeps rows with any capacity municipality in the portfolio', () => {
|
|
const rows = mergePeopleSources(source)
|
|
const scoped = scopePeopleRows(rows, new Set([6]))
|
|
expect(scoped.map((row) => row.contactID).sort()).toEqual([100])
|
|
})
|
|
|
|
it('drops rows without any capacity municipality in the portfolio', () => {
|
|
const rows = mergePeopleSources(source)
|
|
const scoped = scopePeopleRows(rows, new Set([1]))
|
|
expect(scoped.map((row) => row.contactID)).toEqual([103])
|
|
})
|
|
|
|
it('hides staff with an empty carteira from every advisor portfolio', () => {
|
|
const rows = mergePeopleSources(source)
|
|
const scoped = scopePeopleRows(rows, new Set([3, 4, 5, 6]))
|
|
expect(scoped.some((row) => row.contactID === 102)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('filterPeopleRows', () => {
|
|
const rows = mergePeopleSources(source)
|
|
|
|
it('filters by capacity with OR semantics within the facet', () => {
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, capacities: ['lideranca'] }).map((row) => row.contactID),
|
|
).toEqual([100])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, capacities: ['lideranca', 'dobradinha'] })
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([100, 101])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, capacities: ['assessora'] })
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([100, 103])
|
|
})
|
|
|
|
it('filters by municipality intersecting any capacity', () => {
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, municipalities: [5] }).map((row) => row.contactID),
|
|
).toEqual([101])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, municipalities: [3] }).map((row) => row.contactID),
|
|
).toEqual([100])
|
|
expect(filterPeopleRows(rows, { page: 1, municipalities: [99] })).toEqual([])
|
|
})
|
|
|
|
it('filters by leadership status, dropping non-leadership rows', () => {
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, statuses: ['engajado'] }).map((row) => row.contactID),
|
|
).toEqual([100])
|
|
expect(filterPeopleRows(rows, { page: 1, statuses: ['negativo'] })).toEqual([])
|
|
})
|
|
|
|
it('filters by party (dobradinha free text) with OR semantics, dropping rows without one (C130)', () => {
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, parties: ['PCdoB'] }).map((row) => row.contactID),
|
|
).toEqual([101])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, parties: ['PCdoB', 'PT'] }).map((row) => row.contactID),
|
|
).toEqual([101])
|
|
expect(filterPeopleRows(rows, { page: 1, parties: ['PT'] })).toEqual([])
|
|
})
|
|
|
|
it('filters absence with OR semantics within the facet (C117)', () => {
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, ausencias: ['sem_contato'] })
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([101, 102, 103])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, ausencias: ['sem_base'] })
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([102, 103])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, ausencias: ['sem_assessor'] })
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([102, 103])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, ausencias: ['sem_contato', 'sem_base'] })
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([101, 102, 103])
|
|
})
|
|
|
|
it('filters "qualquer ausência" as the exact union of the three predicates (C125)', () => {
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, ausencias: ['qualquer_ausencia'] })
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([101, 102, 103])
|
|
expect(
|
|
filterPeopleRows(rows, {
|
|
page: 1,
|
|
ausencias: ['qualquer_ausencia', 'sem_contato'],
|
|
})
|
|
.map((row) => row.contactID)
|
|
.sort(),
|
|
).toEqual([101, 102, 103])
|
|
})
|
|
|
|
it('combines "qualquer ausência" with the other filters (AND across facets)', () => {
|
|
expect(
|
|
filterPeopleRows(rows, {
|
|
page: 1,
|
|
municipalities: [5],
|
|
ausencias: ['qualquer_ausencia'],
|
|
}).map((row) => row.contactID),
|
|
).toEqual([101])
|
|
expect(
|
|
filterPeopleRows(rows, {
|
|
page: 1,
|
|
statuses: ['engajado'],
|
|
ausencias: ['qualquer_ausencia'],
|
|
}),
|
|
).toEqual([])
|
|
})
|
|
|
|
it('combines absence facets with the other filters (AND across facets)', () => {
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, capacities: ['assessora'], ausencias: ['sem_base'] }).map(
|
|
(row) => row.contactID,
|
|
),
|
|
).toEqual([103])
|
|
expect(
|
|
filterPeopleRows(rows, { page: 1, statuses: ['engajado'], ausencias: ['sem_contato'] }),
|
|
).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('sortPeopleRows (C117)', () => {
|
|
const rows = mergePeopleSources(source)
|
|
const ids = (sorted: readonly MergedPerson[]) => sorted.map((row) => row.contactID)
|
|
|
|
it('sorts by name asc by default contract, desc inverts', () => {
|
|
expect(ids(sortPeopleRows(rows, 'name', 'asc'))).toEqual([101, 102, 103, 100])
|
|
expect(ids(sortPeopleRows(rows, 'name', 'desc'))).toEqual([100, 103, 102, 101])
|
|
})
|
|
|
|
it('sorts text columns with nulls always last', () => {
|
|
expect(ids(sortPeopleRows(rows, 'contact', 'asc'))).toEqual([100, 101, 102, 103])
|
|
expect(ids(sortPeopleRows(rows, 'base', 'asc'))).toEqual([101, 100, 102, 103])
|
|
expect(ids(sortPeopleRows(rows, 'base', 'desc'))).toEqual([100, 101, 102, 103])
|
|
})
|
|
|
|
it('sorts by party with nulls (people without dobradinha) always last (C130)', () => {
|
|
expect(ids(sortPeopleRows(rows, 'party', 'asc'))).toEqual([101, 102, 103, 100])
|
|
expect(ids(sortPeopleRows(rows, 'party', 'desc'))).toEqual([101, 102, 103, 100])
|
|
})
|
|
|
|
it('sorts municipality columns by count, ties and nulls breaking by name', () => {
|
|
expect(ids(sortPeopleRows(rows, 'assessora', 'desc'))).toEqual([100, 103, 101, 102])
|
|
expect(ids(sortPeopleRows(rows, 'lidera', 'desc'))).toEqual([100, 101, 102, 103])
|
|
expect(ids(sortPeopleRows(rows, 'aliada', 'desc'))).toEqual([101, 102, 103, 100])
|
|
expect(ids(sortPeopleRows(rows, 'assessora', 'asc'))).toEqual([103, 100, 101, 102])
|
|
})
|
|
|
|
it('sorts assessorado by the union advisor count without resolving names', () => {
|
|
expect(ids(sortPeopleRows(rows, 'assessorado', 'desc'))).toEqual([101, 100, 102, 103])
|
|
expect(ids(sortPeopleRows(rows, 'assessorado', 'asc'))).toEqual([100, 101, 102, 103])
|
|
})
|
|
|
|
it('does not mutate the input rows', () => {
|
|
const before = ids(rows)
|
|
sortPeopleRows(rows, 'lidera', 'desc')
|
|
expect(ids(rows)).toEqual(before)
|
|
})
|
|
})
|
|
|
|
describe('peopleNameSubline (C129 + C130)', () => {
|
|
it('returns the ballot name when the dobradinha has one (legenda overrides base)', () => {
|
|
const ana = byContact(mergePeopleSources(source), 101)
|
|
expect(peopleNameSubline(ana)).toBe('Ana do Povo')
|
|
})
|
|
|
|
it('falls back to the base city when the dobradinha has no ballot name (C130)', () => {
|
|
const withoutBallotName = mergePeopleSources({
|
|
...source,
|
|
deputies: [{ ...source.deputies[0]!, ballotName: null }],
|
|
})
|
|
const ana = byContact(withoutBallotName, 101)
|
|
expect(peopleNameSubline(ana)).toBe('Camaçari')
|
|
const maria = byContact(mergePeopleSources(source), 100)
|
|
expect(peopleNameSubline(maria)).toBe('Feira de Santana')
|
|
})
|
|
|
|
it('returns null when neither ballot name nor city is present', () => {
|
|
const withoutSubline = mergePeopleSources({
|
|
...source,
|
|
deputies: [{ ...source.deputies[0]!, ballotName: null, city: null }],
|
|
})
|
|
const ana = byContact(withoutSubline, 101)
|
|
expect(peopleNameSubline(ana)).toBeNull()
|
|
})
|
|
|
|
it('skips a ballot name identical to the real name (redundant duplicate line), falling back to base', () => {
|
|
const withIdenticalBallotName = mergePeopleSources({
|
|
...source,
|
|
deputies: [{ ...source.deputies[0]!, ballotName: 'Ana Lima' }],
|
|
})
|
|
const ana = byContact(withIdenticalBallotName, 101)
|
|
expect(peopleNameSubline(ana)).toBe('Camaçari')
|
|
})
|
|
})
|
|
|
|
describe('peopleFilterFacetsFromRows', () => {
|
|
it('unions the capacity municipalities and statuses of the scoped rows with the selected values', () => {
|
|
const rows = mergePeopleSources(source)
|
|
const facets = peopleFilterFacetsFromRows(rows, { page: 1, municipalities: [99] })
|
|
expect(facets.municipalityIDs).toEqual([1, 3, 4, 5, 6, 99])
|
|
expect(facets.statuses).toEqual(['engajado'])
|
|
})
|
|
|
|
it('unions the dobradinha parties with the selected values, sorted pt-BR (C130)', () => {
|
|
const rows = mergePeopleSources(source)
|
|
const facets = peopleFilterFacetsFromRows(rows, { page: 1, parties: ['XYZ'] })
|
|
expect(facets.parties).toEqual(['PCdoB', 'XYZ'])
|
|
})
|
|
|
|
it('keeps every leadership status present in the rows, in canonical selector order (C119)', () => {
|
|
const withAllStatuses: PeopleMergeSources = {
|
|
...source,
|
|
leaderships: (['engajado', 'a_abordar', 'em_disputa', 'lembranca', 'negativo'] as const).map(
|
|
(supportStatus, index) => ({
|
|
...source.leaderships[0]!,
|
|
id: 10 + index,
|
|
contactID: 100 + index,
|
|
supportStatus,
|
|
}),
|
|
),
|
|
}
|
|
const facets = peopleFilterFacetsFromRows(mergePeopleSources(withAllStatuses), { page: 1 })
|
|
expect(facets.statuses).toEqual([
|
|
'engajado',
|
|
'a_abordar',
|
|
'em_disputa',
|
|
'lembranca',
|
|
'negativo',
|
|
])
|
|
})
|
|
})
|