teqo/tests/unit/campaignListUrl.unit.spec.ts
Francisco Ignácio Fontoura Solla dc9a39f800 Pass 2 W0: pina contratos de URL e loaders das listas + truth-up TESTING/TECH-DEBT
- unit: campaignListUrl (canonicalize/clamp/redirect), supporterUi, actionPlanUi (matriz tab->where), 5 parsers de entidade
- int: campaignEntityListData smokes (escopo por role, busca por marker, shape de paginacao)
- fixtures: suporte a stateDeputy (create + cleanup)
- docs: contagens reais (45/47/7), Praca ~96 hits, conflito C8 F4 resolvido (10 ladders restantes -> W4d)
2026-07-25 13:30:39 -03:00

151 lines
5.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
allParamValues,
buildListHref,
firstValue,
inspectRawListParams,
normalizedText,
resolveListUrl,
strictDecimalInteger,
type RawSearchParams,
} from '@/utilities/campaignListUrl'
describe('firstValue', () => {
it('returns the first cell of a repeated param and passes scalars through', () => {
expect(firstValue(['a', 'b'])).toBe('a')
expect(firstValue('x')).toBe('x')
expect(firstValue(undefined)).toBeUndefined()
})
})
describe('allParamValues', () => {
it('flattens, trims, dedupes and drops empty cells', () => {
expect(allParamValues([' a ', 'b', 'a', ''])).toEqual(['a', 'b'])
expect(allParamValues('solo')).toEqual(['solo'])
expect(allParamValues(undefined)).toEqual([])
expect(allParamValues([' '])).toEqual([])
})
})
describe('normalizedText', () => {
it('trims and turns empty/non-string into undefined', () => {
expect(normalizedText(' salvador ')).toBe('salvador')
expect(normalizedText('')).toBeUndefined()
expect(normalizedText(' ')).toBeUndefined()
expect(normalizedText(undefined)).toBeUndefined()
})
})
describe('strictDecimalInteger', () => {
it('accepts only positive decimal integers without leading zeros', () => {
expect(strictDecimalInteger('12')).toBe(12)
expect(strictDecimalInteger('1')).toBe(1)
expect(strictDecimalInteger('0')).toBeUndefined()
expect(strictDecimalInteger('01')).toBeUndefined()
expect(strictDecimalInteger('-3')).toBeUndefined()
expect(strictDecimalInteger('1.5')).toBeUndefined()
expect(strictDecimalInteger('abc')).toBeUndefined()
expect(strictDecimalInteger(undefined)).toBeUndefined()
})
it('rejects unsafe integers', () => {
expect(strictDecimalInteger('9007199254740993')).toBeUndefined()
})
})
describe('inspectRawListParams', () => {
const paramNameSet = new Set(['q', 'page'])
it('flags unsupported params and excludes them from the serialized query', () => {
const result = inspectRawListParams({ q: 'x', foo: '1' }, paramNameSet)
expect(result.hasUnsupportedParams).toBe(true)
expect(result.query).toBe('q=x')
})
it('serializes repeated values in order and skips undefined', () => {
const result = inspectRawListParams({ q: ['a', 'b'], page: undefined }, paramNameSet)
expect(result.hasUnsupportedParams).toBe(false)
expect(result.query).toBe('q=a&q=b')
})
})
// A minimal list-state fixture mirroring how supporter/actionPlan lists wire
// resolveListUrl: q + page, canonical order q-then-page, page omitted at 1.
type FixtureState = { page: number; q?: string }
const fixtureParamNameSet = new Set(['q', 'page'])
const parseFixture = (params: RawSearchParams): FixtureState => {
const q = normalizedText(firstValue(params.q))
return {
page: strictDecimalInteger(firstValue(params.page)) ?? 1,
...(q ? { q } : {}),
}
}
const buildFixtureSearchParams = (state: FixtureState, page = state.page): URLSearchParams => {
const params = new URLSearchParams()
if (state.q) params.set('q', state.q)
if (page > 1) params.set('page', String(page))
return params
}
const resolveFixtureUrl = (params: RawSearchParams, totalPages?: number) =>
resolveListUrl({
params,
paramNameSet: fixtureParamNameSet,
parse: parseFixture,
buildSearchParams: buildFixtureSearchParams,
basePath: '/campanha/lista',
totalPages,
})
describe('resolveListUrl', () => {
it('passes a canonical URL through without a redirect', () => {
const result = resolveFixtureUrl({ q: 'x', page: '2' })
expect(result.state).toEqual({ page: 2, q: 'x' })
expect(result.href).toBe('/campanha/lista?q=x&page=2')
expect(result.redirectHref).toBeUndefined()
})
it('redirects when raw params are in non-canonical order', () => {
const result = resolveFixtureUrl({ page: '2', q: 'x' })
expect(result.redirectHref).toBe('/campanha/lista?q=x&page=2')
})
it('redirects on unsupported params, dropping them', () => {
const result = resolveFixtureUrl({ q: 'x', foo: '1' })
expect(result.state).toEqual({ page: 1, q: 'x' })
expect(result.redirectHref).toBe('/campanha/lista?q=x')
})
it('clamps page to totalPages and redirects', () => {
const result = resolveFixtureUrl({ page: '9' }, 3)
expect(result.state.page).toBe(3)
expect(result.redirectHref).toBe('/campanha/lista?page=3')
})
it('does not clamp when totalPages is 0 (empty result set)', () => {
const result = resolveFixtureUrl({ page: '9' }, 0)
expect(result.state.page).toBe(9)
expect(result.redirectHref).toBeUndefined()
})
it('normalizes page=1 out of the URL', () => {
const result = resolveFixtureUrl({ page: '1' })
expect(result.href).toBe('/campanha/lista')
expect(result.redirectHref).toBe('/campanha/lista')
})
})
describe('buildListHref', () => {
it('omits the query entirely when the builder emits no params', () => {
expect(buildListHref({ page: 1 }, buildFixtureSearchParams, '/campanha/lista', 1)).toBe(
'/campanha/lista',
)
expect(buildListHref({ page: 1 }, buildFixtureSearchParams, '/campanha/lista', 4)).toBe(
'/campanha/lista?page=4',
)
})
})