mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
Ações de ajuste (votos, tendência, atualização, liderança) agora retornam à origem ao salvar (toast + from allowlist B110 / Início), sem avançar sozinhas para outro wizard. Removidos fila de encadeamento (wizardActionChain.ts), param entry, skip/Pular (chrome mobile sempre X) e prefill de nota sem produtor. Liderança: botão pós-save 'Continuar' → 'Concluir' (origem). Sem migration.
150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
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 { 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 returns to origin', 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('/campanha')
|
||
})
|
||
})
|
||
|
||
it('returns to the allowlisted origin when a return path is set (B110)', async () => {
|
||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||
ok: true,
|
||
payload: {
|
||
status: 'success',
|
||
savedExpectedVotes: { pessimistic: 150, central: 250, optimistic: 350 },
|
||
},
|
||
})
|
||
|
||
renderVotesStep({ ...defaultProps, returnPath: '/campanha/municipios/cairu' })
|
||
|
||
fireEvent.change(screen.getByLabelText('Média'), { target: { value: '250' } })
|
||
fireEvent.click(screen.getByRole('button', { name: 'Salvar estimativas →' }))
|
||
|
||
await waitFor(() => {
|
||
expect(pushMock).toHaveBeenCalledWith('/campanha/municipios/cairu')
|
||
})
|
||
})
|
||
|
||
it('never renders a chain skip link (B168)', () => {
|
||
renderVotesStep({ ...defaultProps })
|
||
|
||
expect(screen.queryByRole('link', { name: /^Pular$/i })).toBeNull()
|
||
})
|
||
})
|