mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:05:19 +00:00
- Extrai toMunicipalityUpdateViewModel/loadUpdateAuthorNames compartilhados (feed + batch da lista); remove avatarClassName morto. - MunicipalityRelationTriggerProps compartilhado nos 3 wrappers; triggerClassName no ExpectedVotesControl; ChipLabel/RelationGroup/CityDash no card. - Sheet host: cleanup de unmount no overlay (sem drawer órfão), registerFooter valida open, branch nulo do handleOpenChange limpa footer; chrome freeze documentado. - A11y/UX: ícone de alerta no rodapé frio (WCAG 1.4.1), iniciais de avatar aria-hidden, chip sem_base sem target morto, label da cidade com Tendência, aria-label da barra anuncia os 3 cenários, card layout 'colocação · votos'. - E2E: settleStream no B184 (flake da cópia transitória do streaming), cards ancorados por hasText (q= casa prefixos), cor da borda via classe do token.
518 lines
21 KiB
TypeScript
518 lines
21 KiB
TypeScript
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||
import { createElement, type ReactElement } from 'react'
|
||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
||
|
||
import { MunicipalityListAdvisorsControl } from '@/components/campaign/municipality/MunicipalityListAdvisorsControl'
|
||
import { MunicipalityListExpectedVotesControl } from '@/components/campaign/municipality/MunicipalityListExpectedVotesControl'
|
||
import { MunicipalityListLeadershipsControl } from '@/components/campaign/municipality/MunicipalityListLeadershipsControl'
|
||
import { MunicipalityListLevelControl } from '@/components/campaign/municipality/MunicipalityListLevelControl'
|
||
import { MunicipalityListTrendControl } from '@/components/campaign/municipality/MunicipalityListTrendControl'
|
||
import { MunicipalityListUpdateControl } from '@/components/campaign/municipality/MunicipalityListUpdateControl'
|
||
import type { CampaignCellEditOverlayVariant } from '@/components/campaign/shared/CampaignCellEditOverlay'
|
||
import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
|
||
import { MunicipalityPortfolioCell } from '@/components/campaign/shared/MunicipalityPortfolioCell'
|
||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||
import { toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate'
|
||
|
||
/**
|
||
* B42: the quick-edit cells of `/campanha/municipios` share one container
|
||
* (`CampaignCellEditOverlay`) that is a Popover in the desktop table and a
|
||
* bottom Drawer in the mobile card. These cases pin the part a call site can
|
||
* silently get wrong — which container it renders and whether the touch surface
|
||
* says which município is being edited — for all of them at once. Three
|
||
* auto-save; the level cell (E14) submits explicitly and is here for the
|
||
* container contract all four share.
|
||
*/
|
||
const MUNICIPALITY_NAME = 'Feira de Santana'
|
||
|
||
type OverlayCase = {
|
||
name: string
|
||
/**
|
||
* The accessible name of the trigger. It replaces the pill / avatar stack /
|
||
* number it wraps, so it has to carry that value — each case spells the value
|
||
* out, which is the part a refactor drops without noticing.
|
||
*/
|
||
triggerLabel: string
|
||
/** Text the Drawer header must carry beyond the município name. */
|
||
drawerTitle: string
|
||
/**
|
||
* Label of the sheet's primary footer action, for the cells whose commit is
|
||
* an explicit submit (B32+ F5). A footer REPLACES the default "Fechar", so
|
||
* the two assertions are exclusive.
|
||
*/
|
||
footerLabel?: string
|
||
element: (variant: CampaignCellEditOverlayVariant) => ReactElement
|
||
}
|
||
|
||
const overlayCases: OverlayCase[] = [
|
||
{
|
||
name: 'tendência',
|
||
triggerLabel: `Editar tendência política em ${MUNICIPALITY_NAME} — Favorável`,
|
||
drawerTitle: 'Editar tendência',
|
||
element: (variant) =>
|
||
createElement(MunicipalityListTrendControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
status: 'favoravel',
|
||
trendNote: 'Vereador migrou para a base',
|
||
variant,
|
||
}),
|
||
},
|
||
{
|
||
name: 'assessores',
|
||
triggerLabel: `Editar assessores em ${MUNICIPALITY_NAME} — Ana Bastos`,
|
||
drawerTitle: 'Atribuir assessores',
|
||
element: (variant) =>
|
||
createElement(MunicipalityListAdvisorsControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
currentAdvisorIDs: [7],
|
||
isPriority: false,
|
||
advisorNamesById: new Map([[7, { id: 7, name: 'Ana Bastos', phone: null }]]),
|
||
options: [{ id: 7, name: 'Ana Bastos', isCurrent: false }],
|
||
variant,
|
||
}),
|
||
},
|
||
{
|
||
name: 'lideranças',
|
||
triggerLabel: `Editar lideranças em ${MUNICIPALITY_NAME} — Maria de Jesus`,
|
||
drawerTitle: 'Gerenciar lideranças',
|
||
element: (variant) =>
|
||
createElement(MunicipalityListLeadershipsControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
currentLeadershipIDs: [11],
|
||
leadershipNamesById: new Map([[11, { id: 11, name: 'Maria de Jesus' }]]),
|
||
options: [{ id: 11, name: 'Maria de Jesus' }],
|
||
variant,
|
||
}),
|
||
},
|
||
{
|
||
name: 'votos estimados',
|
||
// B193 — the votes trigger announces the whole interval (the strip shows
|
||
// all three scenarios), not just the active one.
|
||
triggerLabel: `Editar votos estimados em ${MUNICIPALITY_NAME} — Pessimista: sem estimativa; Média: 1.200; Otimista: sem estimativa`,
|
||
drawerTitle: 'Editar votos estimados',
|
||
element: (variant) =>
|
||
createElement(MunicipalityListExpectedVotesControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
expectedVotes: toVoteEstimateScenarioViewModel({ central: 1200 }),
|
||
pledgeCoverage: null,
|
||
variant,
|
||
}),
|
||
},
|
||
{
|
||
name: 'nível de envolvimento',
|
||
triggerLabel: `Nível de envolvimento de ${MUNICIPALITY_NAME}: N3 · Rede + agenda`,
|
||
drawerTitle: 'Registrar nível de envolvimento',
|
||
footerLabel: 'Registrar movimento',
|
||
element: (variant) =>
|
||
createElement(MunicipalityListLevelControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
level: 'n3',
|
||
levelNote: 'Rede montada e giro agendado',
|
||
levelChangedAt: '2026-07-01T12:00:00.000Z',
|
||
variant,
|
||
}),
|
||
},
|
||
{
|
||
name: 'sinal',
|
||
triggerLabel: `Registrar atualização em ${MUNICIPALITY_NAME} — Sem sinal`,
|
||
drawerTitle: 'Registrar atualização',
|
||
footerLabel: 'Registrar atualização',
|
||
element: (variant) =>
|
||
// `createElement`'s typing puts a required `children` in props, so the
|
||
// rest-argument form this rule prefers does not type-check here.
|
||
// eslint-disable-next-line react/no-children-prop
|
||
createElement(MunicipalityListUpdateControl, {
|
||
municipalityID: 1,
|
||
municipalitySlug: 'feira-de-santana',
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
lastSignalAt: null,
|
||
variant,
|
||
formAction: async () => ({}),
|
||
isStaff: true,
|
||
children: createElement('span', null, 'Sem sinal'),
|
||
}),
|
||
},
|
||
]
|
||
|
||
beforeAll(() => {
|
||
// The advisors control renders `cmdk`, which measures its list and keeps the
|
||
// selected item in view. jsdom implements neither. Same `ResizeObserver` stub
|
||
// as `campaignCompositionCleanup.unit.spec.ts`.
|
||
vi.stubGlobal(
|
||
'ResizeObserver',
|
||
class {
|
||
observe() {}
|
||
unobserve() {}
|
||
disconnect() {}
|
||
},
|
||
)
|
||
Element.prototype.scrollIntoView = () => {}
|
||
})
|
||
|
||
afterEach(cleanup)
|
||
|
||
afterAll(() => {
|
||
vi.unstubAllGlobals()
|
||
Reflect.deleteProperty(Element.prototype, 'scrollIntoView')
|
||
})
|
||
|
||
// The Popover variant wraps its trigger in `CampaignHoverTooltip` (Radix),
|
||
// which refuses to render outside a provider; production mounts it in the
|
||
// `(app)` layout.
|
||
const renderWithTooltip = (ui: ReactElement) => render(createElement(TooltipProvider, null, ui))
|
||
|
||
describe('campaign cell edit overlay', () => {
|
||
it.each(overlayCases)(
|
||
'opens $name in a Drawer that names the município on touch',
|
||
async ({ triggerLabel, drawerTitle, footerLabel, element }) => {
|
||
renderWithTooltip(element('sheet'))
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: triggerLabel }))
|
||
|
||
const dialog = await screen.findByRole('dialog')
|
||
const title = dialog.querySelector('[data-slot="drawer-title"]')
|
||
expect(title?.textContent).toBe(drawerTitle)
|
||
expect(dialog.querySelector('[data-slot="drawer-description"]')?.textContent).toBe(
|
||
MUNICIPALITY_NAME,
|
||
)
|
||
// Focus lands on the title, never on a field: on touch the alternative is
|
||
// the virtual keyboard covering the sheet before it can be read. Awaited
|
||
// because the advisors sheet mounts `cmdk`, which delays it by a tick.
|
||
await waitFor(() => expect(document.activeElement).toBe(title))
|
||
const footer = dialog.querySelector('[data-slot="drawer-footer"]')
|
||
if (footerLabel) {
|
||
// A footer replaces the default dismissal: the commit is the primary
|
||
// action and "Cancelar" is what stands in for "Fechar".
|
||
expect(footer?.textContent).toContain(footerLabel)
|
||
expect(footer?.textContent).not.toContain('Fechar')
|
||
} else {
|
||
// Swiping down is the gesture, but it is not an affordance anyone sees.
|
||
expect(footer?.textContent).toContain('Fechar')
|
||
}
|
||
expect(document.querySelector('[data-slot="popover-content"]')).toBeNull()
|
||
},
|
||
)
|
||
|
||
it.each(overlayCases)(
|
||
'keeps $name in a Popover on the desktop table',
|
||
async ({ triggerLabel, element }) => {
|
||
renderWithTooltip(element('popover'))
|
||
|
||
const trigger = screen.getByRole('button', { name: triggerLabel })
|
||
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog')
|
||
|
||
fireEvent.click(trigger)
|
||
|
||
await screen.findByRole('dialog')
|
||
expect(document.querySelector('[data-slot="popover-content"]')).not.toBeNull()
|
||
expect(document.querySelector('[data-slot="drawer-popup"]')).toBeNull()
|
||
// Header and subject line are Drawer chrome. A Popover is anchored to the
|
||
// trigger it came from, and that trigger's accessible name already carries
|
||
// both the município and the current value.
|
||
expect(document.querySelector('[data-slot="drawer-title"]')).toBeNull()
|
||
expect(document.querySelector('[data-slot="drawer-description"]')).toBeNull()
|
||
},
|
||
)
|
||
|
||
/**
|
||
* B32+ F4. The region used to live inside the overlay, and closing is exactly
|
||
* what commits a draft — so the announcement that mattered most was the one
|
||
* that unmounted before it could be read. It now sits beside the trigger,
|
||
* which outlives the dialog.
|
||
*/
|
||
/**
|
||
* The other half of the same policy: a region mounted on every row would
|
||
* register ~250 polite live regions on one município page (5 controls × the
|
||
* card and table trees), so it appears with the first open — early enough for
|
||
* "Salvando…", which cannot happen before one.
|
||
*/
|
||
it('does not mount the live region until the overlay has been opened once', () => {
|
||
renderWithTooltip(
|
||
createElement(MunicipalityListTrendControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
status: 'favoravel',
|
||
trendNote: null,
|
||
variant: 'popover',
|
||
}),
|
||
)
|
||
|
||
expect(document.querySelector('[aria-live="polite"]')).toBeNull()
|
||
|
||
fireEvent.click(
|
||
screen.getByRole('button', {
|
||
name: `Editar tendência política em ${MUNICIPALITY_NAME} — Favorável`,
|
||
}),
|
||
)
|
||
|
||
expect(document.querySelector('[aria-live="polite"]')).not.toBeNull()
|
||
})
|
||
|
||
it('keeps the live region mounted after the overlay closes', async () => {
|
||
const fetchMock = vi.fn(
|
||
() =>
|
||
new Promise(() => {
|
||
// Never settles: "Salvando…" is the state under test.
|
||
}),
|
||
)
|
||
vi.stubGlobal('fetch', fetchMock)
|
||
|
||
renderWithTooltip(
|
||
createElement(MunicipalityListTrendControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
status: 'favoravel',
|
||
trendNote: null,
|
||
variant: 'popover',
|
||
}),
|
||
)
|
||
|
||
const trigger = screen.getByRole('button', {
|
||
name: `Editar tendência política em ${MUNICIPALITY_NAME} — Favorável`,
|
||
})
|
||
fireEvent.click(trigger)
|
||
|
||
const select = await screen.findByLabelText('Tendência')
|
||
fireEvent.change(select, { target: { value: 'desfavoravel' } })
|
||
// Dismissing flushes the debounced save, and the dialog goes with it.
|
||
fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' })
|
||
|
||
await waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull())
|
||
await waitFor(() => {
|
||
const region = document.querySelector('[aria-live="polite"]')
|
||
expect(region?.textContent).toBe('Salvando tendência.')
|
||
})
|
||
})
|
||
|
||
/**
|
||
* The sheet's submit sits in the footer, OUTSIDE the `<form>` it belongs to —
|
||
* associated by the standard `form` attribute. This case is what says that
|
||
* association still reaches React's action (B32+ F5); without it the button
|
||
* would look right and do nothing.
|
||
*/
|
||
it('submits the signal form from a button rendered in the sheet footer', async () => {
|
||
const formAction = vi.fn(async () => ({}))
|
||
|
||
renderWithTooltip(
|
||
// `createElement`'s typing puts a required `children` in props, so the
|
||
// rest-argument form this rule prefers does not type-check here.
|
||
// eslint-disable-next-line react/no-children-prop
|
||
createElement(MunicipalityListUpdateControl, {
|
||
municipalityID: 1,
|
||
municipalitySlug: 'feira-de-santana',
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
lastSignalAt: null,
|
||
variant: 'sheet',
|
||
formAction,
|
||
isStaff: true,
|
||
children: createElement('span', null, 'Sem sinal'),
|
||
}),
|
||
)
|
||
|
||
fireEvent.click(
|
||
screen.getByRole('button', {
|
||
name: `Registrar atualização em ${MUNICIPALITY_NAME} — Sem sinal`,
|
||
}),
|
||
)
|
||
|
||
const dialog = await screen.findByRole('dialog')
|
||
// The polarity toggle (C107) is `single` with `neutra` as default; the
|
||
// sheet renders the list layout, so the label is visually hidden but the
|
||
// group keeps its accessible name. Switching the value keeps the form
|
||
// valid and exercises the control before submit.
|
||
fireEvent.click(within(screen.getByLabelText('Polaridade')).getByRole('radio', { name: 'Boa' }))
|
||
fireEvent.change(screen.getByLabelText('Texto'), {
|
||
target: { value: 'Atualização registrada no campo.' },
|
||
})
|
||
|
||
const submit = dialog.querySelector<HTMLButtonElement>('[data-slot="drawer-footer"] button')
|
||
expect(submit?.textContent).toContain('Registrar atualização')
|
||
expect(submit?.getAttribute('form')).toBe(
|
||
dialog.querySelector('form')?.getAttribute('id') ?? null,
|
||
)
|
||
|
||
fireEvent.click(submit as HTMLButtonElement)
|
||
|
||
await waitFor(() => expect(formAction).toHaveBeenCalledTimes(1))
|
||
})
|
||
|
||
/**
|
||
* C109. The cases above render each cell WITHOUT the provider — they exercise
|
||
* the per-cell Drawer fallback, which never hit the crash. On the real mobile
|
||
* list a `CampaignListSheetProvider` wraps the cards, the overlay publishes
|
||
* into its single Drawer, and the custom footer used to be portaled from the
|
||
* cell: `createPortal` keeps the SOURCE tree's React context, so the
|
||
* `DrawerCloseButton` inside the footer resolved `useDialogRootContext()`
|
||
* as undefined and the sheet never rendered. These cases pin the shared path:
|
||
* the footer must render INSIDE the host's Drawer (context present), close
|
||
* via "Cancelar", and come back fresh on a reopen.
|
||
*/
|
||
describe('shared sheet host path (B42 provider)', () => {
|
||
it('opens the signal sheet with its custom footer inside the shared Drawer, and Cancelar closes it', async () => {
|
||
render(
|
||
createElement(
|
||
CampaignListSheetProvider,
|
||
null,
|
||
// `createElement`'s typing puts a required `children` in props, so
|
||
// the rest-argument form this rule prefers does not type-check here.
|
||
// eslint-disable-next-line react/no-children-prop
|
||
createElement(MunicipalityListUpdateControl, {
|
||
municipalityID: 1,
|
||
municipalitySlug: 'feira-de-santana',
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
lastSignalAt: null,
|
||
variant: 'sheet',
|
||
formAction: async () => ({}),
|
||
isStaff: true,
|
||
children: createElement('span', null, 'Sem sinal'),
|
||
}),
|
||
),
|
||
)
|
||
|
||
fireEvent.click(
|
||
screen.getByRole('button', {
|
||
name: `Registrar atualização em ${MUNICIPALITY_NAME} — Sem sinal`,
|
||
}),
|
||
)
|
||
|
||
const dialog = await screen.findByRole('dialog')
|
||
const footer = dialog.querySelector('[data-slot="drawer-footer"]')
|
||
// The custom footer replaces the default "Fechar" — the two buttons the
|
||
// phone thumb expects, rendered in the host's Drawer context.
|
||
expect(footer?.textContent).toContain('Registrar atualização')
|
||
expect(footer?.textContent).toContain('Cancelar')
|
||
expect(footer?.textContent).not.toContain('Fechar')
|
||
|
||
fireEvent.click(within(footer as HTMLElement).getByRole('button', { name: 'Cancelar' }))
|
||
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
|
||
})
|
||
|
||
/**
|
||
* F5 across the provider path: the form body is portaled into the shared
|
||
* Drawer and the submit button lives in the host-rendered footer — the
|
||
* standard `form` attribute has to reach across that boundary for the
|
||
* association to survive. The fallback-path twin above pins the same
|
||
* contract in the per-cell Drawer.
|
||
*/
|
||
it('submits the signal form from the custom footer across the shared Drawer', async () => {
|
||
const formAction = vi.fn(async () => ({}))
|
||
|
||
render(
|
||
createElement(
|
||
CampaignListSheetProvider,
|
||
null,
|
||
// eslint-disable-next-line react/no-children-prop
|
||
createElement(MunicipalityListUpdateControl, {
|
||
municipalityID: 1,
|
||
municipalitySlug: 'feira-de-santana',
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
lastSignalAt: null,
|
||
variant: 'sheet',
|
||
formAction,
|
||
isStaff: true,
|
||
children: createElement('span', null, 'Sem sinal'),
|
||
}),
|
||
),
|
||
)
|
||
|
||
fireEvent.click(
|
||
screen.getByRole('button', {
|
||
name: `Registrar atualização em ${MUNICIPALITY_NAME} — Sem sinal`,
|
||
}),
|
||
)
|
||
|
||
const dialog = await screen.findByRole('dialog')
|
||
fireEvent.click(
|
||
within(screen.getByLabelText('Polaridade')).getByRole('radio', { name: 'Boa' }),
|
||
)
|
||
fireEvent.change(screen.getByLabelText('Texto'), {
|
||
target: { value: 'Atualização registrada no campo.' },
|
||
})
|
||
|
||
const submit = dialog.querySelector<HTMLButtonElement>('[data-slot="drawer-footer"] button')
|
||
expect(submit?.textContent).toContain('Registrar atualização')
|
||
expect(submit?.getAttribute('form')).toBe(
|
||
dialog.querySelector('form')?.getAttribute('id') ?? null,
|
||
)
|
||
|
||
fireEvent.click(submit as HTMLButtonElement)
|
||
|
||
await waitFor(() => expect(formAction).toHaveBeenCalledTimes(1))
|
||
})
|
||
|
||
it('opens the engagement-level sheet, closes, and reopens with a fresh custom footer', async () => {
|
||
render(
|
||
createElement(
|
||
CampaignListSheetProvider,
|
||
null,
|
||
createElement(MunicipalityListLevelControl, {
|
||
municipalityID: 1,
|
||
municipalityName: MUNICIPALITY_NAME,
|
||
level: 'n3',
|
||
levelNote: 'Rede montada e giro agendado',
|
||
levelChangedAt: '2026-07-01T12:00:00.000Z',
|
||
variant: 'sheet',
|
||
}),
|
||
),
|
||
)
|
||
|
||
const trigger = screen.getByRole('button', {
|
||
name: `Nível de envolvimento de ${MUNICIPALITY_NAME}: N3 · Rede + agenda`,
|
||
})
|
||
|
||
fireEvent.click(trigger)
|
||
let dialog = await screen.findByRole('dialog')
|
||
let footer = dialog.querySelector('[data-slot="drawer-footer"]')
|
||
expect(footer?.textContent).toContain('Registrar movimento')
|
||
expect(footer?.textContent).toContain('Cancelar')
|
||
expect(footer?.textContent).not.toContain('Fechar')
|
||
|
||
fireEvent.click(within(footer as HTMLElement).getByRole('button', { name: 'Cancelar' }))
|
||
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
|
||
|
||
// Reopening mounts a fresh chrome — the custom footer must come back.
|
||
fireEvent.click(trigger)
|
||
dialog = await screen.findByRole('dialog')
|
||
footer = dialog.querySelector('[data-slot="drawer-footer"]')
|
||
expect(footer?.textContent).toContain('Registrar movimento')
|
||
})
|
||
})
|
||
|
||
/**
|
||
* B34's cell is the one caller that is a sheet at EVERY viewport: it picks its
|
||
* surface by `pointer:`, not by width, so it has no `variant` to sweep and
|
||
* cannot join the cases above. It is here for the half it does share — the
|
||
* sheet chrome and, above all, `initialFocus`: its own Drawer used to hand
|
||
* focus to the first chip, a link that navigates away from the row being
|
||
* edited.
|
||
*/
|
||
it('opens the município portfolio in a Drawer focused on its title', async () => {
|
||
renderWithTooltip(
|
||
createElement(MunicipalityPortfolioCell, {
|
||
ownerId: 1,
|
||
ownerName: 'Maria Souza',
|
||
municipalityIds: [11],
|
||
municipalityIndex: [{ id: 11, slug: 'feira-de-santana' }],
|
||
commitAction: async () => ({}),
|
||
drawerTitle: 'Editar municípios',
|
||
updateErrorMessage: 'Não foi possível atualizar os municípios.',
|
||
}),
|
||
)
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: 'Editar municípios de Maria Souza' }))
|
||
|
||
const dialog = await screen.findByRole('dialog')
|
||
const title = dialog.querySelector('[data-slot="drawer-title"]')
|
||
expect(title?.textContent).toBe('Editar municípios')
|
||
expect(dialog.querySelector('[data-slot="drawer-description"]')?.textContent).toBe(
|
||
'Maria Souza',
|
||
)
|
||
await waitFor(() => expect(document.activeElement).toBe(title))
|
||
expect(dialog.textContent).toContain('Fechar')
|
||
})
|
||
})
|