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
87 lines
3.3 KiB
TypeScript
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()
|
|
})
|
|
})
|