Merge pull request #354 from fsolla/agent/22-acoes-rapidas-apoiadores
B86 — Ações rápidas — Apoiadores (staff): catálogo no FAB/overlay
This commit is contained in:
commit
8f88c93429
7 changed files with 359 additions and 1 deletions
75
docs/plans/acoes-rapidas-apoiadores-impl.md
Normal file
75
docs/plans/acoes-rapidas-apoiadores-impl.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Impl: Ações rápidas — Apoiadores (staff)
|
||||
|
||||
Status: aprovado (gate humano 2026-08-03)
|
||||
Atualizado em: 2026-08-03
|
||||
Issue: #22
|
||||
Intenção: docs/plans/acoes-rapidas-apoiadores.md
|
||||
Appetite restante: ~0,25–0,5 dia eng (herdado); sem migration
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** staff em `/campanha/apoiadores` (lista e detalhe) cadastra / importa sem voltar ao Início — via o chrome de ações rápidas (FAB/overlay) já montado para staff em toda rota fora do Início e `/acoes/*`.
|
||||
- **O que NÃO negociar:** staff-only nesta rota (leader → B89 Contatos, não esta rota); sem novo Consent (launchers apontam para os flows C2 existentes, que fail-closed); sem A1–A5 como primários (recomendação da intenção: **A — não incluir**); busca já vive no overlay (B91 ✓).
|
||||
- **O que reavaliar:** a intenção assume "atalho import se houver URL estável" — a rota `/campanha/apoiadores/importar` existe e é estável (wizard 3 passos; apontar a ela = passo 1 = upload, sem bypass de preview — revisão obrigatória da intenção satisfeita). O gating do import no código é **coordinator-only** (`isCampaignCoordinator` na página e redirect no `importar/page.tsx`), mais restrito que `isUnrestrictedCampaignRole` — o catálogo do drawer deve espelhar esse gate.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Fab["CampaignQuickActionsHost (staff, pathname)"] --> Reg["resolveQuickActionsForPath"]
|
||||
Reg --> Supp["supporterQuickActions.ts (B86)"]
|
||||
Supp --> List["lista / apoiadores"]
|
||||
Supp --> Detail["detalhe /apoiadores/:id (numérico)"]
|
||||
List --> Cad["register-supporter → /apoiadores/novo (staff)"]
|
||||
List --> Imp["import-supporters → /apoiadores/importar (coordinator)"]
|
||||
Detail --> Cad
|
||||
Detail --> Imp
|
||||
Reg -->|"novo|importar não casam"| Empty["[] (FAB = só busca)"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A | B | C
|
||||
**Recomendação:** **A** — módulo novo `supporterQuickActions.ts` espelhando `campaignAdvisorQuickActions.ts` (B87), com superfície lista+detalhe, catálogo de 2 launchers, import gated a coordinator; registro no registry central; testes unit espelhando B87/B85.
|
||||
**Rejeitadas:**
|
||||
|
||||
- **B — superfície inclui `/apoiadores/novo` e `/apoiadores/importar` com o mesmo catálogo:** redundante (a página já é o próprio formulário/wizard); precedente B85 exclui `nova` do detalhe e deixa a página de form com FAB só-busca. Rejeitada.
|
||||
- **C — "Importar CSV" para todo staff (`isUnrestrictedCampaignRole`):** espelha a intenção mas NÃO o gate de servidor — `importar/page.tsx` redireciona não-coordinator para `/apoiadores`; o launcher levaria advisor/candidate a um redirect. O drawer deve espelhar o gate da página (coordinator-only), como o botão "Importar CSV" da lista faz.
|
||||
- **D — catálogo com A1–A5 do Início (prefill de município):** rejeitado pela própria intenção (questão em aberto → recomendação A) e não há município garantido no contexto da ficha de apoiador.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`CAMPAIGN_SUPPORTERS_HOME`** (`src/lib/campaignPaths.ts`): constante `/campanha/apoiadores`, padrão dos irmãos (`CAMPAIGN_DEMANDS_HOME`, `CAMPAIGN_ADVISORS_HOME`).
|
||||
- **`supporterQuickActions.ts`** (`src/lib/`): `isSupportersListPath` (match exato), `parseSupporterDetailId` (regex `^/campanha/apoiadores/(\d+)(?:\/|$)`, como `parseAdvisorDetailId`), `isSupportersPath`, `resolveSupporterListQuickActions` / `resolveSupporterDetailQuickActions` / `resolveSupporterQuickActionsForPath`. Gate: `isStaffCampaignRole` (coordinator/advisor/candidate — igual a `canAccessSupporterArea`); `import-supporters` adicional só `role === 'coordinator'` (espelha `isCampaignCoordinator`).
|
||||
- **`campaignQuickActionRegistry.ts`**: entrada nova delegando para `resolveSupporterQuickActionsForPath`, agrupada com as verticais (após advisor, antes de territórios). Sem colisão de path com outros matchers.
|
||||
- **Actions:** `register-supporter` (label "Cadastrar apoiador", `UserPlus`, href `/campanha/apoiadores/novo` — id igual ao da intenção/i18n) e `import-supporters` (label "Importar CSV", `FileUp`, href `/campanha/apoiadores/importar`). `CampaignQuickAction` client-safe; overlay já renderiza via `CampaignHomeActionStrip variant="grid"`.
|
||||
- **Migration:** sem migration; sem collection; sem Consent; sem server action nova.
|
||||
- **Access / Consent:** role predicates client-safe (`isStaffCampaignRole` / `role === 'coordinator'`); nenhuma escrita; launchers para flows existentes fail-closed.
|
||||
- **UI:** Impeccable **B** (a intenção pede "Craft + Revisão na implementação") — a superfície é o chrome existente (FAB/overlay B100+), sem shape novo; nada a renderizar além do que o registro devolve. Mount já cobre staff em `/apoiadores/**` (`shouldMountQuickActionsFab`); leader já fica de fora (mount só em Contatos).
|
||||
|
||||
### Dados → forma (se aplicável)
|
||||
|
||||
- N/A — sem dados novos; launchers de navegação (hrefs), mesma forma dos catálogos B80–B90.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Tracer / schema+server** — constante de path + módulo `supporterQuickActions.ts` + entrada no registry; testes unit novos (`supporterQuickActions.unit.spec.ts`) + atualização do pin "unregistered paths" e teste de delegação no `campaignQuickAction.unit.spec.ts`.
|
||||
2. **UI** — nenhuma mudança de componente; verificação via unit (catálogo por role/path) + smoke manual do FAB em `/campanha/apoiadores` se o dev server estiver disponível.
|
||||
3. **Gates** — `pnpm gate:fast` na iteração; entrega com `pnpm push`; PR Ready `--base main` + `Closes #22` + auto-merge.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **Context sync / prefill de município na ficha de apoiador** (`CampaignQuickActionContextSync`): o apoiador tem `municipality` opcional, mas a intenção não pede A1–A5 — fora de escopo; gatilho se a ficha ganhar wizards.
|
||||
- **Tocar os botões da página `/apoiadores`** ("Novo" / "Importar CSV"): o botão da lista já cobre desktop; o drawer cobre mobile — não duplicar nem refatorar (o catálogo é independente).
|
||||
- **B79 drawer antigo / `CampaignQuickActionsDrawer`**: arquitetura atual é FAB+overlay (B100+); nenhum resquício a alterar.
|
||||
- **`needs:consent` na Issue:** herdado da vertical C2 (bloqueio de produção para dados reais); B86 só navega para flows existentes fail-closed — não coleta nada, não depende de chave nova.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Gate de import divergir entre drawer e página** (advisor/candidate vendo "Importar CSV"): mitigado — gate `role === 'coordinator'` espelha `isCampaignCoordinator`, pinado em teste.
|
||||
- **Ficha de apoiador com id não-numérico futuro** (slug): regex numérica só casa ids atuais; se virar slug, o parser evolui com a rota (mesmo caminho do assessor).
|
||||
- **Colisão de path no registry:** `/campanha/apoiadores/*` não é reivindicado por nenhum matcher existente; teste de delegação pinado.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto (staff cadastra/importa do drawer; leader fora; sem A1–A5; busca já no overlay)
|
||||
- [ ] Invariantes AGENTS/engineering-standards (client-safe, sem escrita, sem migration/Consent)
|
||||
- [ ] Testes de domínio previstos (unit: paths, catálogo por role — coordinator 2 ações, advisor/candidate 1, leader `[]`; registry: delegação + unregistered paths atualizado)
|
||||
75
docs/plans/escala-dry-pos-acoes-rapidas.md
Normal file
75
docs/plans/escala-dry-pos-acoes-rapidas.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Escala/DRY pós-B86 — normalização de pathname de lista nas ações rápidas
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-08-03
|
||||
Issue: —
|
||||
Prioridade: P2
|
||||
Item do roadmap: débito do lote B80–B90 (colhido no `/simplify` de B86, triage aprovada)
|
||||
Model: composer-2.5
|
||||
Impeccable: A — refactor só-backend (client-safe), sem mudança de comportamento
|
||||
Appetite: ~0,25–0,5 dia eng; sem migration
|
||||
|
||||
## Problema
|
||||
|
||||
O conhecimento de "normalizar pathname de lista (match exato + trailing slash)" está
|
||||
duplicado em **6+ módulos** da família de ações rápidas:
|
||||
|
||||
- `isAdvisorsListPath` (`src/lib/campaignAdvisorQuickActions.ts`)
|
||||
- `isDemandsListPath` (`src/lib/campaignQuickActionDemands.ts`)
|
||||
- `isStateDeputyListPath` (`src/lib/campaignQuickActionDobradinhas.ts`)
|
||||
- `isConceptsPath` / `isProfilePath` (`src/lib/campaignReferenceQuickActions.ts`)
|
||||
- `isLeadershipListPath` (`src/lib/campaignQuickActionLeadership.ts`)
|
||||
- `isSupportersListPath` (`src/lib/supporterQuickActions.ts` — B86)
|
||||
|
||||
E `src/lib/campaignQuickActionPaths.ts` já existe como home da família de path helpers
|
||||
client-safe, com um `normalizePathname` **privado** fazendo exatamente o mesmo trabalho
|
||||
(só usado pelos parsers de superfície activity/organization).
|
||||
|
||||
## Escopo
|
||||
|
||||
- Extrair helper compartilhado (ex. `isListPath(pathname, home)` e/ou
|
||||
`normalizePathname` exportado) em `src/lib/campaignQuickActionPaths.ts`.
|
||||
- Migrar os 6+ call sites acima para o helper; manter exports públicos de cada módulo
|
||||
(`isXListPath`) para não quebrar specs/registry — os wrappers delegam.
|
||||
- Se natural, derivar as regex de detalhe numérico da constante de home
|
||||
(`escapeRegExp`) — só se o diff ficar limpo; caso contrário, registrar como fase 2
|
||||
com gatilho (3º vertical com `[id]` numérico — hoje são 2: advisor + supporter).
|
||||
- **Não** mexer nos parsers de superfície activity/organization além de reusar o
|
||||
`normalizePathname` exportado (mesma lógica, menos duplicação).
|
||||
|
||||
## Já resolvido no simplify (não reabrir)
|
||||
|
||||
- Pins de teste do B86 (trailing-slash no registry, `/abc` → `[]`, mount staff em
|
||||
`/apoiadores` e não-leader) — aplicados na sessão B86.
|
||||
|
||||
## Explicitamente fora (deste lote)
|
||||
|
||||
- **S2** naming polarity `import-supporters` × `SUPPORTER_IMPORT_HREF` — descartado
|
||||
(batch plural intencional; score 2).
|
||||
- **S3** regex aceita `042` — descartado (espelha `parseAdvisorDetailId`; consistência).
|
||||
- **S4** caso advisor direto no spec do módulo — descartado (coberto via registry).
|
||||
- **S5** quirks de regex (nested tail, dígitos ilimitados, id≤0) — descartados
|
||||
(espelham siblings).
|
||||
- **S6** extrair regex numérica em 2 sites — **defer com gatilho**: 3º vertical com
|
||||
`[id]` numérico.
|
||||
- **S7** cache `Map<CampaignRole>` em 3 sites — **defer com gatilho**: 4º site com
|
||||
shape compatível (advisor/supporter/home divergem em gate e construção).
|
||||
- **S8** id `register-supporter` sobrecarregado (leader × staff) — **defer com
|
||||
gatilho**: se algo agregar catálogos ou assert de unicidade global de id.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. Helper compartilhado em `campaignQuickActionPaths.ts` + migração dos 6+ call sites.
|
||||
2. `pnpm gate:fast` (unit cobre todos os módulos tocados) + `pnpm push`.
|
||||
|
||||
## Rabbit holes
|
||||
|
||||
- Renomear `isXListPath` nos specs — os wrappers mantêm o contrato, specs intactos.
|
||||
- Tocar comportamento (ex. normalizar pathname em vez de comparar os dois spelling) —
|
||||
refactor é puramente estrutural; comportamento idêntico.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- 6+ call sites = risco de diff ruidoso — wrappers delegando mantêm o diff mínimo.
|
||||
- Algum matcher depende da ordem exata dos `===` — unit specs existentes pinam cada
|
||||
módulo; rodar `test:unit` completo no gate.
|
||||
|
|
@ -7,6 +7,8 @@ export const CAMPAIGN_DEMANDS_HOME = '/campanha/demandas' as const
|
|||
|
||||
export const CAMPAIGN_ADVISORS_HOME = '/campanha/assessores' as const
|
||||
|
||||
export const CAMPAIGN_SUPPORTERS_HOME = '/campanha/apoiadores' as const
|
||||
|
||||
export const LEADER_CONTACTS_HOME = '/campanha/contatos' as const
|
||||
|
||||
export const CAMPAIGN_PROFILE_HOME = '/campanha/perfil' as const
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
import { resolveLeaderContactsQuickActions } from '@/lib/leaderContactsQuickActions'
|
||||
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
|
||||
import { resolveSupporterQuickActionsForPath } from '@/lib/supporterQuickActions'
|
||||
|
||||
const isTerritoriesListPath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_TERRITORIES_HOME || pathname.startsWith(`${CAMPAIGN_TERRITORIES_HOME}/`)
|
||||
|
|
@ -82,6 +83,9 @@ export const resolveQuickActionsForPath = (
|
|||
const advisorActions = resolveAdvisorQuickActionsForPath(pathname, role, context)
|
||||
if (advisorActions.length > 0) return advisorActions
|
||||
|
||||
const supporterActions = resolveSupporterQuickActionsForPath(pathname, role, context)
|
||||
if (supporterActions.length > 0) return supporterActions
|
||||
|
||||
if (isTerritoriesListPath(pathname)) {
|
||||
return resolveStaffHomeQuickActions(role, pathname)
|
||||
}
|
||||
|
|
|
|||
79
src/lib/supporterQuickActions.ts
Normal file
79
src/lib/supporterQuickActions.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { FileUp, UserPlus } from 'lucide-react'
|
||||
|
||||
import { CAMPAIGN_SUPPORTERS_HOME } from '@/lib/campaignPaths'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
|
||||
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
export const SUPPORTER_CREATE_HREF = `${CAMPAIGN_SUPPORTERS_HOME}/novo` as const
|
||||
|
||||
export const SUPPORTER_IMPORT_HREF = `${CAMPAIGN_SUPPORTERS_HOME}/importar` as const
|
||||
|
||||
const supporterDetailPathPattern = /^\/campanha\/apoiadores\/(\d+)(?:\/|$)/
|
||||
|
||||
export const isSupportersListPath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_SUPPORTERS_HOME || pathname === `${CAMPAIGN_SUPPORTERS_HOME}/`
|
||||
|
||||
export const parseSupporterDetailId = (pathname: string): number | undefined => {
|
||||
const match = pathname.match(supporterDetailPathPattern)
|
||||
if (!match) return undefined
|
||||
const id = Number(match[1])
|
||||
return Number.isInteger(id) && id > 0 ? id : undefined
|
||||
}
|
||||
|
||||
export const isSupportersPath = (pathname: string): boolean =>
|
||||
isSupportersListPath(pathname) || parseSupporterDetailId(pathname) !== undefined
|
||||
|
||||
const registerSupporterAction: CampaignQuickAction = {
|
||||
id: 'register-supporter',
|
||||
label: 'Cadastrar apoiador',
|
||||
icon: UserPlus,
|
||||
description: 'Registrar um apoiador no cadastro nominal',
|
||||
href: SUPPORTER_CREATE_HREF,
|
||||
}
|
||||
|
||||
const importSupportersAction: CampaignQuickAction = {
|
||||
id: 'import-supporters',
|
||||
label: 'Importar CSV',
|
||||
icon: FileUp,
|
||||
description: 'Importar apoiadores em lote a partir de planilha',
|
||||
href: SUPPORTER_IMPORT_HREF,
|
||||
}
|
||||
|
||||
const coordinatorSupporterQuickActions: readonly CampaignQuickAction[] = [
|
||||
registerSupporterAction,
|
||||
importSupportersAction,
|
||||
]
|
||||
|
||||
const staffSupporterQuickActions: readonly CampaignQuickAction[] = [registerSupporterAction]
|
||||
|
||||
const supporterQuickActionsByRole = new Map<CampaignRole, readonly CampaignQuickAction[]>()
|
||||
|
||||
const supporterQuickActionsForRole = (role: CampaignRole): readonly CampaignQuickAction[] => {
|
||||
if (!isStaffCampaignRole(role)) return []
|
||||
const cached = supporterQuickActionsByRole.get(role)
|
||||
if (cached) return cached
|
||||
// Import is coordinator-only — mirrors the page gate (`isCampaignCoordinator`);
|
||||
// the import route redirects non-coordinators back to the list.
|
||||
const actions =
|
||||
role === 'coordinator' ? coordinatorSupporterQuickActions : staffSupporterQuickActions
|
||||
supporterQuickActionsByRole.set(role, actions)
|
||||
return actions
|
||||
}
|
||||
|
||||
export const resolveSupporterListQuickActions = (
|
||||
role: CampaignRole,
|
||||
): readonly CampaignQuickAction[] => supporterQuickActionsForRole(role)
|
||||
|
||||
export const resolveSupporterDetailQuickActions = (
|
||||
role: CampaignRole,
|
||||
): readonly CampaignQuickAction[] => supporterQuickActionsForRole(role)
|
||||
|
||||
export const resolveSupporterQuickActionsForPath = (
|
||||
pathname: string,
|
||||
role: CampaignRole,
|
||||
_context: CampaignQuickActionContext,
|
||||
): readonly CampaignQuickAction[] => {
|
||||
if (!isSupportersPath(pathname)) return []
|
||||
return supporterQuickActionsForRole(role)
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from '@/lib/campaignQuickActionPaths'
|
||||
import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry'
|
||||
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
|
||||
import { SUPPORTER_CREATE_HREF, SUPPORTER_IMPORT_HREF } from '@/lib/supporterQuickActions'
|
||||
|
||||
describe('campaignQuickActionMount', () => {
|
||||
it('treats Início as exact match only', () => {
|
||||
|
|
@ -62,6 +63,8 @@ describe('campaignQuickActionMount', () => {
|
|||
expect(shouldMountQuickActionsFab('/campanha/territorios', 'coordinator')).toBe(true)
|
||||
expect(shouldMountQuickActionsFab('/campanha/demandas', 'coordinator')).toBe(true)
|
||||
expect(shouldMountQuickActionsFab('/campanha/demandas/foo', 'coordinator')).toBe(true)
|
||||
expect(shouldMountQuickActionsFab('/campanha/apoiadores', 'coordinator')).toBe(true)
|
||||
expect(shouldMountQuickActionsFab('/campanha/apoiadores/42', 'coordinator')).toBe(true)
|
||||
expect(shouldMountQuickActionsFab('/campanha', 'coordinator')).toBe(false)
|
||||
expect(shouldMountQuickActionsFab('/campanha/acoes/registrar-sinal', 'advisor')).toBe(false)
|
||||
})
|
||||
|
|
@ -69,6 +72,7 @@ describe('campaignQuickActionMount', () => {
|
|||
it('mounts for leader only on contacts', () => {
|
||||
expect(shouldMountQuickActionsFab('/campanha/contatos', 'leader')).toBe(true)
|
||||
expect(shouldMountQuickActionsFab('/campanha/municipios', 'leader')).toBe(false)
|
||||
expect(shouldMountQuickActionsFab('/campanha/apoiadores', 'leader')).toBe(false)
|
||||
})
|
||||
|
||||
it('skips the E13 tour composer (B84)', () => {
|
||||
|
|
@ -257,7 +261,7 @@ describe('campaignQuickActionRegistry', () => {
|
|||
})
|
||||
|
||||
it('returns empty catalog for unregistered paths', () => {
|
||||
expect(resolveQuickActionsForPath('/campanha/apoiadores', 'coordinator', {})).toEqual([])
|
||||
expect(resolveQuickActionsForPath('/campanha/lugar-inexistente', 'coordinator', {})).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty catalog on conceitos and perfil for staff (B90)', () => {
|
||||
|
|
@ -368,6 +372,31 @@ describe('campaignQuickActionRegistry', () => {
|
|||
it('returns empty catalog on assessores for advisor lockdown', () => {
|
||||
expect(resolveQuickActionsForPath('/campanha/assessores', 'advisor', {})).toEqual([])
|
||||
})
|
||||
|
||||
it('delegates apoiadores list and detail catalogs (B86)', () => {
|
||||
const list = resolveQuickActionsForPath('/campanha/apoiadores', 'coordinator', {})
|
||||
expect(list.map((action) => action.id)).toEqual(['register-supporter', 'import-supporters'])
|
||||
expect(list[0]?.href).toBe(SUPPORTER_CREATE_HREF)
|
||||
expect(list[1]?.href).toBe(SUPPORTER_IMPORT_HREF)
|
||||
|
||||
const advisorList = resolveQuickActionsForPath('/campanha/apoiadores', 'advisor', {})
|
||||
expect(advisorList.map((action) => action.id)).toEqual(['register-supporter'])
|
||||
|
||||
const detail = resolveQuickActionsForPath('/campanha/apoiadores/9', 'candidate', {})
|
||||
expect(detail.map((action) => action.id)).toEqual(['register-supporter'])
|
||||
|
||||
const listTrailingSlash = resolveQuickActionsForPath('/campanha/apoiadores/', 'advisor', {})
|
||||
expect(listTrailingSlash.map((action) => action.id)).toEqual(['register-supporter'])
|
||||
|
||||
const detailTrailingSlash = resolveQuickActionsForPath(
|
||||
'/campanha/apoiadores/9/',
|
||||
'candidate',
|
||||
{},
|
||||
)
|
||||
expect(detailTrailingSlash.map((action) => action.id)).toEqual(['register-supporter'])
|
||||
|
||||
expect(resolveQuickActionsForPath('/campanha/apoiadores', 'leader', {})).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('campaignQuickActionDemands paths', () => {
|
||||
|
|
|
|||
94
tests/unit/supporterQuickActions.unit.spec.ts
Normal file
94
tests/unit/supporterQuickActions.unit.spec.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
SUPPORTER_CREATE_HREF,
|
||||
SUPPORTER_IMPORT_HREF,
|
||||
isSupportersListPath,
|
||||
isSupportersPath,
|
||||
parseSupporterDetailId,
|
||||
resolveSupporterDetailQuickActions,
|
||||
resolveSupporterListQuickActions,
|
||||
resolveSupporterQuickActionsForPath,
|
||||
} from '@/lib/supporterQuickActions'
|
||||
|
||||
describe('supporterQuickActions paths', () => {
|
||||
it('matches apoiadores list exactly', () => {
|
||||
expect(isSupportersListPath('/campanha/apoiadores')).toBe(true)
|
||||
expect(isSupportersListPath('/campanha/apoiadores/')).toBe(true)
|
||||
expect(isSupportersListPath('/campanha/apoiadores/novo')).toBe(false)
|
||||
expect(isSupportersListPath('/campanha/apoiadores/importar')).toBe(false)
|
||||
})
|
||||
|
||||
it('matches apoiadores detail ids', () => {
|
||||
expect(parseSupporterDetailId('/campanha/apoiadores/42')).toBe(42)
|
||||
expect(parseSupporterDetailId('/campanha/apoiadores/42/')).toBe(42)
|
||||
expect(parseSupporterDetailId('/campanha/apoiadores/novo')).toBeUndefined()
|
||||
expect(parseSupporterDetailId('/campanha/apoiadores/importar')).toBeUndefined()
|
||||
expect(parseSupporterDetailId('/campanha/apoiadores/abc')).toBeUndefined()
|
||||
expect(isSupportersPath('/campanha/apoiadores/42')).toBe(true)
|
||||
expect(isSupportersPath('/campanha/apoiadores/novo')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveSupporterListQuickActions', () => {
|
||||
it('returns register-supporter + import-supporters for coordinator', () => {
|
||||
const actions = resolveSupporterListQuickActions('coordinator')
|
||||
expect(actions.map((action) => action.id)).toEqual(['register-supporter', 'import-supporters'])
|
||||
expect(actions[0]?.href).toBe(SUPPORTER_CREATE_HREF)
|
||||
expect(actions[1]?.href).toBe(SUPPORTER_IMPORT_HREF)
|
||||
})
|
||||
|
||||
it('returns register-supporter only for advisor and candidate', () => {
|
||||
for (const role of ['advisor', 'candidate'] as const) {
|
||||
const actions = resolveSupporterListQuickActions(role)
|
||||
expect(actions.map((action) => action.id)).toEqual(['register-supporter'])
|
||||
expect(actions[0]?.href).toBe(SUPPORTER_CREATE_HREF)
|
||||
}
|
||||
})
|
||||
|
||||
it('returns empty catalog for leader lockdown', () => {
|
||||
expect(resolveSupporterListQuickActions('leader')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveSupporterDetailQuickActions', () => {
|
||||
it('mirrors the list catalog on detail (B86)', () => {
|
||||
expect(resolveSupporterDetailQuickActions('coordinator').map((action) => action.id)).toEqual([
|
||||
'register-supporter',
|
||||
'import-supporters',
|
||||
])
|
||||
expect(resolveSupporterDetailQuickActions('advisor').map((action) => action.id)).toEqual([
|
||||
'register-supporter',
|
||||
])
|
||||
expect(resolveSupporterDetailQuickActions('leader')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveSupporterQuickActionsForPath', () => {
|
||||
it('delegates list and detail catalogs (B86)', () => {
|
||||
const list = resolveSupporterQuickActionsForPath('/campanha/apoiadores', 'coordinator', {})
|
||||
expect(list.map((action) => action.id)).toEqual(['register-supporter', 'import-supporters'])
|
||||
|
||||
const detail = resolveSupporterQuickActionsForPath('/campanha/apoiadores/7', 'coordinator', {})
|
||||
expect(detail.map((action) => action.id)).toEqual(['register-supporter', 'import-supporters'])
|
||||
})
|
||||
|
||||
it('returns empty catalog on form and wizard pages (search-only FAB)', () => {
|
||||
expect(
|
||||
resolveSupporterQuickActionsForPath('/campanha/apoiadores/novo', 'coordinator', {}),
|
||||
).toEqual([])
|
||||
expect(
|
||||
resolveSupporterQuickActionsForPath('/campanha/apoiadores/importar', 'coordinator', {}),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty catalog on non-numeric detail (route 404s)', () => {
|
||||
expect(
|
||||
resolveSupporterQuickActionsForPath('/campanha/apoiadores/abc', 'coordinator', {}),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty catalog for leader lockdown', () => {
|
||||
expect(resolveSupporterQuickActionsForPath('/campanha/apoiadores', 'leader', {})).toEqual([])
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue