teqo-backup/tests/unit/recordingListUrl.unit.spec.ts

249 lines
9.6 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import { SPEECH_TOPICS } from '@/lib/speechFacets'
import {
buildRecordingFiltersKey,
buildRecordingListHref,
parseRecordingListParams,
parseRecordingPeople,
recordingHasActiveFilters,
recordingSortIsDuration,
recordingSortOrder,
removeRecordingPerson,
resolveRecordingListUrl,
toggleRecordingPerson,
} from '@/utilities/recordings/recordingListUrl'
import { SPEECH_DURATION_BUCKETS } from '@/utilities/speech/speechListUrl'
describe('recording list URL (C199)', () => {
it('parses the canonical state and rejects an unready query', () => {
expect(parseRecordingListParams({ source: 'enviadas', q: 'merenda', page: '3' })).toEqual({
source: 'enviadas',
page: 3,
q: 'merenda',
})
// One character is not a ready search query (same rule as contact search).
expect(parseRecordingListParams({ source: 'enviadas', q: 'a' })).toEqual({
source: 'enviadas',
page: 1,
})
expect(parseRecordingListParams({ source: 'enviadas', page: '0' }).page).toBe(1)
})
it('keeps `source=enviadas` in the canonical href and drops unsupported params', () => {
const canonical = resolveRecordingListUrl({ source: 'enviadas', q: 'merenda' })
expect(canonical.state).toEqual({ source: 'enviadas', page: 1, q: 'merenda' })
expect(canonical.href).toBe('/campanha/comunicacao/acervo?source=enviadas&q=merenda')
expect(canonical.redirectHref).toBeUndefined()
const polluted = resolveRecordingListUrl({
source: 'enviadas',
q: 'merenda',
phase: 'Ordem do Dia',
foo: 'bar',
})
expect(polluted.redirectHref).toBe('/campanha/comunicacao/acervo?source=enviadas&q=merenda')
const unready = resolveRecordingListUrl({ source: 'enviadas', q: 'a' })
expect(unready.href).toBe('/campanha/comunicacao/acervo?source=enviadas')
expect(unready.redirectHref).toBe('/campanha/comunicacao/acervo?source=enviadas')
})
it('clamps the page to the last one and builds pagination hrefs', () => {
const clamped = resolveRecordingListUrl({ source: 'enviadas', page: '9' }, 3)
expect(clamped.state.page).toBe(3)
expect(clamped.href).toBe('/campanha/comunicacao/acervo?source=enviadas&page=3')
expect(buildRecordingListHref({ source: 'enviadas', page: 1 }, 2)).toBe(
'/campanha/comunicacao/acervo?source=enviadas&page=2',
)
expect(buildRecordingListHref({ source: 'enviadas', page: 2 }, 1)).toBe(
'/campanha/comunicacao/acervo?source=enviadas',
)
})
it('parses repeated `person` values: trim, oversized drop, case-insensitive dedupe', () => {
expect(parseRecordingPeople([' Solla ', 'solla', 'Presidente'])).toEqual([
'Solla',
'Presidente',
])
expect(parseRecordingPeople(undefined)).toEqual([])
expect(parseRecordingPeople('')).toEqual([])
expect(parseRecordingPeople('x'.repeat(121))).toEqual([])
})
it('canonicalizes and serializes the `person` facet with the query', () => {
const parsed = parseRecordingListParams({
source: 'enviadas',
q: 'merenda',
person: ['Solla', 'Presidente'],
})
expect(parsed.people).toEqual(['Solla', 'Presidente'])
const canonical = resolveRecordingListUrl({
source: 'enviadas',
q: 'merenda',
person: ['Solla', 'Presidente'],
})
expect(canonical.href).toBe(
'/campanha/comunicacao/acervo?source=enviadas&q=merenda&person=Solla&person=Presidente',
)
expect(canonical.redirectHref).toBeUndefined()
})
it('drops a duplicated facet value on canonicalization', () => {
const canonical = resolveRecordingListUrl({
source: 'enviadas',
person: ['Solla', 'solla', 'x'.repeat(200)],
})
expect(canonical.href).toBe('/campanha/comunicacao/acervo?source=enviadas&person=Solla')
expect(canonical.redirectHref).toBe('/campanha/comunicacao/acervo?source=enviadas&person=Solla')
})
it('toggles and removes one person, resetting to the first page', () => {
const base = { source: 'enviadas' as const, page: 3, q: 'merenda' }
const added = toggleRecordingPerson(base, 'Solla')
expect(added).toEqual({ ...base, page: 1, people: ['Solla'] })
const removed = toggleRecordingPerson({ ...base, people: ['Solla'] }, 'solla')
expect(removed).toEqual({ ...base, page: 1, people: undefined })
expect(removeRecordingPerson({ ...base, people: ['Solla', 'Presidente'] }, 'Solla')).toEqual({
...base,
page: 1,
people: ['Presidente'],
})
expect(removeRecordingPerson({ ...base, people: ['Solla'] }, 'Solla')).toEqual({
...base,
page: 1,
people: undefined,
})
})
it('parses the C219 parity facets and serializes them in canonical order', () => {
const canonical = resolveRecordingListUrl({
source: 'enviadas',
q: 'merenda',
mode: 'tema',
year: ['2026', '2024'],
topic: 'saude',
scope: 'bahia',
municipality: '42',
duration: 'curta',
sort: 'duracao_maior',
person: 'Solla',
page: '2',
})
expect(canonical.state).toEqual({
source: 'enviadas',
page: 2,
q: 'merenda',
mode: 'tema',
years: [2026, 2024],
topics: ['saude'],
scopes: ['bahia'],
municipalities: [42],
durations: ['curta'],
sort: 'duracao_maior',
people: ['Solla'],
})
expect(canonical.href).toBe(
'/campanha/comunicacao/acervo?source=enviadas&q=merenda&mode=tema&year=2026&year=2024' +
'&topic=saude&scope=bahia&municipality=42&duration=curta&sort=duracao_maior&person=Solla&page=2',
)
expect(canonical.redirectHref).toBeUndefined()
})
it('keeps the C199 deep link byte-identical (no new param is serialized)', () => {
const canonical = resolveRecordingListUrl({
source: 'enviadas',
q: 'merenda',
person: ['Solla', 'Presidente'],
page: '2',
})
expect(canonical.href).toBe(
'/campanha/comunicacao/acervo?source=enviadas&q=merenda&person=Solla&person=Presidente&page=2',
)
expect(canonical.redirectHref).toBeUndefined()
})
it('canonicalizes the default sort and a theme without a query away', () => {
const defaultSort = resolveRecordingListUrl({ source: 'enviadas', sort: 'recentes' })
expect(defaultSort.state.sort).toBeUndefined()
expect(defaultSort.href).toBe('/campanha/comunicacao/acervo?source=enviadas')
expect(defaultSort.redirectHref).toBe(defaultSort.href)
// The parsed state keeps `mode`, but the canonical href drops it and the
// redirect lands on the URL without the meaningless param (same as Câmara).
const themeWithoutQuery = resolveRecordingListUrl({ source: 'enviadas', mode: 'tema' })
expect(themeWithoutQuery.state.mode).toBe('tema')
expect(themeWithoutQuery.href).toBe('/campanha/comunicacao/acervo?source=enviadas')
expect(themeWithoutQuery.redirectHref).toBe(themeWithoutQuery.href)
// An unknown sort value can never widen the contract: it canonicalizes away.
const unknownSort = resolveRecordingListUrl({ source: 'enviadas', sort: 'aleatorio' })
expect(unknownSort.state.sort).toBeUndefined()
expect(unknownSort.redirectHref).toBe(unknownSort.href)
})
it('collapses an exhaustive enum selection to no filter (same rule as the Câmara)', () => {
const allTopics = resolveRecordingListUrl({
source: 'enviadas',
topic: SPEECH_TOPICS.map(({ value }) => value),
})
expect(allTopics.state.topics).toBeUndefined()
expect(allTopics.href).toBe('/campanha/comunicacao/acervo?source=enviadas')
const allDurations = resolveRecordingListUrl({
source: 'enviadas',
duration: SPEECH_DURATION_BUCKETS.map(({ value }) => value),
})
expect(allDurations.state.durations).toBeUndefined()
expect(allDurations.href).toBe('/campanha/comunicacao/acervo?source=enviadas')
})
it('drops structurally invalid year/municipality values on canonicalization', () => {
const canonical = resolveRecordingListUrl({
source: 'enviadas',
year: ['1800', 'abc', '2026'],
municipality: ['-3', '0', '7'],
})
expect(canonical.state.years).toEqual([2026])
expect(canonical.state.municipalities).toEqual([7])
expect(canonical.href).toBe(
'/campanha/comunicacao/acervo?source=enviadas&year=2026&municipality=7',
)
})
it('exposes the canonical filter key and the Payload sort of each order', () => {
expect(
buildRecordingFiltersKey({ source: 'enviadas', page: 1, q: 'merenda', topics: ['saude'] }),
).toBe('source=enviadas&q=merenda&topic=saude')
expect(recordingSortOrder({ source: 'enviadas', page: 1 })).toBe('-createdAt')
expect(recordingSortOrder({ source: 'enviadas', page: 1, sort: 'duracao_maior' })).toBe(
'-durationSeconds',
)
expect(recordingSortOrder({ source: 'enviadas', page: 1, sort: 'duracao_menor' })).toBe(
'durationSeconds',
)
expect(recordingSortIsDuration({ source: 'enviadas', page: 1 })).toBe(false)
expect(recordingSortIsDuration({ source: 'enviadas', page: 1, sort: 'duracao_maior' })).toBe(
true,
)
})
it('tells an unfiltered page (including pagination) from a narrowed one', () => {
expect(recordingHasActiveFilters({ source: 'enviadas', page: 1 })).toBe(false)
expect(recordingHasActiveFilters({ source: 'enviadas', page: 4 })).toBe(false)
expect(recordingHasActiveFilters({ source: 'enviadas', page: 1, sort: 'duracao_maior' })).toBe(
false,
)
expect(recordingHasActiveFilters({ source: 'enviadas', page: 1, q: 'merenda' })).toBe(true)
expect(recordingHasActiveFilters({ source: 'enviadas', page: 1, years: [2026] })).toBe(true)
expect(recordingHasActiveFilters({ source: 'enviadas', page: 1, people: ['Solla'] })).toBe(true)
})
})