mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Merge branch 'cursor/3a35211d' (B45: catálogo de ações do Início por persona)
This commit is contained in:
commit
60dcbb23c8
13 changed files with 327 additions and 23 deletions
|
|
@ -7,10 +7,11 @@ alwaysApply: false
|
|||
|
||||
## Status
|
||||
|
||||
- **B46–B55 registrados no roadmap (2026-07-29, sem código):** thumb-zone do Início (**B46**) + busca global fatiada (**B47** input → **B48** Municípios/TIs → **B49–B53** outros grupos → **B54** grid/cap → **B55** WA/share). Cadeia: B45 → B46 → B47 → …. Planos em `docs/plans/posicao-botoes-*` e `busca-global-*`. Assumido: staff-only; clique = rotas de detalhe existentes; TIs no grupo Municípios.
|
||||
- **B44 entregue em código (2026-07-29):** primitivo UX-1 — `CampaignHomeActionButton` (círculo `bg-muted` + ícone + label; um só controle focável) e `CampaignHomeActionStrip` (`overflow-x-auto`, `touch-pan-x`, snap). Descrição: Tooltip shadcn em ponteiro fino; long-press (~450ms) abre Drawer em ponteiro grosso (`useSyncExternalStore` + `matchMedia`, um alvo no DOM) sem disparar click (`src/lib/campaignLongPress.ts`). **Sem** montar catálogo em `/campanha` — B45. Specs: `campaignLongPress.unit.spec.tsx`, `campaignHomeActionButton.unit.spec.tsx`. Plano: [`botao-acao-inicio-strip.md`](../../../docs/plans/botao-acao-inicio-strip.md). Desbloqueia **B45**.
|
||||
- **B45 entregue em código (2026-07-29):** catálogo de ações do Início por persona (UX-1 chassis) — `src/lib/campaignHomeActions.ts` (`homeActionsForRole`, `toHomeActionButtonProps`) + ilha client `CampaignHomeActions` (ícones Lucide no client; RSC passa `role` + `uncoveredMunicipalitiesHref` de `buildMunicipalityListHref`). Staff (coordinator/candidate/advisor): 6 ações; `uncovered-municipalities` → `?coverage=sem_assessor&sort=votos`; demais inertes (`cursor-default`). Leader: 2 ações; `my-contacts` → `/campanha/contatos`. Seção "O que você quer fazer?" acima da strip B44. Unit `campaignHomeActions.unit.spec.ts`; e2e `campaignHomeActions.e2e.spec.ts`. Sem migration. Plano: [`catalogo-acoes-inicio-por-persona.md`](../../../docs/plans/catalogo-acoes-inicio-por-persona.md). Próximo: **B46** thumb-zone → **B47–B55** busca global.
|
||||
- **B46–B55 registrados no roadmap (2026-07-29, sem código):** thumb-zone do Início (**B46**) + busca global fatiada (**B47** input → **B48** Municípios/TIs → **B49–B53** outros grupos → **B54** grid/cap → **B55** WA/share). Cadeia: B45 ✓ → B46 → B47 → …. Planos em `docs/plans/posicao-botoes-*` e `busca-global-*`. Assumido: staff-only; clique = rotas de detalhe existentes; TIs no grupo Municípios.
|
||||
- **B44 entregue em código (2026-07-29):** primitivo UX-1 — `CampaignHomeActionButton` (círculo `bg-muted` + ícone + label; um só controle focável) e `CampaignHomeActionStrip` (`overflow-x-auto`, `touch-pan-x`, snap). Descrição: Tooltip shadcn em ponteiro fino; long-press (~450ms) abre Drawer em ponteiro grosso (`useSyncExternalStore` + `matchMedia`, um alvo no DOM) sem disparar click (`src/lib/campaignLongPress.ts`). Montado no Início por **B45** ✓. Specs: `campaignLongPress.unit.spec.tsx`, `campaignHomeActionButton.unit.spec.tsx`. Plano: [`botao-acao-inicio-strip.md`](../../../docs/plans/botao-acao-inicio-strip.md).
|
||||
- **B43 entregue em código (2026-07-29):** chassis UX-1 — `/campanha` fica só shell (`CampaignPageShell` vazio); dashboard staff move para `/campanha/quadro` (corpo idêntico ao antigo Início); `LeaderContactsPanel` em `/campanha/contatos`; nav staff ganha **Quadro** (sidebar; fora do bottom nav mobile); liderança **Início** + **Meus contatos**; `requireCampaignPageActor` redireciona leader bloqueado para `/campanha/contatos` (não para o Início blank). Sem migration. Plano: [`inicio-em-branco-quadro.md`](../../../docs/plans/inicio-em-branco-quadro.md). Desbloqueia **B44** / **B45**.
|
||||
- **Sessão observada CG 2026-07-29 (Bloco B):** 1º hands-on ~54 min. Achados U1–U8 / O9–O11 — não rola; mental model **Ação→Local→Quem**; confirmações invisíveis; forms precisam ser contínuos; hit = lista sem assessor × votos 2022; mapa ≠ mesa de trabalho. Pivot: tabelas=bulk; operação=fluxos lineares tipo Nubank. Snapshot [`sessao-observada-coordenador-2026-07-29-snapshot.md`](../../../docs/plans/sessao-observada-coordenador-2026-07-29-snapshot.md); CUSTOMER.md/OST atualizados. Prazo soft: manejável até reunião assessoria **03/08**. **Chassis:** ~~B43~~ ✓ → ~~B44~~ ✓ → **B45** catálogo → **B46** thumb-zone → **B47–B55** busca global (wizards depois).
|
||||
- **Sessão observada CG 2026-07-29 (Bloco B):** 1º hands-on ~54 min. Achados U1–U8 / O9–O11 — não rola; mental model **Ação→Local→Quem**; confirmações invisíveis; forms precisam ser contínuos; hit = lista sem assessor × votos 2022; mapa ≠ mesa de trabalho. Pivot: tabelas=bulk; operação=fluxos lineares tipo Nubank. Snapshot [`sessao-observada-coordenador-2026-07-29-snapshot.md`](../../../docs/plans/sessao-observada-coordenador-2026-07-29-snapshot.md); CUSTOMER.md/OST atualizados. Prazo soft: manejável até reunião assessoria **03/08**. **Chassis:** ~~B43~~ ✓ → ~~B44~~ ✓ → ~~B45~~ ✓ → **B46** thumb-zone → **B47–B55** busca global (wizards depois).
|
||||
- **E12+ entregue em código (2026-07-29):** escala/DRY pós-E12 — F1 forçada: `src/lib/metropolitanoTerritoryPeers.ts` unifica constantes, filtros Salvador vs demais RMS e `catalogPeersForSlug` (rollup E17/E12, `slugsForMetropolitanoSubRow`, benchmark T4). F2 não entrou — `benchmarkBySlug` já memoiza por slug. Spec `metropolitanoTerritoryPeers.unit.spec.ts`. Sem migration/UI. Plano: [`escala-dry-pos-e12.md`](../../../docs/plans/escala-dry-pos-e12.md).
|
||||
- **B17+ entregue em código (2026-07-29):** Escala/DRY pós-B17 — F1: `readCampaignColumnVisibility` antes do `Promise.all` e `loadLeadershipOptions` / `loadStateDeputyOptions` só quando a coluna de relação está visível (`resolveVisibleColumns`). F2: `CampaignListEmptyState` na prop `empty` de `CampaignTable` (e `SupporterList`) em demandas, organizações e apoiadores, para o botão "Colunas" sobreviver com zero linhas. Plano: [`escala-dry-pos-b17.md`](../../../docs/plans/escala-dry-pos-b17.md).
|
||||
- **Fill-in remover coluna Tipo entregue em código (2026-07-29):** em `/campanha/municipios`, sumiram a coluna "Tipo", o filtro single-select no header (B16), o ramo mobile em `MunicipalityFilters`, a sort key `kind` e o param `?kind=` em `municipalityListUrl` / `buildMunicipalityListWhere` / `payloadSortFieldByKey` — bookmarks antigos ignoram `kind` no parse. `municipalityKindLabels` permanece para detalhe, mapa e dossiê. `MunicipalityHeaderFilter` ficou só com filtros multi (`singleRows`/`applyMunicipalityKindFilter` deletados). B17: 11 colunas staff; e2e do picker usa Território; B18 usa `?priority=alta`. Sem migration/action/Consent. Plano: [`remover-coluna-tipo-municipios.md`](../../../docs/plans/remover-coluna-tipo-municipios.md).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Catálogo de ações do Início por persona
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-07-29
|
||||
Status: entregue (2026-07-29)
|
||||
Atualizado em: 2026-07-29 — as-built: catálogo em `src/lib/campaignHomeActions.ts`; mount via `CampaignHomeActions` (client island — RSC não passa ícones Lucide); href de municípios sem assessor resolvido no server; e2e `campaignHomeActions.e2e.spec.ts`.
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B45 — chassis UX-1)
|
||||
Impeccable: B — mount da strip B44 no Início com catálogo por role
|
||||
Appetite: ~0,5 dia eng; módulo client-safe + page wire-up; 2 atalhos com href; resto inerte
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,10 +1,24 @@
|
|||
import { CampaignHomeActions } from '@/components/campaign/dashboard/CampaignHomeActions'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { isStaffCampaignRole } from '@/lib/campaignRoles'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import { buildMunicipalityListHref } from '@/utilities/municipality/municipalityListUrl'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function CampaignHomePage() {
|
||||
await requireCampaignPageActor()
|
||||
const user = await requireCampaignPageActor()
|
||||
|
||||
return <CampaignPageShell aria-label="Início">{null}</CampaignPageShell>
|
||||
const uncoveredMunicipalitiesHref = isStaffCampaignRole(user.role)
|
||||
? buildMunicipalityListHref({ page: 1, coverage: 'sem_assessor', sort: 'votos' }, 1)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<CampaignPageShell aria-label="Início">
|
||||
<CampaignHomeActions
|
||||
role={user.role}
|
||||
uncoveredMunicipalitiesHref={uncoveredMunicipalitiesHref}
|
||||
/>
|
||||
</CampaignPageShell>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useCoarsePointer } from '@/lib/campaignCoarsePointer'
|
||||
import { useCampaignLongPress } from '@/lib/campaignLongPress'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type CampaignHomeActionButtonProps = {
|
||||
label: string
|
||||
|
|
@ -69,6 +70,7 @@ export const CampaignHomeActionButton = ({
|
|||
)
|
||||
|
||||
const useLongPress = Boolean(description) && isCoarsePointer && !disabled
|
||||
const isInert = !disabled && !href && !onClick
|
||||
const longPress = useCampaignLongPress({
|
||||
enabled: useLongPress,
|
||||
onLongPress: openDescription,
|
||||
|
|
@ -76,7 +78,7 @@ export const CampaignHomeActionButton = ({
|
|||
})
|
||||
|
||||
const pointerHandlers = useLongPress ? longPress : undefined
|
||||
const className = actionControlClassName
|
||||
const className = cn(actionControlClassName, isInert && 'cursor-default')
|
||||
const aria = {
|
||||
'aria-label': label,
|
||||
'aria-describedby': drawerOpen && useLongPress ? descriptionId : undefined,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const CampaignHomeActionStrip = ({
|
|||
className,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
actions?: readonly CampaignHomeActionButtonProps[]
|
||||
actions?: readonly (CampaignHomeActionButtonProps & { id?: string })[]
|
||||
ariaLabel?: string
|
||||
className?: string
|
||||
}) => (
|
||||
|
|
@ -30,9 +30,9 @@ export const CampaignHomeActionStrip = ({
|
|||
>
|
||||
<ul role="list" className="m-0 flex min-w-max list-none snap-x snap-proximity gap-6 p-0 pb-1">
|
||||
{children}
|
||||
{actions?.map((action) => (
|
||||
<li key={action.href ?? action.label} className="m-0 list-none p-0">
|
||||
<CampaignHomeActionButton {...action} />
|
||||
{actions?.map(({ id, ...button }) => (
|
||||
<li key={id ?? button.href ?? button.label} className="m-0 list-none p-0">
|
||||
<CampaignHomeActionButton {...button} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
28
src/components/campaign/dashboard/CampaignHomeActions.tsx
Normal file
28
src/components/campaign/dashboard/CampaignHomeActions.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
'use client'
|
||||
|
||||
import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip'
|
||||
import { homeActionsForRole, toHomeActionButtonProps } from '@/lib/campaignHomeActions'
|
||||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
const homeActionsHeadingId = 'campaign-home-actions-heading'
|
||||
|
||||
export const CampaignHomeActions = ({
|
||||
role,
|
||||
uncoveredMunicipalitiesHref,
|
||||
}: {
|
||||
role: CampaignRole
|
||||
uncoveredMunicipalitiesHref?: string
|
||||
}) => {
|
||||
const actions = toHomeActionButtonProps(homeActionsForRole(role), uncoveredMunicipalitiesHref)
|
||||
|
||||
if (actions.length === 0) return null
|
||||
|
||||
return (
|
||||
<section aria-labelledby={homeActionsHeadingId} className="w-full">
|
||||
<h2 id={homeActionsHeadingId} className="text-base font-semibold text-foreground">
|
||||
O que você quer fazer?
|
||||
</h2>
|
||||
<CampaignHomeActionStrip actions={actions} className="mt-4 w-full" />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
|
||||
import { isStaffCampaignRole, isUnrestrictedCampaignRole } from '@/lib/campaignRoles'
|
||||
import type { CampaignUser } from '@/payload-types'
|
||||
import { canAccessSupporterArea } from '@/utilities/supporter/supporterUi'
|
||||
|
|
@ -59,7 +60,7 @@ const staffSecondaryNav: CampaignNavItem[] = [
|
|||
|
||||
const leaderNav: CampaignNavItem[] = [
|
||||
{ title: 'Início', href: '/campanha', icon: HomeIcon },
|
||||
{ title: 'Meus contatos', href: '/campanha/contatos', icon: UsersIcon },
|
||||
{ title: 'Meus contatos', href: LEADER_CONTACTS_HOME, icon: UsersIcon },
|
||||
]
|
||||
|
||||
export const getCampaignNav = (role: CampaignUser['role']): CampaignNavItem[] => {
|
||||
|
|
|
|||
132
src/lib/campaignHomeActions.ts
Normal file
132
src/lib/campaignHomeActions.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import type { LucideIcon } from 'lucide-react'
|
||||
import {
|
||||
BarChart3,
|
||||
Handshake,
|
||||
Inbox,
|
||||
Megaphone,
|
||||
TrendingUp,
|
||||
UserPlus,
|
||||
UserRoundSearch,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
|
||||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
import { isStaffCampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
type CampaignHomeActionId =
|
||||
| 'update-votes'
|
||||
| 'register-signal'
|
||||
| 'change-trend'
|
||||
| 'update-leadership'
|
||||
| 'register-demand'
|
||||
| 'uncovered-municipalities'
|
||||
| 'register-supporter'
|
||||
| 'my-contacts'
|
||||
|
||||
export type CampaignHomeAction = {
|
||||
id: CampaignHomeActionId
|
||||
label: string
|
||||
icon: LucideIcon
|
||||
description: string
|
||||
}
|
||||
|
||||
const staffHomeActionsCoordinator: readonly CampaignHomeAction[] = [
|
||||
{
|
||||
id: 'update-votes',
|
||||
label: 'Atualizar votos de um município',
|
||||
icon: BarChart3,
|
||||
description: 'Atualizar a projeção (média, pessimista, otimista) de um município',
|
||||
},
|
||||
{
|
||||
id: 'register-signal',
|
||||
label: 'Registrar o que aconteceu',
|
||||
icon: Megaphone,
|
||||
description: 'Anotar sinal urgente: invasão, perda de apoio, novo apoio, dificuldade',
|
||||
},
|
||||
{
|
||||
id: 'change-trend',
|
||||
label: 'Mudar tendência',
|
||||
icon: TrendingUp,
|
||||
description: 'Favorável, neutra ou desfavorável — com o porquê',
|
||||
},
|
||||
{
|
||||
id: 'update-leadership',
|
||||
label: 'Atualizar liderança',
|
||||
icon: Handshake,
|
||||
description: 'Trocar quem coordena, status de apoio ou votos que a pessoa declara',
|
||||
},
|
||||
{
|
||||
id: 'register-demand',
|
||||
label: 'Registrar pedido',
|
||||
icon: Inbox,
|
||||
description: 'Demanda de material, transporte, diária ou outro pedido do município',
|
||||
},
|
||||
{
|
||||
id: 'uncovered-municipalities',
|
||||
label: 'Ver quem ainda não está coberto',
|
||||
icon: UserRoundSearch,
|
||||
description: 'Abrir a lista de municípios sem assessor (atalho — não é wizard)',
|
||||
},
|
||||
]
|
||||
|
||||
const portfolioScopeSuffix = ' nos municípios da sua carteira'
|
||||
|
||||
const staffHomeActionsAdvisor: readonly CampaignHomeAction[] = staffHomeActionsCoordinator.map(
|
||||
(action) => {
|
||||
if (action.id === 'uncovered-municipalities') {
|
||||
return {
|
||||
...action,
|
||||
description:
|
||||
'Abrir a lista de municípios sem assessor da sua carteira (atalho — não é wizard)',
|
||||
}
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
description: `${action.description}${portfolioScopeSuffix}`,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const leaderHomeActions: readonly CampaignHomeAction[] = [
|
||||
{
|
||||
id: 'register-supporter',
|
||||
label: 'Cadastrar apoiador',
|
||||
icon: UserPlus,
|
||||
description: 'Registrar alguém da sua rede que apoia o Solla',
|
||||
},
|
||||
{
|
||||
id: 'my-contacts',
|
||||
label: 'Ver meus contatos',
|
||||
icon: Users,
|
||||
description: 'Abrir a lista dos apoiadores que você cadastrou',
|
||||
},
|
||||
]
|
||||
|
||||
export const homeActionsForRole = (role: CampaignRole): readonly CampaignHomeAction[] => {
|
||||
if (role === 'leader') return leaderHomeActions
|
||||
if (!isStaffCampaignRole(role)) return []
|
||||
if (role === 'advisor') return staffHomeActionsAdvisor
|
||||
return staffHomeActionsCoordinator
|
||||
}
|
||||
|
||||
export type ResolvedCampaignHomeAction = CampaignHomeAction & {
|
||||
href?: string
|
||||
}
|
||||
|
||||
export const toHomeActionButtonProps = (
|
||||
actions: readonly CampaignHomeAction[],
|
||||
uncoveredMunicipalitiesHref?: string,
|
||||
): ResolvedCampaignHomeAction[] =>
|
||||
actions.map((action) => {
|
||||
let href: string | undefined
|
||||
if (action.id === 'my-contacts') {
|
||||
href = LEADER_CONTACTS_HOME
|
||||
} else if (action.id === 'uncovered-municipalities') {
|
||||
href = uncoveredMunicipalitiesHref
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
...(href ? { href } : {}),
|
||||
}
|
||||
})
|
||||
2
src/lib/campaignPaths.ts
Normal file
2
src/lib/campaignPaths.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/** Client-safe `/campanha` route paths shared by nav, gates, and home actions. */
|
||||
export const LEADER_CONTACTS_HOME = '/campanha/contatos' as const
|
||||
|
|
@ -2,6 +2,7 @@ import 'server-only'
|
|||
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
|
||||
import { isStaffCampaignRole, isUnrestrictedCampaignRole } from '@/lib/campaignRoles'
|
||||
import { getCampaignUser } from '@/utilities/campaignAuth'
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ export type CampaignPageActor = NonNullable<Awaited<ReturnType<typeof getCampaig
|
|||
|
||||
export type CampaignPageGate = 'staff' | 'unrestricted' | 'noLeader'
|
||||
|
||||
export const LEADER_CONTACTS_HOME = '/campanha/contatos'
|
||||
export { LEADER_CONTACTS_HOME }
|
||||
|
||||
/** Staff dashboard (B43 — moved off blank `/campanha`). */
|
||||
export const CAMPAIGN_STAFF_QUADRO_PATH = '/campanha/quadro'
|
||||
|
|
|
|||
60
tests/e2e/campaignHomeActions.e2e.spec.ts
Normal file
60
tests/e2e/campaignHomeActions.e2e.spec.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { expect, test } from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
const staffActionLabels = [
|
||||
'Atualizar votos de um município',
|
||||
'Registrar o que aconteceu',
|
||||
'Mudar tendência',
|
||||
'Atualizar liderança',
|
||||
'Registrar pedido',
|
||||
'Ver quem ainda não está coberto',
|
||||
] as const
|
||||
|
||||
test.describe('Início — catálogo de ações (B45)', () => {
|
||||
test('staff sees six home actions and can open municipalities without coverage', async ({
|
||||
campaign,
|
||||
page,
|
||||
}) => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'O que você quer fazer?', exact: true }),
|
||||
).toBeVisible()
|
||||
|
||||
for (const label of staffActionLabels) {
|
||||
const link = page.getByRole('link', { name: label, exact: true })
|
||||
const button = page.getByRole('button', { name: label, exact: true })
|
||||
await expect(link.or(button)).toBeVisible()
|
||||
}
|
||||
|
||||
await page.getByRole('link', { name: 'Ver quem ainda não está coberto', exact: true }).click()
|
||||
await page.waitForURL(/\/campanha\/municipios/)
|
||||
const url = new URL(page.url())
|
||||
expect(url.searchParams.get('coverage')).toBe('sem_assessor')
|
||||
expect(url.searchParams.get('sort')).toBe('votos')
|
||||
})
|
||||
|
||||
test('leader sees two home actions and can open contacts', async ({ campaign, page }) => {
|
||||
const { fixtures } = campaign
|
||||
const leader = await fixtures.createCampaignUser('leader', {
|
||||
name: fixtures.value('Liderança'),
|
||||
})
|
||||
|
||||
await campaign.login(page, leader.email!, leader.password)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'O que você quer fazer?', exact: true }),
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Cadastrar apoiador', exact: true }),
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: 'Ver meus contatos', exact: true })).toBeVisible()
|
||||
|
||||
await page.getByRole('link', { name: 'Ver meus contatos', exact: true }).click()
|
||||
await page.waitForURL(/\/campanha\/contatos/)
|
||||
await expect(page.getByRole('heading', { name: /Meus contatos/ })).toBeVisible()
|
||||
})
|
||||
})
|
||||
63
tests/unit/campaignHomeActions.unit.spec.ts
Normal file
63
tests/unit/campaignHomeActions.unit.spec.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { homeActionsForRole, toHomeActionButtonProps } from '@/lib/campaignHomeActions'
|
||||
import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
|
||||
|
||||
const staffActionIds = [
|
||||
'update-votes',
|
||||
'register-signal',
|
||||
'change-trend',
|
||||
'update-leadership',
|
||||
'register-demand',
|
||||
'uncovered-municipalities',
|
||||
] as const
|
||||
|
||||
const leaderActionIds = ['register-supporter', 'my-contacts'] as const
|
||||
|
||||
describe('homeActionsForRole', () => {
|
||||
it.each(['coordinator', 'candidate'] as const)('returns six staff actions for %s', (role) => {
|
||||
const ids = homeActionsForRole(role).map((action) => action.id)
|
||||
expect(ids).toEqual([...staffActionIds])
|
||||
})
|
||||
|
||||
it('returns six staff actions for advisor with portfolio-scoped descriptions', () => {
|
||||
const actions = homeActionsForRole('advisor')
|
||||
expect(actions.map((action) => action.id)).toEqual([...staffActionIds])
|
||||
expect(
|
||||
actions.some((action) => action.description.includes('nos municípios da sua carteira')),
|
||||
).toBe(true)
|
||||
const uncovered = actions.find((action) => action.id === 'uncovered-municipalities')
|
||||
expect(uncovered?.description).toContain('sua carteira')
|
||||
expect(uncovered?.description).not.toContain('nos municípios da sua carteira nos municípios')
|
||||
})
|
||||
|
||||
it('returns two leader actions without municipality staff ids', () => {
|
||||
const actions = homeActionsForRole('leader')
|
||||
expect(actions.map((action) => action.id)).toEqual([...leaderActionIds])
|
||||
for (const staffId of staffActionIds) {
|
||||
expect(actions.some((action) => action.id === staffId)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('toHomeActionButtonProps', () => {
|
||||
it('wires my-contacts to the leader contacts route', () => {
|
||||
const [supporter, contacts] = toHomeActionButtonProps(homeActionsForRole('leader'))
|
||||
expect(supporter.href).toBeUndefined()
|
||||
expect(contacts.href).toBe(LEADER_CONTACTS_HOME)
|
||||
})
|
||||
|
||||
it('wires uncovered-municipalities when href is provided', () => {
|
||||
const href = '/campanha/municipios?coverage=sem_assessor&sort=votos'
|
||||
const props = toHomeActionButtonProps(homeActionsForRole('coordinator'), href)
|
||||
const uncovered = props.find((action) => action.id === 'uncovered-municipalities')
|
||||
expect(uncovered?.href).toBe(href)
|
||||
const inert = props.find((action) => action.id === 'update-votes')
|
||||
expect(inert?.href).toBeUndefined()
|
||||
})
|
||||
|
||||
it('omits uncovered href when not provided', () => {
|
||||
const props = toHomeActionButtonProps(homeActionsForRole('coordinator'))
|
||||
expect(props.find((action) => action.id === 'uncovered-municipalities')?.href).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue