teqo-backup/tests/unit/municipalityUpdateList.unit.spec.ts
Francisco Solla 7343f15053 feat(C89): página Atualizações — feed de cards + filtro omnibox + criar (modal/bottom sheet)
Substitui o placeholder B164 por um feed operacional da carteira (staff):
- Cards com avatar do autor, polaridade/urgente/adversário, município e data
- Filtro omnibox (município via dotted-path, polaridade, urgente, autor, busca)
  no padrão das listas (URL canônica + adapter puro)
- Criação in-place: modal desktop / bottom sheet mobile, reusando o registro
  unificado (C87) com prefill do município do filtro ativo
- Leader lockdown intacto (gate staff + canReadMunicipalityUpdate fail-closed)
- parseMunicipalityUpdateFormData único para os 3 forms de atualização
- Loader cruzando a carteira sem cláusula de município (access por ator)
- Int/unit specs (19 unit + 5 int) + convenções

Fix real: o modal de criação não pode ser filho React do <form> de filtros —
React 19 não liga progressive enhancement em action-form aninhado.
2026-08-08 23:07:23 -03:00

233 lines
8.3 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
import {
buildCampaignUpdatesFeedHref,
buildCampaignUpdatesFeedWhere,
parseCampaignUpdatesFeedParams,
resolveCampaignUpdatesFeedUrl,
serializeCanonicalCampaignUpdatesFeedSearchParams,
} from '@/utilities/municipality/municipalityUpdateListUrl'
import {
applyCampaignUpdatesFeedSuggestion,
buildCampaignUpdatesFeedChips,
buildCampaignUpdatesFeedSuggestions,
clearCampaignUpdatesFeedFilters,
removeCampaignUpdatesFeedChip,
} from '@/utilities/municipality/municipalityUpdateOmnibox'
const [firstSlug, secondSlug] = municipalityCatalog.map((entry) => entry.slug)
describe('campaign updates feed URL contract (C89)', () => {
it('empty params resolve to the default feed and the bare base path', () => {
const { state } = resolveCampaignUpdatesFeedUrl({})
expect(state).toEqual({ page: 1 })
expect(buildCampaignUpdatesFeedHref(state, 1)).toBe('/campanha/atualizacoes')
})
it('canonicalizes page 1 away and clamps an out-of-range page', () => {
const parsed = parseCampaignUpdatesFeedParams({ page: '1' })
expect(parsed.page).toBe(1)
expect(buildCampaignUpdatesFeedHref(parsed, 1)).toBe('/campanha/atualizacoes')
const resolved = resolveCampaignUpdatesFeedUrl({ page: '99' }, 3)
expect(resolved.state.page).toBe(3)
})
it('parses free-text q and trims empties away', () => {
const state = parseCampaignUpdatesFeedParams({ q: ' reunião ' })
expect(state.q).toBe('reunião')
expect(buildCampaignUpdatesFeedHref(state, 1)).toBe('/campanha/atualizacoes?q=reuni%C3%A3o')
expect(parseCampaignUpdatesFeedParams({ q: ' ' }).q).toBeUndefined()
})
it('parses municipality slugs, dropping unknown or invalid tokens', () => {
const state = parseCampaignUpdatesFeedParams({
slug: [firstSlug, secondSlug, 'nao-existe'],
})
expect(state.slugs).toEqual([firstSlug, secondSlug])
})
it('canonicalizes "all polarities" to absent while keeping a subset', () => {
const all = parseCampaignUpdatesFeedParams({
polarity: ['boa', 'neutra', 'ruim'],
})
expect(all.polarities).toBeUndefined()
const subset = parseCampaignUpdatesFeedParams({ polarity: ['boa', 'ruim'] })
expect(subset.polarities).toEqual(['boa', 'ruim'])
expect(serializeCanonicalCampaignUpdatesFeedSearchParams(subset).toString()).toBe(
'polarity=boa&polarity=ruim',
)
})
it('treats urgent as present-only boolean', () => {
expect(parseCampaignUpdatesFeedParams({ urgent: 'true' }).urgent).toBe(true)
expect(parseCampaignUpdatesFeedParams({ urgent: 'false' }).urgent).toBeUndefined()
expect(parseCampaignUpdatesFeedParams({}).urgent).toBeUndefined()
})
it('parses author ids numerically and sorted', () => {
const state = parseCampaignUpdatesFeedParams({ author: ['3', 'abc', '1'] })
expect(state.authors).toEqual([1, 3])
})
it('redirects when an unsupported search param is present', () => {
const resolved = resolveCampaignUpdatesFeedUrl({ q: 'x', foo: 'bar' })
expect(resolved.redirectHref).toBe('/campanha/atualizacoes?q=x')
})
it('does not redirect on the canonical contract', () => {
const resolved = resolveCampaignUpdatesFeedUrl({ q: 'x' })
expect(resolved.redirectHref).toBeUndefined()
})
})
describe('campaign updates feed where builder (C89)', () => {
it('builds an empty where for the default feed', () => {
expect(buildCampaignUpdatesFeedWhere({ page: 1 })).toEqual({})
})
it('maps every dimension to its Payload constraint', () => {
const where = buildCampaignUpdatesFeedWhere({
page: 1,
q: 'adesão',
slugs: [firstSlug],
polarities: ['boa'],
urgent: true,
authors: [3],
})
expect(where).toEqual({
and: [
{ body: { contains: 'adesão' } },
{ 'municipality.slug': { in: [firstSlug] } },
{ polarity: { in: ['boa'] } },
{ urgent: { equals: true } },
{ author: { in: [3] } },
],
})
})
it('filters municipality slugs through the relationship slug field, fail-closed', () => {
const where = buildCampaignUpdatesFeedWhere({
page: 1,
slugs: ['slug-sem-cadastro'],
})
expect(where).toEqual({
and: [{ 'municipality.slug': { in: ['slug-sem-cadastro'] } }],
})
})
})
describe('campaign updates feed omnibox (C89)', () => {
const base = parseCampaignUpdatesFeedParams({})
const authorOptions = [{ value: '3', label: 'Ana Assessora' }]
const municipalitySlugOptions = [firstSlug]
it('builds chips from URL filters', () => {
const state = parseCampaignUpdatesFeedParams({
q: 'feira',
slug: firstSlug,
polarity: 'boa',
urgent: 'true',
author: '3',
})
const chips = buildCampaignUpdatesFeedChips({
state,
municipalityNameBySlug: new Map([[firstSlug, 'Feira de Santana']]),
authorNameById: new Map([[3, 'Ana Assessora']]),
})
expect(chips.map((chip) => chip.id)).toEqual([
'q',
`slug:${firstSlug}`,
'polarity:boa',
'urgent',
'author:3',
])
expect(chips.find((chip) => chip.id === 'q')?.label).toBe('Busca: feira')
})
// Small dimension shortcuts (urgente/polaridade/autor) surface on focus; the
// big Município group is revealed by typing — same as the municipality list.
it('suggests the small dimension groups on an empty query', () => {
const suggestions = buildCampaignUpdatesFeedSuggestions({
query: '',
municipalitySlugOptions,
authorOptions,
})
const groups = new Set(suggestions.map((suggestion) => suggestion.group))
expect(groups).toEqual(new Set(['Urgente', 'Polaridade', 'Autor']))
})
it('reveals the Município group once text is typed', () => {
const suggestions = buildCampaignUpdatesFeedSuggestions({
query: firstSlug.slice(0, 4),
municipalitySlugOptions,
authorOptions,
})
expect(suggestions.some((entry) => entry.group === 'Município')).toBe(true)
})
it('matches typed text against a dimension and the text search', () => {
const suggestions = buildCampaignUpdatesFeedSuggestions({
query: 'pol',
municipalitySlugOptions,
authorOptions,
})
expect(suggestions.some((entry) => entry.id === 'q:pol')).toBe(true)
expect(suggestions.some((entry) => entry.group === 'Polaridade')).toBe(true)
})
it('applies toggles and text search, resetting the page', () => {
const text = applyCampaignUpdatesFeedSuggestion({
state: base,
suggestionId: 'q:Mucuri',
})
expect(text).toEqual({ kind: 'url', state: { page: 1, q: 'Mucuri' } })
const slug = applyCampaignUpdatesFeedSuggestion({
state: base,
suggestionId: `slug:${firstSlug}`,
})
expect(slug).toEqual({ kind: 'url', state: { page: 1, slugs: [firstSlug] } })
const urgentOn = applyCampaignUpdatesFeedSuggestion({ state: base, suggestionId: 'urgent' })
const urgentOff = applyCampaignUpdatesFeedSuggestion({
state: urgentOn.state,
suggestionId: 'urgent',
})
expect(urgentOn).toEqual({ kind: 'url', state: { page: 1, urgent: true } })
expect(urgentOff.state.urgent).toBeUndefined()
const author = applyCampaignUpdatesFeedSuggestion({ state: base, suggestionId: 'author:3' })
expect(author).toEqual({ kind: 'url', state: { page: 1, authors: [3] } })
})
it('rejects unknown suggestion ids and bad author ids', () => {
expect(applyCampaignUpdatesFeedSuggestion({ state: base, suggestionId: 'nope' }).state).toEqual(
base,
)
expect(
applyCampaignUpdatesFeedSuggestion({ state: base, suggestionId: 'author:abc' }).state,
).toEqual(base)
expect(
applyCampaignUpdatesFeedSuggestion({ state: base, suggestionId: 'polarity:beta' }).state,
).toEqual(base)
})
it('removes chips dimension by dimension and clears everything', () => {
const state = parseCampaignUpdatesFeedParams({
q: 'x',
slug: firstSlug,
polarity: 'boa',
urgent: 'true',
author: '3',
})
const afterQ = removeCampaignUpdatesFeedChip({ state, chipId: 'q' })
expect(afterQ.state.q).toBeUndefined()
const afterSlug = removeCampaignUpdatesFeedChip({ state, chipId: `slug:${firstSlug}` })
expect(afterSlug.state.slugs).toBeUndefined()
const clear = clearCampaignUpdatesFeedFilters()
expect(clear).toEqual({ kind: 'clear', state: { page: 1 } })
})
})