diff --git a/docs/plans/acoes-rapidas-apoiadores-impl.md b/docs/plans/acoes-rapidas-apoiadores-impl.md new file mode 100644 index 00000000..e01f4842 --- /dev/null +++ b/docs/plans/acoes-rapidas-apoiadores-impl.md @@ -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) diff --git a/docs/plans/escala-dry-pos-acoes-rapidas.md b/docs/plans/escala-dry-pos-acoes-rapidas.md new file mode 100644 index 00000000..7e0e33b8 --- /dev/null +++ b/docs/plans/escala-dry-pos-acoes-rapidas.md @@ -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` 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. diff --git a/src/lib/campaignPaths.ts b/src/lib/campaignPaths.ts index 56c0bf02..88f24e8e 100644 --- a/src/lib/campaignPaths.ts +++ b/src/lib/campaignPaths.ts @@ -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 diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index 6caa1a41..d3860d8c 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -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) } diff --git a/src/lib/supporterQuickActions.ts b/src/lib/supporterQuickActions.ts new file mode 100644 index 00000000..371b7caf --- /dev/null +++ b/src/lib/supporterQuickActions.ts @@ -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() + +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) +} diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts index 83e89c39..30447756 100644 --- a/tests/unit/campaignQuickAction.unit.spec.ts +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -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', () => { diff --git a/tests/unit/supporterQuickActions.unit.spec.ts b/tests/unit/supporterQuickActions.unit.spec.ts new file mode 100644 index 00000000..0e976925 --- /dev/null +++ b/tests/unit/supporterQuickActions.unit.spec.ts @@ -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([]) + }) +})