teqo/tests/unit/wizardExpectedVotesStep.unit.spec.tsx
Francisco Solla 212a1f2423 feat(campaign): C87 — atualização unificada (texto + polaridade + urgente)
Substitui o modelo de MunicipalityUpdate (kind/semanal/urgente/nota/sinal + signalType)
por um modelo unificado: body obrigatório + polarity (boa/neutra/ruim) + urgent + adversarySignal.

- Schema: polarity enum obrigatório, urgent boolean default false, adversarySignal boolean
- Wizard: novo WizardUpdateBodyStep com campo de texto + polaridade + urgente
- Lista: controle unificado de atualização com polaridade inline
- Feed: exibe polaridade como badge colorido + indicador de urgência
- V2 status strip: atualização rápida de polaridade
- E11: queries atualizadas para polarity + adversarySignal
- Migração: 20260807_030000_unify_municipality_update (backfill polarity=neutra, urgent de kind, adversarySignal de kind+signalType)
- Testes: 18/18 integração C87, 593/593 total, 1399/1399 unitários
- Fix: testes de state deputy usavam nomes fixos que colidiam entre execuções
2026-08-07 05:04:41 -03:00

137 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { WizardExpectedVotesStep } from '@/components/campaign/shared/WizardExpectedVotesStep'
import { CampaignWizardChromeProvider } from '@/components/campaign/shell/CampaignWizardChromeContext'
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
import { postCampaignJson } from '@/lib/campaignJsonRequest'
const pushMock = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: pushMock,
replace: vi.fn(),
}),
}))
vi.mock('@/lib/campaignJsonRequest', () => ({
postCampaignJson: vi.fn(),
}))
vi.mock('sonner', () => ({
toast: { success: vi.fn() },
}))
const defaultProps = {
actionSlug: 'atualizar-votos',
municipalityId: 1,
municipalityName: 'Cairu',
municipalitySlug: 'cairu',
initialExpectedVotes: { pessimistic: 100, central: 200, optimistic: 300 },
}
const renderVotesStep = (props: React.ComponentProps<typeof WizardExpectedVotesStep>) =>
render(
<CampaignWizardChromeProvider>
<WizardExpectedVotesStep {...props} />
</CampaignWizardChromeProvider>,
)
describe('WizardExpectedVotesStep', () => {
afterEach(() => {
cleanup()
vi.mocked(postCampaignJson).mockReset()
pushMock.mockReset()
})
it('applies shortcuts to the focused scenario', () => {
renderVotesStep({ ...defaultProps })
fireEvent.focus(screen.getByLabelText('Pessimista'))
fireEvent.click(screen.getByRole('button', { name: '2×' }))
expect((screen.getByLabelText('Pessimista') as HTMLInputElement).value).toBe('200')
expect((screen.getByLabelText('Média') as HTMLInputElement).value).toBe('200')
})
it('highlights the last focused scenario in compact inputs', () => {
renderVotesStep({ ...defaultProps })
const central = screen.getByLabelText('Média')
const optimistic = screen.getByLabelText('Otimista')
expect(central.className).toMatch(/border-primary/)
expect(optimistic.className).toMatch(/bg-muted/)
fireEvent.focus(optimistic)
expect(central.className).toMatch(/bg-muted/)
expect(optimistic.className).toMatch(/border-primary/)
})
it('shows violation alert and highlights inputs without navigation', () => {
renderVotesStep({ ...defaultProps })
fireEvent.change(screen.getByLabelText('Pessimista'), { target: { value: '900' } })
fireEvent.click(screen.getByRole('button', { name: 'Salvar estimativas →' }))
const step = screen.getByRole('main', { name: /Ajustar votos estimados/i })
expect(within(step).getByRole('alert').textContent).toMatch(/Pessimista/i)
expect(screen.getByLabelText('Pessimista').getAttribute('aria-invalid')).toBe('true')
expect(screen.getByLabelText('Média').getAttribute('aria-invalid')).toBe('true')
})
it('clears violation when a draft is edited', () => {
renderVotesStep({ ...defaultProps })
fireEvent.change(screen.getByLabelText('Pessimista'), { target: { value: '900' } })
fireEvent.click(screen.getByRole('button', { name: 'Salvar estimativas →' }))
const step = screen.getByRole('main', { name: /Ajustar votos estimados/i })
expect(within(step).getByRole('alert')).toBeTruthy()
fireEvent.change(screen.getByLabelText('Média'), { target: { value: '950' } })
expect(within(step).queryByRole('alert')).toBeNull()
expect(screen.getByLabelText('Pessimista').getAttribute('aria-invalid')).toBeNull()
})
it('posts batch expected votes and continues the wizard chain', async () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: {
status: 'success',
savedExpectedVotes: { pessimistic: 150, central: 250, optimistic: 350 },
},
})
renderVotesStep({ ...defaultProps })
fireEvent.change(screen.getByLabelText('Pessimista'), { target: { value: '150' } })
fireEvent.change(screen.getByLabelText('Média'), { target: { value: '250' } })
fireEvent.change(screen.getByLabelText('Otimista'), { target: { value: '350' } })
fireEvent.click(screen.getByRole('button', { name: 'Salvar estimativas →' }))
await waitFor(() => {
expect(postCampaignJson).toHaveBeenCalledWith('/campanha/municipios/expected-votes', {
municipalityId: 1,
expectedVotes: { pessimistic: 150, central: 250, optimistic: 350 },
})
})
await waitFor(() => {
expect(pushMock).toHaveBeenCalledWith(
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`,
)
})
})
it('shows skip to the next chain step when votes is chained', () => {
renderVotesStep({ ...defaultProps, entryAction: 'register-update' })
const skip = screen.getByRole('link', { name: /^Pular$/i })
expect(skip.getAttribute('href')).toBe(
`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&entry=register-update`,
)
})
})