Merge pull request #321 from fsolla/cursor/pool-i289-de3861a0-5cd2
B135 — Wizard Voltar = passo imediatamente anterior na cadeia
This commit is contained in:
commit
66c84a9f6b
13 changed files with 339 additions and 42 deletions
61
docs/plans/wizard-voltar-passo-anterior-cadeia-impl.md
Normal file
61
docs/plans/wizard-voltar-passo-anterior-cadeia-impl.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Impl: Wizard — Voltar = passo imediatamente anterior (também na cadeia)
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #289
|
||||
Intenção: docs/plans/wizard-voltar-passo-anterior-cadeia.md
|
||||
Appetite restante: herdado (~0,5–1d)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** Voltar (header + back do SO) no wizard `/campanha/acoes/*` navega ao passo lógico imediatamente anterior — na cadeia B98, do 1º passo de um elo encadeado volta ao passo principal do elo anterior no mesmo município, nunca à busca de município.
|
||||
- **O que NÃO negociar:** staff only; sem migration; leader lockdown; `returnPath` (B110) intacto; Registrar pedido (A5) fora — só contrato plugável.
|
||||
- **O que reavaliar:** hipótese de “history replace no avanço” — confirmado como causa do back cego; corrigir com `push` no avanço + `replace` no Voltar.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Steps["Wizard steps"] --> Prev["wizardPreviousHref"]
|
||||
Prev --> Chain["wizardActionChain"]
|
||||
Forward["Salvar / encadear"] --> Push["router.push"]
|
||||
Voltar["Header Voltar"] --> Replace["router.replace(previousHref)"]
|
||||
SOBack["SO back"] --> History["history stack coerente"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A) só corrigir `previousHref` | B) `previousHref` + push/replace | C) popstate interceptor global
|
||||
**Recomendação:** **B** — dono único de passo anterior em `wizardActionChain`; avanço empilha histórico; Voltar substitui entrada atual para não duplicar.
|
||||
**Rejeitadas:** C (explode appetite); A sem push (back do SO continua mentindo).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`wizardPreviousHref`** (`src/lib/wizardActionChain.ts`): `WizardStepKind` + href do passo anterior (interno, principal, elo encadeado).
|
||||
- **Steps** (`Wizard*Step.tsx`): trocar `previousHref` ad hoc pelo helper.
|
||||
- **`CampaignWizardNavLink`**: prop `replace` para Voltar.
|
||||
- **`CampaignMobileTopBar`**: Voltar com `replace={true}`.
|
||||
- **Chain continues** (`WizardExpectedVotesStep`, `WizardTrendNoteStep`, `WizardSignalBodyStep`, `WizardLeadershipStep`): `router.push` em vez de `replace`.
|
||||
- **Migration:** sem migration.
|
||||
- **Testes:** unit em `wizardActionChain.unit.spec.ts`; ajuste em `wizardExpectedVotesStep`, `wizardNavigationPending`.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Tracer** — `wizardPreviousHref` + unit tests.
|
||||
2. **UI** — steps + nav semantics (push/replace).
|
||||
3. **Gates** — `pnpm gate:fast`; entrega `pnpm push`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Implementar A5 / registrar-pedido.
|
||||
- Popstate handler global fora do wizard.
|
||||
- Desfazer saves ao voltar.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Histórico duplicado se Voltar usa push** — mitigado com `replace` no chrome Voltar.
|
||||
- **Leadership form vs grid** — `previousHref` dinâmico por `mode`.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção coberto
|
||||
- [x] Invariantes AGENTS/engineering-standards
|
||||
- [x] Testes unit previstos
|
||||
|
|
@ -20,7 +20,6 @@ import {
|
|||
} from '@/components/ui/Drawer'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardActionHref } from '@/lib/campaignActionRoutes'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import {
|
||||
WIZARD_LEADERSHIP_ADD_TILE_LABEL,
|
||||
|
|
@ -39,6 +38,7 @@ import {
|
|||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardLeadershipSkip,
|
||||
|
|
@ -120,7 +120,7 @@ export const WizardLeadershipStep = ({
|
|||
|
||||
const handleContinue = () => {
|
||||
startContinueTransition(() => {
|
||||
router.replace(chainContinueHref)
|
||||
router.push(chainContinueHref)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +129,13 @@ export const WizardLeadershipStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={stepTitle}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
previousHref={wizardPreviousHref({
|
||||
actionSlug,
|
||||
stepKind: mode.kind === 'form' ? 'leadership-form' : 'leadership-grid',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skipConfig}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { Button } from '@/components/ui/button'
|
|||
import { Spinner } from '@/components/ui/Spinner'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardSignalHref } from '@/lib/campaignActionRoutes'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import type { MunicipalitySignalType } from '@/lib/schemas/municipalityUpdate'
|
||||
|
|
@ -21,6 +20,7 @@ import {
|
|||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardSignalSkip,
|
||||
|
|
@ -58,7 +58,7 @@ export const WizardSignalBodyStep = ({
|
|||
useCampaignFormSuccessToast(state, () => {
|
||||
recordLastActedMunicipality(municipalitySlug)
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'register-signal')
|
||||
router.replace(
|
||||
router.push(
|
||||
wizardChainContinueHref(sessionEntry, 'register-signal', municipalitySlug, returnPath),
|
||||
)
|
||||
})
|
||||
|
|
@ -68,13 +68,13 @@ export const WizardSignalBodyStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={stepTitle}
|
||||
previousHref={wizardSignalHref(
|
||||
previousHref={wizardPreviousHref({
|
||||
actionSlug,
|
||||
stepKind: 'signal-body',
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
returnPath,
|
||||
)}
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
DrawerTitle,
|
||||
} from '@/components/ui/Drawer'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardActionHref, wizardSignalHref } from '@/lib/campaignActionRoutes'
|
||||
import { wizardSignalHref } from '@/lib/campaignActionRoutes'
|
||||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import {
|
||||
municipalitySignalTypeMeta,
|
||||
|
|
@ -25,7 +25,7 @@ import {
|
|||
} from '@/lib/municipalitySignalTypeMeta'
|
||||
import type { MunicipalitySignalType } from '@/lib/schemas/municipalityUpdate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
import { wizardChainEndHref, wizardPreviousHref } from '@/lib/wizardActionChain'
|
||||
import { resolveWizardSignalSkip, WIZARD_SIGNAL_TYPE_STEP_TITLE } from '@/lib/wizardSignalUi'
|
||||
|
||||
type WizardSignalTypeStepProps = {
|
||||
|
|
@ -53,7 +53,13 @@ export const WizardSignalTypeStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={WIZARD_SIGNAL_TYPE_STEP_TITLE}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
previousHref={wizardPreviousHref({
|
||||
actionSlug,
|
||||
stepKind: 'signal-type',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
contentAlign="end"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
DrawerTitle,
|
||||
} from '@/components/ui/Drawer'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardActionHref, wizardTrendHref } from '@/lib/campaignActionRoutes'
|
||||
import { wizardTrendHref } from '@/lib/campaignActionRoutes'
|
||||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import {
|
||||
politicalTrendWizardMetaByStatus,
|
||||
|
|
@ -30,7 +30,7 @@ import {
|
|||
} from '@/lib/politicalTrendWizardUi'
|
||||
import type { PoliticalTrendStatusValue } from '@/lib/schemas/municipality'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
import { wizardChainEndHref, wizardPreviousHref } from '@/lib/wizardActionChain'
|
||||
|
||||
type WizardTrendChoiceStepProps = {
|
||||
actionSlug: string
|
||||
|
|
@ -61,7 +61,13 @@ export const WizardTrendChoiceStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={wizardTrendChoiceStepTitle(currentStatus)}
|
||||
previousHref={wizardActionHref(actionSlug, municipalitySlug, { entryAction, returnPath })}
|
||||
previousHref={wizardPreviousHref({
|
||||
actionSlug,
|
||||
stepKind: 'trend-choice',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { Button } from '@/components/ui/button'
|
|||
import { Spinner } from '@/components/ui/Spinner'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardTrendHref } from '@/lib/campaignActionRoutes'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import {
|
||||
|
|
@ -25,6 +24,7 @@ import {
|
|||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import { politicalTrendLabels } from '@/utilities/municipality/municipalityLabels'
|
||||
|
||||
|
|
@ -61,9 +61,7 @@ export const WizardTrendNoteStep = ({
|
|||
useCampaignFormSuccessToast(state, () => {
|
||||
recordLastActedMunicipality(municipalitySlug)
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'change-trend')
|
||||
router.replace(
|
||||
wizardChainContinueHref(sessionEntry, 'change-trend', municipalitySlug, returnPath),
|
||||
)
|
||||
router.push(wizardChainContinueHref(sessionEntry, 'change-trend', municipalitySlug, returnPath))
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -71,14 +69,13 @@ export const WizardTrendNoteStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={stepTitle}
|
||||
previousHref={wizardTrendHref(
|
||||
previousHref={wizardPreviousHref({
|
||||
actionSlug,
|
||||
stepKind: 'trend-note',
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
undefined,
|
||||
returnPath,
|
||||
)}
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@ import { cn } from '@/lib/utils'
|
|||
*/
|
||||
export const CampaignWizardNavLink = ({
|
||||
href,
|
||||
replace = false,
|
||||
className,
|
||||
children,
|
||||
...anchorProps
|
||||
}: Omit<ComponentPropsWithoutRef<'a'>, 'href'> & { href: string }) => {
|
||||
}: Omit<ComponentPropsWithoutRef<'a'>, 'href'> & { href: string; replace?: boolean }) => {
|
||||
const { isPending } = useCampaignListTransition()
|
||||
|
||||
return (
|
||||
<CampaignTransitionAnchor
|
||||
href={href}
|
||||
replace={replace}
|
||||
className={cn(isPending && 'pointer-events-none', className)}
|
||||
aria-busy={isPending || undefined}
|
||||
{...anchorProps}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { Alert, AlertDescription } from '@/components/ui/Alert'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Spinner } from '@/components/ui/Spinner'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardActionHref } from '@/lib/campaignActionRoutes'
|
||||
import { postCampaignJson } from '@/lib/campaignJsonRequest'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import {
|
||||
|
|
@ -31,6 +30,7 @@ import {
|
|||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
applyVoteShortcut,
|
||||
|
|
@ -85,9 +85,7 @@ export const WizardExpectedVotesStep = ({
|
|||
|
||||
const continueAfterVotes = () => {
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'update-votes')
|
||||
router.replace(
|
||||
wizardChainContinueHref(sessionEntry, 'update-votes', municipalitySlug, returnPath),
|
||||
)
|
||||
router.push(wizardChainContinueHref(sessionEntry, 'update-votes', municipalitySlug, returnPath))
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
|
|
@ -138,7 +136,13 @@ export const WizardExpectedVotesStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={wizardNextStepTitle(actionSlug)}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
previousHref={wizardPreviousHref({
|
||||
actionSlug,
|
||||
stepKind: 'votes',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const CampaignMobileTopBar = ({ notificationBell }: { notificationBell?:
|
|||
asChild
|
||||
disabled={isPending}
|
||||
>
|
||||
<CampaignWizardNavLink href={chrome.previousHref}>
|
||||
<CampaignWizardNavLink href={chrome.previousHref} replace>
|
||||
<ArrowLeft className="size-4 shrink-0" aria-hidden />
|
||||
Voltar
|
||||
</CampaignWizardNavLink>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@
|
|||
|
||||
import {
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
type CampaignWizardActionId,
|
||||
campaignWizardActionIdForSlug,
|
||||
isWizardReturnPath,
|
||||
wizardActionHref,
|
||||
wizardSignalHref,
|
||||
wizardTrendHref,
|
||||
type CampaignWizardActionId,
|
||||
} from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
|
||||
|
|
@ -109,3 +110,137 @@ export const resolveWizardChainEntry = (
|
|||
entryAction: CampaignWizardActionId | undefined,
|
||||
currentAction: WizardChainActionId,
|
||||
): WizardChainActionId => (isWizardChainActionId(entryAction) ? entryAction : currentAction)
|
||||
|
||||
/** Discriminates wizard chrome steps for `wizardPreviousHref` (B135). */
|
||||
type WizardStepKind =
|
||||
| 'municipality-search'
|
||||
| 'votes'
|
||||
| 'trend-choice'
|
||||
| 'trend-note'
|
||||
| 'signal-type'
|
||||
| 'signal-body'
|
||||
| 'leadership-grid'
|
||||
| 'leadership-form'
|
||||
|
||||
const wizardPrincipalStepHref = (
|
||||
action: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
sessionEntry: WizardChainActionId,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
const actionSlug = CAMPAIGN_WIZARD_ACTION_SLUGS[action]
|
||||
switch (action) {
|
||||
case 'update-votes':
|
||||
case 'update-leadership':
|
||||
return wizardActionHref(actionSlug, municipalitySlug, {
|
||||
entryAction: sessionEntry,
|
||||
returnPath,
|
||||
})
|
||||
case 'register-signal':
|
||||
return wizardSignalHref(actionSlug, municipalitySlug, undefined, sessionEntry, returnPath)
|
||||
case 'change-trend':
|
||||
return wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
sessionEntry,
|
||||
undefined,
|
||||
returnPath,
|
||||
)
|
||||
default: {
|
||||
const exhaustive: never = action
|
||||
return exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Principal step of the chain link immediately before `current` in a session owned by
|
||||
* `sessionEntry`. Undefined when `current` is the session principal (standalone entry).
|
||||
*/
|
||||
const wizardChainPreviousPrincipalHref = (
|
||||
current: WizardChainActionId,
|
||||
sessionEntry: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): string | undefined => {
|
||||
if (current === sessionEntry) {
|
||||
return undefined
|
||||
}
|
||||
const queue = wizardChainAfter(sessionEntry)
|
||||
const index = queue.indexOf(current)
|
||||
if (index === -1) {
|
||||
return undefined
|
||||
}
|
||||
if (index === 0) {
|
||||
return wizardPrincipalStepHref(sessionEntry, municipalitySlug, sessionEntry, returnPath)
|
||||
}
|
||||
return wizardPrincipalStepHref(queue[index - 1], municipalitySlug, sessionEntry, returnPath)
|
||||
}
|
||||
|
||||
type WizardPreviousHrefInput = {
|
||||
actionSlug: string
|
||||
stepKind: WizardStepKind
|
||||
municipalitySlug?: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Href for the logically previous wizard step — internal sub-steps, principal search,
|
||||
* or the preceding B98 chain link's principal step (same município).
|
||||
*/
|
||||
export const wizardPreviousHref = (input: WizardPreviousHrefInput): string => {
|
||||
const { actionSlug, stepKind, municipalitySlug, entryAction, returnPath } = input
|
||||
|
||||
if (stepKind === 'municipality-search') {
|
||||
return wizardChainEndHref(returnPath)
|
||||
}
|
||||
|
||||
if (stepKind === 'trend-note') {
|
||||
return wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
undefined,
|
||||
returnPath,
|
||||
)
|
||||
}
|
||||
|
||||
if (stepKind === 'signal-body') {
|
||||
return wizardSignalHref(actionSlug, municipalitySlug, undefined, entryAction, returnPath)
|
||||
}
|
||||
|
||||
if (stepKind === 'leadership-form') {
|
||||
return wizardActionHref(actionSlug, municipalitySlug, { entryAction, returnPath })
|
||||
}
|
||||
|
||||
const currentActionId = campaignWizardActionIdForSlug(actionSlug)
|
||||
const searchHref = wizardActionHref(actionSlug, undefined, { returnPath })
|
||||
|
||||
if (!municipalitySlug || !currentActionId || !isWizardChainActionId(currentActionId)) {
|
||||
return searchHref
|
||||
}
|
||||
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, currentActionId)
|
||||
|
||||
if (
|
||||
(stepKind === 'votes' || stepKind === 'leadership-grid') &&
|
||||
currentActionId === sessionEntry
|
||||
) {
|
||||
return searchHref
|
||||
}
|
||||
|
||||
const chainPrev = wizardChainPreviousPrincipalHref(
|
||||
currentActionId,
|
||||
sessionEntry,
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
)
|
||||
if (chainPrev) {
|
||||
return chainPrev
|
||||
}
|
||||
|
||||
return searchHref
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
resolveWizardChainEntry,
|
||||
wizardChainAfter,
|
||||
wizardChainContinueHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
|
||||
describe('wizardActionChain', () => {
|
||||
|
|
@ -87,4 +88,81 @@ describe('wizardActionChain', () => {
|
|||
expect(resolveWizardChainEntry('register-signal', 'update-votes')).toBe('register-signal')
|
||||
expect(resolveWizardChainEntry('register-demand', 'change-trend')).toBe('change-trend')
|
||||
})
|
||||
|
||||
describe('wizardPreviousHref', () => {
|
||||
it('returns chain-end for municipality search', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'municipality-search',
|
||||
}),
|
||||
).toBe(CAMPAIGN_HOME)
|
||||
})
|
||||
|
||||
it('returns municipality search for principal post-municipio steps', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'votes',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-choice',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia`)
|
||||
})
|
||||
|
||||
it('returns previous chain principal when chained from another entry', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-choice',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'update-votes',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu&entry=update-votes`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'registrar-sinal',
|
||||
stepKind: 'signal-type',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'update-votes',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-lideranca',
|
||||
stepKind: 'leadership-grid',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'register-signal',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu&entry=register-signal`)
|
||||
})
|
||||
|
||||
it('returns internal previous step within a subflow', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-note',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'update-votes',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`)
|
||||
})
|
||||
|
||||
it('honors return path on municipality search back target', () => {
|
||||
const origin = '/campanha/municipios/cairu'
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'municipality-search',
|
||||
returnPath: origin,
|
||||
}),
|
||||
).toBe(origin)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { CampaignWizardChromeProvider } from '@/components/campaign/shell/Campai
|
|||
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
|
||||
import { postCampaignJson } from '@/lib/campaignJsonRequest'
|
||||
|
||||
const replaceMock = vi.fn()
|
||||
const pushMock = vi.fn()
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
replace: replaceMock,
|
||||
push: pushMock,
|
||||
replace: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ describe('WizardExpectedVotesStep', () => {
|
|||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.mocked(postCampaignJson).mockReset()
|
||||
replaceMock.mockReset()
|
||||
pushMock.mockReset()
|
||||
})
|
||||
|
||||
it('applies shortcuts to the focused scenario', () => {
|
||||
|
|
@ -120,7 +120,7 @@ describe('WizardExpectedVotesStep', () => {
|
|||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(replaceMock).toHaveBeenCalledWith(
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`,
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest
|
|||
|
||||
const routerState = vi.hoisted(() => ({
|
||||
push: vi.fn(),
|
||||
resolvePush: null as (() => void) | null,
|
||||
replace: vi.fn(),
|
||||
resolveNav: null as (() => void) | null,
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', async (importActual) => ({
|
||||
|
|
@ -12,7 +13,7 @@ vi.mock('next/navigation', async (importActual) => ({
|
|||
usePathname: () => '/campanha/acoes/atualizar-votos',
|
||||
useRouter: () => ({
|
||||
push: (...args: unknown[]) => routerState.push(...args),
|
||||
replace: vi.fn(),
|
||||
replace: (...args: unknown[]) => routerState.replace(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
|
|
@ -65,15 +66,16 @@ describe('wizard navigation pending', () => {
|
|||
afterEach(() => {
|
||||
cleanup()
|
||||
routerState.push.mockReset()
|
||||
routerState.resolvePush = null
|
||||
routerState.replace.mockReset()
|
||||
routerState.resolveNav = null
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('dims step content and locks chrome while Voltar navigation is in flight', async () => {
|
||||
routerState.push.mockImplementation(
|
||||
routerState.replace.mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
routerState.resolvePush = resolve
|
||||
routerState.resolveNav = resolve
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -90,7 +92,7 @@ describe('wizard navigation pending', () => {
|
|||
fireEvent.click(backLink)
|
||||
})
|
||||
|
||||
expect(routerState.push).toHaveBeenCalledWith('/campanha/acoes/atualizar-votos', {
|
||||
expect(routerState.replace).toHaveBeenCalledWith('/campanha/acoes/atualizar-votos', {
|
||||
scroll: true,
|
||||
})
|
||||
|
||||
|
|
@ -105,7 +107,7 @@ describe('wizard navigation pending', () => {
|
|||
})
|
||||
|
||||
await act(async () => {
|
||||
routerState.resolvePush?.()
|
||||
routerState.resolveNav?.()
|
||||
})
|
||||
|
||||
expect(stepBody!.getAttribute('aria-busy')).not.toBe('true')
|
||||
|
|
|
|||
Loading…
Reference in a new issue