mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Collection actionPlan -> activity (labels "Atividade"/"Atividades"), campo
campaignDemand.actionPlan -> activity, rota /campanha/planos ->
/campanha/atividades (criacao em atividades/nova), sidebar "Atividades" e
rotulos alinhados a entidade ("Tipo de atividade", "Origem da atividade",
"Resultado da atividade"). Valores de enum kind/status/origin inalterados.
Schema movido por migration data-preserving escrita a mao
(20260725_213000_rename_action_plan_to_activity): 4 tabelas, 3 enums, 2
colunas, 39 indices (4 pkeys inclusos), 14 FKs e 2 sequences, cada statement
guardado na existencia do objeto de origem, com down() simetrico. O gerador do
Payload emitiria DROP+CREATE e nao ve o indice parcial escrito a mao
action_plan_upcoming_start_at_idx, entao a lista veio de
pg_indexes/pg_constraint/pg_type/pg_sequences e o snapshot .json foi derivado
do anterior por transformacao textual -- validado por migrate:create __probe
sem mudanca de schema. A mesma migration alinha os 8 indices fosseis %plaza%
que a remodelagem de municipios deixou (renomeou as colunas plaza_id, nao os
indices).
Guard: o describe "banned campaign terminology" de
codebaseConventions.unit.spec.ts virou tabela de termos varrendo src+tests+
scripts (migrations fora -- historia congelada), banindo os compostos
actionPlan, a copy "plano de acao" e a rota antiga, mas nao plan/plano isolado
(colidiria com os planos de assinatura da Fase 2). Allowlist: o proprio spec,
scripts/generate-remodel-municipalities-migration.mjs e o comentario historico
em campaignMigrationReconciliation.int.spec.ts.
Plano: docs/plans/renomear-plano-acao-para-atividade.md
151 lines
5.1 KiB
TypeScript
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/activity 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',
|
|
)
|
|
})
|
|
})
|