diff --git a/docs/plans/detalhe-entidade-titulo-no-header-impl.md b/docs/plans/detalhe-entidade-titulo-no-header-impl.md new file mode 100644 index 00000000..90fd74cc --- /dev/null +++ b/docs/plans/detalhe-entidade-titulo-no-header-impl.md @@ -0,0 +1,77 @@ +# Impl: Detalhe de entidade — título no header (sem hero espaçoso) + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #315 +Intenção: docs/plans/detalhe-entidade-titulo-no-header.md +Appetite restante: ~0,5 dia eng (8 rotas de detalhe + catálogo + e2e) + +## Leitura da intenção + +- **Outcome:** Em cada detalhe de entidade em `/campanha`, a identidade do registro (título + subtítulo) vive no header da shell; o corpo começa direto em abas/seções/chips operacionais, sem hero nem “Voltar para…”. +- **O que NÃO negociar:** listas mantêm header de seção (B123/B133); edit routes inalteradas; chips operacionais no corpo; leader lockdown; aba do browser alinhada à entidade. +- **O que reavaliar:** hipótese de tocar só `campaignPageChrome` — insuficiente: títulos são dinâmicos por registro; precedente de `SetCampaignPageChrome` + `generateMetadata` nas rotas `/editar` é o dono. + +## Abordagem recomendada + +```mermaid +flowchart LR + RSC[Detail page RSC] --> MD[generateMetadata] + RSC --> SC[SetCampaignPageChrome] + SC --> Shell[CampaignPageChromeDisplay] + RSC --> Body[Corpo sem hero] +``` + +**Opções consideradas:** +- A) Estender `resolveCampaignPageChrome` com dados de entidade (impossível — catálogo é estático/client-safe) +- B) `SetCampaignPageChrome` + `generateMetadata` por rota (precedente edit routes) +- C) Novo provider server-side de chrome + +**Recomendação:** B — reusa `SetCampaignPageChrome` e `campaignPageMetadata` já usados em `/editar` e `MunicipalityListPageChrome`; path rules de detalhe passam a `null` (como edit) para não flashar título de seção antes do override. + +**Rejeitadas:** C (twin desnecessário); A (dados dinâmicos não cabem no catálogo). + +### Componentes / mudanças + +- **`campaignPageChrome.ts`:** regras de detalhe retornam `null`; teste unitário atualizado. +- **8 rotas de detalhe** (`municipios/[slug]`, `liderancas/[id]`, `atividades/[slug]`, `demandas/[slug]`, `dobradinhas/[slug]`, `organizacoes/[slug]`, `apoiadores/[id]`, `assessores/[id]`): `generateMetadata` + `SetCampaignPageChrome`; remover hero (`h1`, “Voltar…”, chips de identidade duplicados, linha Assessoria no município). +- **`campaignZoneMap.e2e.spec.ts`:** assert no chrome da shell (`data-slot=campaign-page-chrome-title`) em vez de `h1` no corpo. +- **Migration:** sem migration +- **Access / Consent:** inalterado + +### Mapeamento chrome por rota + +| Rota | title | subtitle | +| ---- | ----- | -------- | +| municipios/[slug] | view.name | formatMunicipalityGeographyLabel(view) | +| liderancas/[id] | Liderança | leadership.name | +| atividades/[slug] | view.title | view.locationLabel ou municipality.name | +| demandas/[slug] | demand.title | `${kind} · ${municipalityName}` | +| dobradinhas/[slug] | stateDeputy.name | party (opcional) | +| organizacoes/[slug] | organization.name | organizationKindLabels[kind] | +| apoiadores/[id] | Apoiador | supporter.name | +| assessores/[id] | Assessor | advisor.name | + +## Fases verificáveis + +1. **Catálogo + testes** — path rules `null` para detalhes; unit spec +2. **UI — 8 páginas** — chrome override + remoção de hero; metadata dinâmica +3. **Gates** — `pnpm gate:fast`; push via `pnpm push` + +## Rabbit holes / Não escopo (engenharia) + +- Novo bloco UI para Assessoria no município +- Wizard, giros, perfil, contatos, rotas `/editar`, listas +- Refatorar `CampaignPageChromeText` (layout já suporta subtitle) + +## Riscos e mitigação + +- **Flash de título de seção:** path rule `null` + `useLayoutEffect` do override (mesmo que edit routes) +- **E2E que asserta `h1`:** atualizar para chrome slot +- **Atividade sem locationLabel:** fallback para `municipality?.name` + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes de domínio previstos (unit chrome + e2e zone map) diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx index 1d8b97f2..b248fbc6 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx @@ -1,5 +1,5 @@ import config from '@payload-config' -import { ArrowLeftIcon, ShieldCheckIcon } from 'lucide-react' +import { ShieldCheckIcon } from 'lucide-react' import Link from 'next/link' import { notFound, redirect } from 'next/navigation' import { getPayload } from 'payload' @@ -9,10 +9,10 @@ import { ConsentText } from '@/components/campaign/shared/ConsentText' import { RemoveSupporterDataButton } from '@/components/campaign/supporter/RemoveSupporterDataButton' import { SupporterShareKit } from '@/components/campaign/supporter/SupporterShareKit' import { VoteIntentionControl } from '@/components/campaign/supporter/VoteIntentionControl' +import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext' import { Badge } from '@/components/ui/Badge' -import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome' +import { campaignPageMetadata } from '@/lib/campaignPageChrome' import { requireCampaignPageActor } from '@/utilities/campaignPageActor' import { loadSupporterDetailConsentData, @@ -25,7 +25,21 @@ import { } from '@/utilities/supporter/supporterUi' import { parseSupporterId } from '@/utilities/supporter/supporterViewModels' -export const metadata = campaignPageMetadataFromCatalog('apoiadores') +export async function generateMetadata({ params }: SupporterDetailPageProps) { + const { id: rawId } = await params + const supporterId = parseSupporterId(rawId) + if (supporterId === null) return campaignPageMetadata({ title: 'Apoiador' }) + + const [user, payload] = await Promise.all([requireCampaignPageActor(), getPayload({ config })]) + if (!canAccessSupporterArea(user.role)) return campaignPageMetadata({ title: 'Apoiador' }) + + try { + const supporter = await loadSupporterDetailPageData(payload, user, supporterId) + return campaignPageMetadata({ title: 'Apoiador', subtitle: supporter.name }) + } catch { + return campaignPageMetadata({ title: 'Apoiador' }) + } +} type SupporterDetailPageProps = { params: Promise<{ id: string }> @@ -65,19 +79,11 @@ export default async function SupporterDetailPage({ params }: SupporterDetailPag return (
-
- -

{supporter.name}

-

- {supporter.city ?? 'Cidade não informada'} - {supporter.territory ? ` · ${supporter.territory}` : ''} -

-
+ +

+ {supporter.city ?? 'Cidade não informada'} + {supporter.territory ? ` · ${supporter.territory}` : ''} +

diff --git a/src/app/(campaign)/campanha/(app)/assessores/[id]/page.tsx b/src/app/(campaign)/campanha/(app)/assessores/[id]/page.tsx index 89d4b78b..e8e7dad9 100644 --- a/src/app/(campaign)/campanha/(app)/assessores/[id]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/assessores/[id]/page.tsx @@ -1,5 +1,5 @@ import config from '@payload-config' -import { ArrowLeftIcon, MapPinIcon } from 'lucide-react' +import { MapPinIcon } from 'lucide-react' import Link from 'next/link' import { notFound } from 'next/navigation' import { getPayload } from 'payload' @@ -7,16 +7,28 @@ import { getPayload } from 'payload' import { AdvisorDebouncedTextCell } from '@/components/campaign/advisor/AdvisorDebouncedTextCell' import { AdvisorPasswordResetButton } from '@/components/campaign/advisor/AdvisorPasswordResetButton' import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' +import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext' import { Badge } from '@/components/ui/Badge' -import { Button } from '@/components/ui/button' -import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome' +import { campaignPageMetadata } from '@/lib/campaignPageChrome' import { formatBrazilianPhoneInput } from '@/lib/phone' import { isPlanilhaPlaceholderEmail } from '@/lib/schemas/advisor' import { loadAdvisorDetail } from '@/utilities/advisorData' import { requireCampaignPageActor } from '@/utilities/campaignPageActor' import { sendAdvisorPasswordResetFormAction, updateAdvisorProfileFormAction } from '../formActions' -export const metadata = campaignPageMetadataFromCatalog('assessores') +export async function generateMetadata({ params }: AdvisorDetailPageProps) { + const { id: rawId } = await params + const advisorId = Number(rawId) + if (!Number.isInteger(advisorId) || advisorId <= 0) { + return campaignPageMetadata({ title: 'Assessor' }) + } + + const payload = await getPayload({ config }) + const advisor = await loadAdvisorDetail(payload, advisorId) + if (!advisor) return campaignPageMetadata({ title: 'Assessor' }) + + return campaignPageMetadata({ title: 'Assessor', subtitle: advisor.name }) +} type AdvisorDetailPageProps = { params: Promise<{ id: string }> @@ -39,19 +51,11 @@ export default async function AdvisorDetailPage({ params }: AdvisorDetailPagePro return ( -
- -

{advisor.name}

-

- E-mail e celular salvam sozinhos ao digitar. A carteira de municípios é editada no modo - edição da lista. -

-
+ +

+ E-mail e celular salvam sozinhos ao digitar. A carteira de municípios é editada no modo + edição da lista. +

diff --git a/src/app/(campaign)/campanha/(app)/atividades/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/atividades/[slug]/page.tsx index e3abed34..125afc60 100644 --- a/src/app/(campaign)/campanha/(app)/atividades/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/atividades/[slug]/page.tsx @@ -14,10 +14,11 @@ import { ActivityTaskChecklist } from '@/components/campaign/activity/ActivityTa import { ActivityUpdateFeed } from '@/components/campaign/activity/ActivityUpdateFeed' import { ActivityUpdateForm } from '@/components/campaign/activity/ActivityUpdateForm' import { CampaignQuickActionContextBridge } from '@/components/campaign/shell/CampaignQuickActionContextBridge' +import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext' import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome' +import { campaignPageMetadata } from '@/lib/campaignPageChrome' import { formatBahiaDateTimeLabel } from '@/lib/campaignTime' import { activityKindLabels } from '@/lib/schemas/activity' import { getActivityDetailPageData } from '@/utilities/activityDetailPageData' @@ -35,7 +36,27 @@ import { requireCampaignPageActor } from '@/utilities/campaignPageActor' import { ActivityOverviewTab } from './ActivityOverviewTab' -export const metadata = campaignPageMetadataFromCatalog('atividades') +export async function generateMetadata({ params }: ActivityDetailPageProps) { + const { slug } = await params + if (!slug) return campaignPageMetadata({ title: 'Atividade' }) + + const [user, payload] = await Promise.all([ + requireCampaignPageActor({ gate: 'staff' }), + getPayload({ config }), + ]) + + try { + const activeTab = 'overview' as const + const context = await resolveAccessibleActivityContext(payload, user, slug, activeTab) + const view = await getActivityDetailPageData(payload, user, context, activeTab, 1) + const subtitle = view.locationLabel || view.municipality?.name + return campaignPageMetadata( + subtitle ? { title: view.title, subtitle } : { title: view.title }, + ) + } catch { + return campaignPageMetadata({ title: 'Atividade' }) + } +} type ActivityDetailPageProps = { params: Promise<{ slug: string }> @@ -73,30 +94,31 @@ export default async function ActivityDetailPage({ const isStaff = isCampaignStaff(user) const canManageLifecycle = isStaff && view.status !== 'realizado' && view.status !== 'cancelado' + const chromeSubtitle = view.locationLabel || view.municipality?.name return (
+ -
-
-
-
-

{view.title}

- - {view.deputyPresent ? Deputado presente : null} -
-
- {activityKindLabels[view.kind]} - - {view.startAt ? formatBahiaDateTimeLabel(view.startAt) : 'Data a definir'} - - {view.locationLabel ? · {view.locationLabel} : null} -
-
-
+
+
+ + {view.deputyPresent ? Deputado presente : null} + {activityKindLabels[view.kind]} + + {view.startAt ? formatBahiaDateTimeLabel(view.startAt) : 'Data a definir'} + +
+
{isStaff ? (
-
diff --git a/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx index f95970c6..c7ad5900 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx @@ -1,5 +1,5 @@ import config from '@payload-config' -import { ArrowLeftIcon, PaperclipIcon } from 'lucide-react' +import { PaperclipIcon } from 'lucide-react' import Link from 'next/link' import { notFound } from 'next/navigation' import { getPayload } from 'payload' @@ -7,9 +7,9 @@ import { getPayload } from 'payload' import { DemandWorkflowCard } from '@/components/campaign/demand/DemandWorkflowCard' import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { CampaignQuickActionContextSync } from '@/components/campaign/shell/CampaignQuickActionContextSync' +import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext' import { Badge } from '@/components/ui/Badge' -import { Button } from '@/components/ui/button' -import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome' +import { campaignPageMetadata } from '@/lib/campaignPageChrome' import { campaignDemandKindLabels, campaignDemandStatusLabels, @@ -24,7 +24,21 @@ import { transitionDemandFormAction, } from './formActions' -export const metadata = campaignPageMetadataFromCatalog('demandas') +export async function generateMetadata({ params }: DemandDetailPageProps) { + const { slug } = await params + const [user, payload] = await Promise.all([ + requireCampaignPageActor({ gate: 'staff' }), + getPayload({ config }), + ]) + + const demand = await loadDemandDetail(payload, user, slug) + if (!demand) return campaignPageMetadata({ title: 'Demanda' }) + + return campaignPageMetadata({ + title: demand.title, + subtitle: `${campaignDemandKindLabels[demand.kind]} · ${demand.municipalityName}`, + }) +} type DemandDetailPageProps = { params: Promise<{ slug: string }> @@ -63,48 +77,44 @@ export default async function DemandDetailPage({ params }: DemandDetailPageProps return ( + 0 ? demand.municipalityId : undefined} demandSlug={demand.slug} /> -
- -
-

{demand.title}

- - {campaignDemandStatusLabels[demand.status]} - -
-

- {campaignDemandKindLabels[demand.kind]} ·{' '} - - {demand.municipalityName} - - {demand.activity ? ( - <> - {' '} - · Atividade:{' '} - - {demand.activity.title} - - - ) : null} - {demand.requesterName ? ` · Solicitada por ${demand.requesterName}` : ''} · Aberta em{' '} - {dateTimeFormatter.format(new Date(demand.createdAt))} -

-
+
+ + {campaignDemandStatusLabels[demand.status]} + +
+

+ + {demand.municipalityName} + + {demand.activity ? ( + <> + {' '} + · Atividade:{' '} + + {demand.activity.title} + + + ) : null} + {demand.requesterName ? ` · Solicitada por ${demand.requesterName}` : ''} · Aberta em{' '} + {dateTimeFormatter.format(new Date(demand.createdAt))} +

{demand.description ? (
diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/page.tsx index 5f1a5cd5..537d9f09 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/page.tsx @@ -1,19 +1,34 @@ import config from '@payload-config' -import { ArrowLeftIcon, HandshakeIcon, MapPinIcon } from 'lucide-react' +import { HandshakeIcon, MapPinIcon } from 'lucide-react' import Link from 'next/link' import { notFound } from 'next/navigation' import { getPayload } from 'payload' import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' +import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext' import { StateDeputyForm } from '@/components/campaign/stateDeputy/StateDeputyForm' import { Badge } from '@/components/ui/Badge' -import { Button } from '@/components/ui/button' -import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome' +import { campaignPageMetadata } from '@/lib/campaignPageChrome' import { requireCampaignPageActor } from '@/utilities/campaignPageActor' import { loadStateDeputyDetail } from '@/utilities/stateDeputyData' import { updateStateDeputyFormAction } from './formActions' -export const metadata = campaignPageMetadataFromCatalog('dobradinhas') +export async function generateMetadata({ params }: StateDeputyDetailPageProps) { + const { slug } = await params + const [user, payload] = await Promise.all([ + requireCampaignPageActor({ gate: 'staff' }), + getPayload({ config }), + ]) + + const stateDeputy = await loadStateDeputyDetail(payload, user, slug) + if (!stateDeputy) return campaignPageMetadata({ title: 'Dobradinha' }) + + return campaignPageMetadata( + stateDeputy.party + ? { title: stateDeputy.name, subtitle: stateDeputy.party } + : { title: stateDeputy.name }, + ) +} type StateDeputyDetailPageProps = { params: Promise<{ slug: string }> @@ -31,21 +46,16 @@ export default async function StateDeputyDetailPage({ params }: StateDeputyDetai return ( -
- -
-

{stateDeputy.name}

- {stateDeputy.party ? {stateDeputy.party} : null} -
- {stateDeputy.notes ? ( -

{stateDeputy.notes}

- ) : null} -
+ + {stateDeputy.notes ? ( +

{stateDeputy.notes}

+ ) : null}
@@ -58,34 +69,26 @@ export default async function LeadershipDetailPage({ params }: LeadershipDetailP return ( + -
- -
-

{leadership.name}

- {leadership.supportStatus ? ( - - ) : null} - - {leadership.hasAppAccess ? 'Com acesso ao app' : 'Sem acesso ao app'} - -
-

- {leadership.phone ? `Celular ${leadership.phone}` : 'Sem celular registrado'} - {leadership.email ? ` · ${leadership.email}` : ''} -

- {leadership.stateDeputies.length > 0 ? ( - +
+ {leadership.supportStatus ? ( + ) : null} -
+ + {leadership.hasAppAccess ? 'Com acesso ao app' : 'Sem acesso ao app'} + +
+

+ {leadership.phone ? `Celular ${leadership.phone}` : 'Sem celular registrado'} + {leadership.email ? ` · ${leadership.email}` : ''} +

+ {leadership.stateDeputies.length > 0 ? ( + + ) : null}
searchParams: Promise } -const dateFormatter = new Intl.DateTimeFormat('pt-BR') - export default async function MunicipalityDetailPage({ params, searchParams, @@ -68,22 +80,12 @@ export default async function MunicipalityDetailPage({ return ( - {/* print: the dossier tab carries its own capa; page chrome stays out. */} -
-
-

{view.name}

- {municipalityKindLabels[view.kind]} -
-

{formatMunicipalityGeographyLabel(view)}

-

- {advisorSummaries.length - ? `Assessoria: ${advisorSummaries.map((advisor) => advisor.name).join(', ')}` - : 'Sem assessor designado.'} - {view.lastUpdateAt - ? ` · Última atualização em ${dateFormatter.format(new Date(view.lastUpdateAt))}` - : ''} -

-
+ @@ -38,26 +52,20 @@ export default async function OrganizationDetailPage({ params }: OrganizationDet return ( -
- -
-

{organization.name}

- {organizationKindLabels[organization.kind]} -
- {organization.municipalityNames.length ? ( -

- Atua em {organization.municipalityNames.join(', ')} -

- ) : null} - {organization.notes ? ( -

{organization.notes}

- ) : null} -
+ + {organization.municipalityNames.length ? ( +

+ Atua em {organization.municipalityNames.join(', ')} +

+ ) : null} + {organization.notes ? ( +

{organization.notes}

+ ) : null}
({ title }) - type PathRule = { match: (pathname: string) => boolean resolve: () => CampaignPageChrome | null @@ -128,12 +126,12 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/municipios\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.municipios.title), + resolve: () => null, }, { // B147 parallel v2 — soft-dep B145 for entity name in chrome; section title for now. match: (pathname) => /^\/campanha\/municipio\/[^/]+\/v2$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.municipios.title), + resolve: () => ({ title: campaignPageChromeCatalog.municipios.title }), }, { match: (pathname) => pathname === '/campanha/territorios', @@ -145,7 +143,7 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/liderancas\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.liderancas.title), + resolve: () => null, }, { match: (pathname) => pathname === '/campanha/liderancas', @@ -157,7 +155,7 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/dobradinhas\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.dobradinhas.title), + resolve: () => null, }, { match: (pathname) => pathname === '/campanha/dobradinhas', @@ -177,7 +175,7 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/atividades\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.atividades.title), + resolve: () => null, }, { match: (pathname) => pathname === '/campanha/atividades', @@ -189,7 +187,7 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/demandas\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.demandas.title), + resolve: () => null, }, { match: (pathname) => pathname === '/campanha/demandas', @@ -205,7 +203,7 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/apoiadores\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.apoiadores.title), + resolve: () => null, }, { match: (pathname) => pathname === '/campanha/apoiadores', @@ -213,7 +211,7 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/assessores\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.assessores.title), + resolve: () => null, }, { match: (pathname) => pathname === '/campanha/assessores', @@ -225,7 +223,7 @@ const pathRules: PathRule[] = [ }, { match: (pathname) => /^\/campanha\/organizacoes\/[^/]+$/.test(pathname), - resolve: () => sectionOnly(campaignPageChromeCatalog.organizacoes.title), + resolve: () => null, }, { match: (pathname) => pathname === '/campanha/organizacoes', diff --git a/tests/e2e/campaignZoneMap.e2e.spec.ts b/tests/e2e/campaignZoneMap.e2e.spec.ts index fa70bd37..b0a07317 100644 --- a/tests/e2e/campaignZoneMap.e2e.spec.ts +++ b/tests/e2e/campaignZoneMap.e2e.spec.ts @@ -49,9 +49,7 @@ test('the dashboard map paints Salvador zone by zone and opens one', async ({ ca await expect(readout).toContainText(/Salvador — ZE \d+/) await readout.getByRole('link', { name: 'Abrir município' }).click() - // The slug is the assertion: the detail page's own

reads the municipality - // ROW, which other specs rename, while the polygon's name comes from the - // static catalog. + // B145: entity identity lives in the shell header, not a body h1. await expect(page).toHaveURL(/\/campanha\/municipios\/salvador-ze-\d+$/) - await expect(page.getByRole('heading', { level: 1 })).toBeVisible() + await expect(page.locator('[data-slot="campaign-page-chrome-title"]')).toBeVisible() }) diff --git a/tests/unit/campaignPageChrome.unit.spec.ts b/tests/unit/campaignPageChrome.unit.spec.ts index 93b257a4..ce4ef71f 100644 --- a/tests/unit/campaignPageChrome.unit.spec.ts +++ b/tests/unit/campaignPageChrome.unit.spec.ts @@ -21,10 +21,13 @@ describe('campaignPageChrome', () => { }) }) - it('resolves detail pages with section title only', () => { - expect(resolveCampaignPageChrome('/campanha/municipios/cairu', 'coordinator')).toEqual({ - title: 'Municípios', - }) + it('returns null for entity detail pages (chrome set per route)', () => { + expect(resolveCampaignPageChrome('/campanha/municipios/cairu', 'coordinator')).toBeNull() + expect(resolveCampaignPageChrome('/campanha/liderancas/42', 'advisor')).toBeNull() + expect(resolveCampaignPageChrome('/campanha/atividades/foo', 'coordinator')).toBeNull() + }) + + it('keeps section title on parallel municipio v2 route (B147 soft-dep)', () => { expect(resolveCampaignPageChrome('/campanha/municipio/cairu/v2', 'coordinator')).toEqual({ title: 'Municípios', })