teqo/tests/unit/campaignMunicipalityAdvisorsCreate.unit.spec.ts
Francisco Ignácio Fontoura Solla 26b2205c3a B154 — criar assessor inline no popover da lista de municípios
Closes #352

- POST /campanha/municipios/advisors aceita body de criação (name) na union;
  createMunicipalityAdvisorRecord cria campaignUser advisor (stub
  <slug>@criado.invalid, senha aleatória) e atribui ao município na mesma
  transação (cap 10 com rollback, lock municipality-advisors:{id})
- MunicipalityAdvisorCreateProvider compartilha o assessor criado entre linhas;
  opção "+ Criar assessor" e chip otimista com id temporário no control
- advisorNameSchema unificado em primitives.ts; predicado de placeholder cobre
  @criado.invalid (reset de senha e célula de e-mail tratam stubs inline)
- Destrava o spec e2e de municipios (falhas pré-existentes em main): key
  warning do fragment trailing de MunicipalityFilters; settle-wait da animação
  do Drawer no teste B136
2026-08-03 22:46:06 -03:00

87 lines
3.3 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { createElement } from 'react'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { MunicipalityListAdvisorsControl } from '@/components/campaign/municipality/MunicipalityListAdvisorsControl'
import { TooltipProvider } from '@/components/ui/tooltip'
import type { EligibleAdvisorOption } from '@/utilities/municipality/municipalityViewModels'
/**
* B154 — the "Criar assessor" affordance is the whole point of the item: it
* must appear exactly when the typed query matches no existing eligible advisor
* (and is long enough to create), and never otherwise. These cases pin that
* contract without touching the network — the popover is only opened and typed
* into; nothing is submitted.
*/
const MUNICIPALITY_NAME = 'Feira de Santana'
const advisorControl = (options: EligibleAdvisorOption[]) =>
createElement(MunicipalityListAdvisorsControl, {
municipalityID: 1,
municipalityName: MUNICIPALITY_NAME,
currentAdvisorIDs: [],
isPriority: false,
advisorNamesById: new Map(),
options,
variant: 'sheet',
})
const openAndType = async (query: string) => {
fireEvent.click(screen.getByRole('button', { name: /Editar assessores em/ }))
const dialog = await screen.findByRole('dialog')
const search = within(dialog).getByLabelText('Buscar assessor')
fireEvent.change(search, { target: { value: query } })
return dialog
}
beforeAll(() => {
// The advisors control renders `cmdk`, which measures its list and keeps the
// selected item in view. jsdom implements neither — same stub as the overlay
// spec.
vi.stubGlobal(
'ResizeObserver',
class {
observe() {}
unobserve() {}
disconnect() {}
},
)
Element.prototype.scrollIntoView = () => {}
})
afterEach(cleanup)
afterAll(() => {
vi.unstubAllGlobals()
Reflect.deleteProperty(Element.prototype, 'scrollIntoView')
})
const renderControl = (element: ReturnType<typeof advisorControl>) =>
render(createElement(TooltipProvider, null, element))
describe('MunicipalityListAdvisorsControl create option (B154)', () => {
it('offers "Criar assessor" when the query matches no eligible advisor', async () => {
renderControl(advisorControl([]))
const dialog = await openAndType('Carlos')
const option = await within(dialog).findByRole('option', { name: /Criar assessor/ })
expect(option.textContent).toContain('Carlos')
expect(within(dialog).queryByText('Nenhum resultado.')).toBeNull()
})
it('keeps the empty state for a query too short to create', async () => {
renderControl(advisorControl([]))
const dialog = await openAndType('C')
await waitFor(() => expect(within(dialog).getByText('Nenhum resultado.')).toBeTruthy())
expect(within(dialog).queryByRole('option', { name: /Criar assessor/ })).toBeNull()
})
it('never offers create when an existing advisor matches the query', async () => {
renderControl(advisorControl([{ id: 7, name: 'Carlos Souza', isCurrent: false }]))
const dialog = await openAndType('Carlos')
expect(await within(dialog).findByRole('option', { name: /Carlos Souza/ })).toBeTruthy()
expect(within(dialog).queryByRole('option', { name: /Criar assessor/ })).toBeNull()
})
})