teqo-backup/tests/unit/campaignCellEditOverlay.unit.spec.ts
Francisco Ignácio Fontoura Solla 59d89ce287 B42: Drawer quick-edit and a tappable card on the mobile municipalities list.
Tendência, Assessores and Votos estimados now share one container
(CampaignCellEditOverlay: Popover on md+, bottom Drawer below), the card drops
to the table's density, and the whole card navigates through a stretched link
instead of a second "Abrir município" button.
2026-07-27 20:49:41 -03:00

143 lines
5.6 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } 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 { MunicipalityListTrendControl } from '@/components/campaign/municipality/MunicipalityListTrendControl'
import type { CampaignCellEditOverlayVariant } from '@/components/campaign/shared/CampaignCellEditOverlay'
import { toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate'
/**
* B42: the three auto-save 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 three at once.
*/
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
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: 'votos estimados',
triggerLabel: `Editar votos estimados em ${MUNICIPALITY_NAME} — Média: 1.200`,
drawerTitle: 'Editar votos estimados',
element: (variant) =>
createElement(MunicipalityListExpectedVotesControl, {
municipalityID: 1,
municipalityName: MUNICIPALITY_NAME,
expectedVotes: toVoteEstimateScenarioViewModel({ central: 1200 }),
pledgeCoverage: null,
variant,
}),
},
]
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')
})
describe('campaign cell edit overlay', () => {
it.each(overlayCases)(
'opens $name in a Drawer that names the município on touch',
async ({ triggerLabel, drawerTitle, element }) => {
render(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))
// Swiping down is the gesture, but it is not an affordance anyone sees.
expect(dialog.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 }) => {
render(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()
},
)
})