mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Merge pull request #273 from fsolla/cursor/pool-i266-0b42294b-54dc
B129 — Remover overview da lista de municípios
This commit is contained in:
commit
544292f8f4
8 changed files with 81 additions and 302 deletions
54
docs/plans/remover-overview-lista-municipios-impl.md
Normal file
54
docs/plans/remover-overview-lista-municipios-impl.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Impl: B129 — Remover overview da lista de municípios
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #266
|
||||
Intenção: docs/plans/remover-overview-lista-municipios.md
|
||||
Appetite restante: remoção cirúrgica (sem redesign)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** `/campanha/municipios` mostra filtros → lista, sem strip de métricas agregadas.
|
||||
- **O que NÃO negociar:** filtros + cenário na barra; overview de apoiadores e dashboard intactos; leader lockdown.
|
||||
- **O que reavaliar:** manter prefetch `loadStatewideSuggestedGoals` — sim, ainda aquece cache para `loadMunicipalityGoalCoverageBundle` usado nas linhas/colunas.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
**Opções consideradas:**
|
||||
|
||||
- **A — Esconder com CSS/feature flag:** rejeitada — morto no loader e no bundle é mais honesto e barato.
|
||||
- **B — Remover UI, manter `overview` no bundle:** rejeitada — viola “dead code dies immediately”.
|
||||
- **C — Remover UI + tipo/campo/computação do loader:** **recomendada**.
|
||||
|
||||
**Rejeitadas:** A, B.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`municipios/page.tsx`:** remover import/render de `MunicipalityListOverview` e `shameHref`.
|
||||
- **`MunicipalityListOverview.tsx`:** deletar arquivo.
|
||||
- **`municipalityPageData.ts`:** remover `MunicipalityListOverviewData`, campo `overview` do bundle, rollup/overview-only fields; manter `staffScope` → `pledgeAggregates` + `goalCoverageByMunicipalityID` para sort/células.
|
||||
- **Tests:** remover asserts de `bundle.overview`; manter asserts de lista/filtros/classe.
|
||||
- **Migration:** sem migration.
|
||||
- **Access / Consent:** sem mudança.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Server** — limpar loader + deletar componente morto.
|
||||
2. **UI** — página sem overview; comentário em `MunicipalityList.tsx`.
|
||||
3. **Gates** — `pnpm gate:fast`; push via `pnpm push`.
|
||||
|
||||
## Rabbit holes / Não escopo
|
||||
|
||||
- Redesenhar `loading.tsx` (fora do aceite).
|
||||
- Tocar `SupporterListOverview` ou dashboard.
|
||||
- **Defer:** escopo de `loadMunicipalityGoalCoverageBundle` ao page slice quando `isPagedByPayload` (gatilho: perf review pós-B129); skip `aggregateByScenario` no loader de lista (gatilho: 2º consumidor de rows-only).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Sort por déficit depende de goal coverage:** mitigado mantendo `loadMunicipalityGoalCoverageBundle` no loader.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto
|
||||
- [x] Invariantes AGENTS/engineering-standards
|
||||
- [x] Testes de domínio previstos (int `municipalityPageData`)
|
||||
|
|
@ -10,7 +10,6 @@ import { RecentVisitTracker } from '@/components/campaign/dashboard/RecentVisitT
|
|||
import { MunicipalityEstimateScenarioProvider } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext'
|
||||
import { MunicipalityFilters } from '@/components/campaign/municipality/MunicipalityFilters'
|
||||
import { MunicipalityList } from '@/components/campaign/municipality/MunicipalityList'
|
||||
import { MunicipalityListOverview } from '@/components/campaign/municipality/MunicipalityListOverview'
|
||||
import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
|
|
@ -56,13 +55,7 @@ export default async function MunicipalitiesPage({ searchParams }: Municipalitie
|
|||
const canMoveEngagementLevel = isCampaignUnrestricted(user)
|
||||
|
||||
const pageBundle = await loadMunicipalityListPageBundle(payload, user, rawSearchParams)
|
||||
const {
|
||||
municipalities: listMunicipalities,
|
||||
totalDocs,
|
||||
totalPages,
|
||||
overview,
|
||||
filterFacets,
|
||||
} = pageBundle
|
||||
const { municipalities: listMunicipalities, totalDocs, totalPages, filterFacets } = pageBundle
|
||||
const resolvedUrl = resolveMunicipalityListUrl(rawSearchParams, totalPages)
|
||||
if (resolvedUrl.redirectHref) redirect(resolvedUrl.redirectHref)
|
||||
const { state } = resolvedUrl
|
||||
|
|
@ -109,20 +102,10 @@ export default async function MunicipalitiesPage({ searchParams }: Municipalitie
|
|||
/>
|
||||
)
|
||||
|
||||
// E9 coluna da vergonha: same scope, filtered down to priority municipalities
|
||||
// with nobody answering for them. `null` when that is already the filter.
|
||||
const shameHref =
|
||||
state.priority === 'alta' && state.coverage === 'sem_assessor'
|
||||
? null
|
||||
: buildMunicipalityListHref({ ...state, priority: 'alta', coverage: 'sem_assessor' }, 1)
|
||||
|
||||
// The overview and the table's filter header stay mounted even with zero
|
||||
// results — only the rows are replaced by the empty state.
|
||||
// The table's filter header stays mounted even with zero results — only the
|
||||
// rows are replaced by the empty state.
|
||||
const listBody = (
|
||||
<>
|
||||
{isStaffView && overview ? (
|
||||
<MunicipalityListOverview view={overview} shameHref={shameHref} />
|
||||
) : null}
|
||||
<MunicipalityList
|
||||
municipalities={listMunicipalities}
|
||||
advisorNamesById={advisorNamesById}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export type MunicipalityListProps = {
|
|||
columnVisibility: CampaignColumnVisibility
|
||||
}
|
||||
|
||||
/** Replaces only the rows: the filter header row and the overview stay put. */
|
||||
/** Replaces only the rows: the filter header row stays put. */
|
||||
const MunicipalityListEmptyState = ({ state }: { state: MunicipalityListState }) => (
|
||||
<Empty className="min-h-56">
|
||||
<EmptyHeader>
|
||||
|
|
|
|||
|
|
@ -1,177 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import { CircleAlertIcon } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useMunicipalityEstimateScenario } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext'
|
||||
import { CampaignTransitionAnchor } from '@/components/campaign/shared/CampaignListPending'
|
||||
import { CampaignMetricStrip } from '@/components/campaign/shared/CampaignMetricStrip'
|
||||
import { campaignPrioritySurfaceClassName } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { VoteEstimateScenarioStrip } from '@/components/campaign/votePledge/VoteEstimateScenarioStrip'
|
||||
import { formatElectionNumber } from '@/lib/electionFormat'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
formatVoteEstimateEndpointsLabel,
|
||||
hasAnyVoteEstimate,
|
||||
voteEstimateScenarioLabels,
|
||||
} from '@/lib/voteEstimate'
|
||||
import {
|
||||
formatGoalCoverageDeficitLabel,
|
||||
formatGoalCoverageRatioLabel,
|
||||
goalCoverageProgressPercent,
|
||||
} from '@/utilities/municipality/goalCoverage'
|
||||
import type { MunicipalityListOverviewData } from '@/utilities/municipality/municipalityPageData'
|
||||
|
||||
const SCENARIO_FLASH_MS = 420
|
||||
|
||||
/**
|
||||
* E9 "coluna da vergonha": the assessoria ratio hides the only gap the
|
||||
* coordination can close today, so the priority municipalities with nobody
|
||||
* answering for them are named as their own number — and the number is the
|
||||
* link that filters the list down to exactly those rows (`shameHref` is
|
||||
* `null` when that filter is already applied, so the count doesn't pretend to
|
||||
* navigate somewhere new).
|
||||
*/
|
||||
const AssessoriaGapDetail = ({
|
||||
priorityWithoutAdvisorCount,
|
||||
shameHref,
|
||||
}: {
|
||||
priorityWithoutAdvisorCount: number
|
||||
shameHref: string | null
|
||||
}) => {
|
||||
if (priorityWithoutAdvisorCount === 0) {
|
||||
return <p className="text-xs text-muted-foreground">Nenhuma prioritária sem responsável</p>
|
||||
}
|
||||
|
||||
const label = `${formatElectionNumber(priorityWithoutAdvisorCount)} ${
|
||||
priorityWithoutAdvisorCount === 1 ? 'prioritária' : 'prioritárias'
|
||||
} sem responsável`
|
||||
|
||||
if (!shameHref) {
|
||||
return <p className="text-xs font-medium text-destructive tabular-nums">{label}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<CampaignTransitionAnchor
|
||||
href={shameHref}
|
||||
scroll={false}
|
||||
className="inline-flex min-h-11 items-center gap-1 text-xs font-medium text-destructive tabular-nums underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<CircleAlertIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
{label}
|
||||
</CampaignTransitionAnchor>
|
||||
)
|
||||
}
|
||||
|
||||
export const MunicipalityListOverview = ({
|
||||
view,
|
||||
shameHref,
|
||||
}: {
|
||||
view: MunicipalityListOverviewData
|
||||
shameHref: string | null
|
||||
}) => {
|
||||
const { scenario } = useMunicipalityEstimateScenario()
|
||||
const [isFlashing, setIsFlashing] = useState(false)
|
||||
const skipFlashRef = useRef(true)
|
||||
const flashTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (skipFlashRef.current) {
|
||||
skipFlashRef.current = false
|
||||
return
|
||||
}
|
||||
const reduceMotion =
|
||||
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
if (reduceMotion) return
|
||||
setIsFlashing(true)
|
||||
clearTimeout(flashTimeoutRef.current)
|
||||
flashTimeoutRef.current = setTimeout(() => setIsFlashing(false), SCENARIO_FLASH_MS)
|
||||
return () => clearTimeout(flashTimeoutRef.current)
|
||||
}, [scenario])
|
||||
|
||||
const scenarioTotals = view.staffVoteTotalByScenario
|
||||
const activeTotal = scenarioTotals[scenario]
|
||||
const endpointsLabel = formatVoteEstimateEndpointsLabel(scenarioTotals)
|
||||
const scenarioLabel = voteEstimateScenarioLabels[scenario]
|
||||
const hasScenarioTotals = hasAnyVoteEstimate(scenarioTotals)
|
||||
const goalCoverage = view.goalCoverageByScenario[scenario]
|
||||
|
||||
const heroDetail = (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{hasScenarioTotals ? (
|
||||
<VoteEstimateScenarioStrip
|
||||
values={scenarioTotals}
|
||||
activeScenario={scenario}
|
||||
labelMode="endpoints"
|
||||
markerMode="active-only"
|
||||
stretch
|
||||
className="max-w-none"
|
||||
/>
|
||||
) : null}
|
||||
{endpointsLabel ? (
|
||||
<p className="text-xs text-muted-foreground tabular-nums">{endpointsLabel}</p>
|
||||
) : null}
|
||||
{view.missingEstimateCount > 0 ? (
|
||||
<p className="text-xs text-muted-foreground tabular-nums">
|
||||
{formatElectionNumber(view.missingEstimateCount)}{' '}
|
||||
{view.missingEstimateCount === 1 ? 'declaração' : 'declarações'} sem estimativa
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Visão geral dos municípios filtrados"
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
'rounded-xl transition-[box-shadow,background-color] duration-300',
|
||||
campaignPrioritySurfaceClassName,
|
||||
isFlashing ? 'ring-2 ring-primary/35' : undefined,
|
||||
)}
|
||||
>
|
||||
<CampaignMetricStrip
|
||||
className="rounded-none ring-0"
|
||||
metrics={[
|
||||
{
|
||||
label: `${scenarioLabel} nos municípios filtrados`,
|
||||
value: formatElectionNumber(activeTotal),
|
||||
detail: heroDetail,
|
||||
emphasize: true,
|
||||
valueAriaLabel: `${scenarioLabel} nos municípios filtrados: ${formatElectionNumber(activeTotal)}${
|
||||
endpointsLabel ? `; ${endpointsLabel}` : ''
|
||||
}${
|
||||
view.missingEstimateCount > 0
|
||||
? `; ${formatElectionNumber(view.missingEstimateCount)} sem estimativa`
|
||||
: ''
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: 'Declarações de votos',
|
||||
value: view.pledgeCount ? formatElectionNumber(view.pledgeCount) : 'Nenhuma',
|
||||
},
|
||||
{
|
||||
label: 'Cobertura de assessoria',
|
||||
value: `${view.withAdvisorCount} de ${view.municipalityCount} municípios`,
|
||||
detail: (
|
||||
<AssessoriaGapDetail
|
||||
priorityWithoutAdvisorCount={view.priorityWithoutAdvisorCount}
|
||||
shameHref={shameHref}
|
||||
/>
|
||||
),
|
||||
progress:
|
||||
view.municipalityCount > 0
|
||||
? Math.round((view.withAdvisorCount / view.municipalityCount) * 100)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
label: 'Cobertura da meta',
|
||||
value: formatGoalCoverageRatioLabel(goalCoverage),
|
||||
detail: formatGoalCoverageDeficitLabel(goalCoverage),
|
||||
progress: goalCoverageProgressPercent(goalCoverage),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -51,33 +51,13 @@ import {
|
|||
type MunicipalityListViewModel,
|
||||
} from '@/utilities/municipality/municipalityViewModels'
|
||||
import { loadStateDeputySummaries } from '@/utilities/stateDeputyData'
|
||||
import {
|
||||
rollupMunicipalityStaffVotes,
|
||||
type MunicipalityPledgeAggregate,
|
||||
} from '@/utilities/votePledgeViews'
|
||||
import { type MunicipalityPledgeAggregate } from '@/utilities/votePledgeViews'
|
||||
|
||||
export const MunicipalityNotFoundError = createEntityNotFoundError(
|
||||
'Municipality',
|
||||
'Município não encontrado.',
|
||||
)
|
||||
|
||||
export type MunicipalityListOverviewData = {
|
||||
municipalityCount: number
|
||||
staffVoteTotalByScenario: Record<VoteEstimateScenario, number>
|
||||
pledgeCount: number
|
||||
missingEstimateCount: number
|
||||
withAdvisorCount: number
|
||||
/**
|
||||
* E9 "coluna da vergonha": priority municipalities in scope with nobody
|
||||
* answering for them — the one gap the coordination can close today, so it
|
||||
* gets named as a number and a filtered link instead of hiding inside the
|
||||
* assessoria ratio.
|
||||
*/
|
||||
priorityWithoutAdvisorCount: number
|
||||
/** E8 "conta da cadeira" — meta × comprometido do escopo filtrado, por cenário. */
|
||||
goalCoverageByScenario: Record<VoteEstimateScenario, MunicipalityGoalCoverage>
|
||||
}
|
||||
|
||||
/** Values still reachable under the OTHER active filters (column-filter options). */
|
||||
type MunicipalityListFilterFacets = {
|
||||
/** Catalog order. */
|
||||
|
|
@ -91,7 +71,6 @@ export type MunicipalityListPageBundle = {
|
|||
totalDocs: number
|
||||
totalPages: number
|
||||
scopeTotal: number
|
||||
overview: MunicipalityListOverviewData | null
|
||||
filterFacets: MunicipalityListFilterFacets
|
||||
}
|
||||
|
||||
|
|
@ -106,10 +85,9 @@ type MunicipalityFacetRow = Pick<Municipality, 'slug' | 'region' | 'advisors'>
|
|||
/**
|
||||
* E10's class filter is the only one that isn't a Payload constraint (the
|
||||
* class is derived from the committed TSE artifact, not stored), so every
|
||||
* place that would otherwise trust `where` — the page query, the overview
|
||||
* scope and the facets — has to apply this predicate itself. Returns `null`
|
||||
* when no class is selected, which is also the signal to keep the cheap
|
||||
* database-paginated path.
|
||||
* place that would otherwise trust `where` — the page query and the facets —
|
||||
* has to apply this predicate itself. Returns `null` when no class is selected,
|
||||
* which is also the signal to keep the cheap database-paginated path.
|
||||
*/
|
||||
const territorialClassFilterPredicate = (
|
||||
state: MunicipalityListState,
|
||||
|
|
@ -320,7 +298,6 @@ export const loadMunicipalityListPageBundle = async (
|
|||
totalDocs: 0,
|
||||
totalPages: 0,
|
||||
scopeTotal: 0,
|
||||
overview: null,
|
||||
filterFacets: emptyMunicipalityListFilterFacets,
|
||||
}
|
||||
}
|
||||
|
|
@ -389,24 +366,17 @@ export const loadMunicipalityListPageBundle = async (
|
|||
isStaff ? loadStatewideSuggestedGoals(payload, user) : null,
|
||||
])
|
||||
|
||||
let overview: MunicipalityListOverviewData | null = null
|
||||
let pledgeAggregates = new Map<number, MunicipalityPledgeAggregate>()
|
||||
let goalCoverageByMunicipalityID = new Map<
|
||||
number,
|
||||
Record<VoteEstimateScenario, MunicipalityGoalCoverage>
|
||||
>()
|
||||
|
||||
// Computed even for an empty filtered scope: the overview stays on screen
|
||||
// (zeroed) next to the empty state instead of disappearing with the rows.
|
||||
if (staffScope) {
|
||||
pledgeAggregates = staffScope.pledgeAggregates
|
||||
// Never mutate the scope: `loadMunicipalityScope` is request-cached and
|
||||
// shared with the facets. Filtering here is what keeps the overview
|
||||
// (E8 coverage, E9 shame column) counting the filtered scope.
|
||||
const scopedMunicipalities = classMatches
|
||||
? staffScope.municipalities.filter((municipality) => classMatches(municipality.slug))
|
||||
: staffScope.municipalities
|
||||
const rollup = rollupMunicipalityStaffVotes(scopedMunicipalities, pledgeAggregates)
|
||||
const goalCoverageBundle = await loadMunicipalityGoalCoverageBundle(
|
||||
payload,
|
||||
user,
|
||||
|
|
@ -414,20 +384,6 @@ export const loadMunicipalityListPageBundle = async (
|
|||
pledgeAggregates,
|
||||
)
|
||||
goalCoverageByMunicipalityID = goalCoverageBundle.coverageByMunicipalityID
|
||||
overview = {
|
||||
municipalityCount: scopedMunicipalities.length,
|
||||
staffVoteTotalByScenario: { ...rollup.staffVoteTotalByScenario },
|
||||
pledgeCount: rollup.pledgeCount,
|
||||
missingEstimateCount: rollup.missingEstimateCount,
|
||||
withAdvisorCount: scopedMunicipalities.filter(
|
||||
(municipality) => (municipality.advisors ?? []).length > 0,
|
||||
).length,
|
||||
priorityWithoutAdvisorCount: scopedMunicipalities.filter(
|
||||
(municipality) =>
|
||||
municipality.priority === 'alta' && (municipality.advisors ?? []).length === 0,
|
||||
).length,
|
||||
goalCoverageByScenario: goalCoverageBundle.aggregateByScenario,
|
||||
}
|
||||
}
|
||||
|
||||
let pageDocs: Municipality[]
|
||||
|
|
@ -468,7 +424,6 @@ export const loadMunicipalityListPageBundle = async (
|
|||
totalDocs,
|
||||
totalPages,
|
||||
scopeTotal: scopeCount.totalDocs,
|
||||
overview,
|
||||
filterFacets,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,10 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
)
|
||||
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Editar tendência política' }).click()
|
||||
const trendButton = page.getByRole('button', {
|
||||
name: new RegExp(`^Editar tendência política em ${municipality.name}`),
|
||||
})
|
||||
await trendButton.click()
|
||||
const trendPopover = page.locator('[data-slot="popover-content"]')
|
||||
await expect(trendPopover).toBeVisible()
|
||||
await expect(trendPopover.getByRole('button', { name: 'Salvar' })).toHaveCount(0)
|
||||
|
|
@ -165,13 +168,11 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
trendPopover.getByLabel('Justificativa').blur(),
|
||||
])
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Editar tendência política' })).toContainText(
|
||||
'Favorável',
|
||||
)
|
||||
await expect(trendButton).toContainText('Favorável')
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.reload()
|
||||
await page.getByRole('button', { name: 'Editar tendência política' }).click()
|
||||
await trendButton.click()
|
||||
const reopened = page.locator('[data-slot="popover-content"]')
|
||||
await expect(reopened.getByLabel('Tendência', { exact: true })).toHaveValue('favoravel')
|
||||
await expect(reopened.getByLabel('Justificativa')).toHaveValue(note)
|
||||
|
|
@ -428,6 +429,8 @@ test.describe('Municípios — FAB ações rápidas mobile (B126)', () => {
|
|||
await page.goto(
|
||||
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
|
||||
)
|
||||
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
|
||||
|
||||
const fab = page.getByRole('button', { name: 'Ações rápidas' })
|
||||
await expect(fab).toBeVisible()
|
||||
await expect(page.getByLabel('Buscar na campanha')).toHaveCount(0)
|
||||
|
|
@ -478,7 +481,7 @@ test.describe('Municípios — FAB overlay polish (B126)', () => {
|
|||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto(`${campaign.baseURL}/campanha/municipios`)
|
||||
await expect(page.getByRole('button', { name: 'Ações rápidas' })).toBeVisible()
|
||||
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Ações rápidas' }).click()
|
||||
const overlay = page.locator('#CampaignQuickActionsOverlay')
|
||||
|
|
|
|||
|
|
@ -562,6 +562,10 @@ export const test = base.extend<CampaignE2ETestFixtures>({
|
|||
})
|
||||
|
||||
export const campaignPageChrome = (page: Page, title: string) =>
|
||||
page.locator('[data-slot="campaign-page-chrome"]').filter({ visible: true }).getByText(title)
|
||||
page
|
||||
.locator('[data-slot="campaign-page-chrome"]')
|
||||
.filter({ visible: true })
|
||||
.getByText(title)
|
||||
.first()
|
||||
|
||||
export { expect }
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import { territorialClassSortWeight } from '@/lib/territorialClassSortWeight'
|
|||
import config from '@/payload.config'
|
||||
import { loadMunicipalityListPageBundle } from '@/utilities/municipality/municipalityPageData'
|
||||
import { computeMunicipalityTerritorialClass } from '@/utilities/municipality/municipalityTerritorialClass'
|
||||
import { aggregatePledgesByMunicipality } from '@/utilities/votePledgeData'
|
||||
import { rollupMunicipalityStaffVotes } from '@/utilities/votePledgeViews'
|
||||
|
||||
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
||||
|
||||
|
|
@ -27,62 +25,25 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
payload = await getPayload({ config: await config })
|
||||
})
|
||||
|
||||
it('keeps a zeroed overview when the filtered municipality set is empty', async () => {
|
||||
it('returns an empty list when the filtered municipality set is empty', async () => {
|
||||
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
|
||||
|
||||
const bundle = await loadMunicipalityListPageBundle(payload, coordinator, { q: 'zzznomatch' })
|
||||
|
||||
// The overview stays on screen next to the empty state instead of unmounting.
|
||||
expect(bundle.municipalities).toHaveLength(0)
|
||||
expect(bundle.overview).not.toBeNull()
|
||||
expect(bundle.overview!.municipalityCount).toBe(0)
|
||||
expect(bundle.overview!.staffVoteTotalByScenario.central).toBe(0)
|
||||
expect(bundle.overview!.goalCoverageByScenario.central.goal).toBe(0)
|
||||
expect(bundle.totalDocs).toBe(0)
|
||||
})
|
||||
|
||||
it('rolls up staffVoteTotal from expectedVotes when no pledge overrides apply', async () => {
|
||||
it('includes 2022 vote position on list rows', async () => {
|
||||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const municipality = await fixtures.getMunicipality()
|
||||
fixtures.touchMunicipality(municipality.id)
|
||||
|
||||
await payload.update({
|
||||
collection: 'municipality',
|
||||
id: municipality.id,
|
||||
data: { expectedVotes: { pessimistic: null, central: 1_500, optimistic: null } },
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
|
||||
const contact = await fixtures.createContact()
|
||||
const leadership = await fixtures.createLeadership({
|
||||
contact: contact.id,
|
||||
municipalities: [municipality.id],
|
||||
supportStatus: 'engajado',
|
||||
})
|
||||
await fixtures.createVotePledge({
|
||||
leadership: leadership.id,
|
||||
municipality: municipality.id,
|
||||
declaredVotes: 80,
|
||||
estimatedVotes: { pessimistic: null, central: 120, optimistic: null },
|
||||
})
|
||||
|
||||
const refreshedMunicipality = await payload.findByID({
|
||||
collection: 'municipality',
|
||||
id: municipality.id,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
const aggregates = await aggregatePledgesByMunicipality(payload, [municipality.id])
|
||||
const rollup = rollupMunicipalityStaffVotes([refreshedMunicipality], aggregates)
|
||||
expect(rollup.staffVoteTotal).toBe(1_500)
|
||||
|
||||
const bundle = await loadMunicipalityListPageBundle(payload, coordinator, {
|
||||
q: municipality.name,
|
||||
})
|
||||
|
||||
expect(bundle.overview).not.toBeNull()
|
||||
expect(bundle.overview!.staffVoteTotalByScenario.central).toBeGreaterThanOrEqual(1_500)
|
||||
const row = bundle.municipalities.find((item) => item.slug === municipality.slug)
|
||||
expect(row?.votePosition2022).not.toBeNull()
|
||||
expect(row!.votePosition2022!.totalUnits).toBe(435)
|
||||
|
|
@ -282,9 +243,9 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
/**
|
||||
* E10: the class is derived from the committed TSE artifact, so the filter
|
||||
* cannot be a Payload constraint. The bundle must still agree with itself —
|
||||
* rows, `totalDocs` and the overview all counting the filtered scope.
|
||||
* rows and `totalDocs` counting the filtered scope.
|
||||
*/
|
||||
it('filters by the derived territorial class and keeps the overview honest', async () => {
|
||||
it('filters by the derived territorial class and keeps totals honest', async () => {
|
||||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
|
||||
|
|
@ -326,7 +287,6 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
})
|
||||
expect(filtered.municipalities.map((row) => row.slug)).toEqual([first.slug])
|
||||
expect(filtered.totalDocs).toBe(1)
|
||||
expect(filtered.overview?.municipalityCount).toBe(1)
|
||||
expect(filtered.municipalities[0]!.territorialClass).toBe(firstClass)
|
||||
expect(filtered.municipalities[0]!.territorialClassFactors.length).toBeGreaterThan(0)
|
||||
|
||||
|
|
@ -411,7 +371,6 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
const included = await loadMunicipalityListPageBundle(payload, advisor, {
|
||||
q: administered.name,
|
||||
})
|
||||
expect(included.overview).not.toBeNull()
|
||||
expect(included.municipalities.some((row) => row.slug === administered.slug)).toBe(true)
|
||||
|
||||
const outsideSlug = municipalityCatalog.find((entry) => entry.slug !== administered.slug)!.slug
|
||||
|
|
@ -419,7 +378,6 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
const excluded = await loadMunicipalityListPageBundle(payload, advisor, {
|
||||
slug: [outsideSlug],
|
||||
})
|
||||
expect(excluded.overview?.municipalityCount).toBe(0)
|
||||
expect(excluded.municipalities).toHaveLength(0)
|
||||
})
|
||||
|
||||
|
|
@ -498,7 +456,6 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
|
||||
const bundle = await loadMunicipalityListPageBundle(payload, leader, { q: municipality.name })
|
||||
|
||||
expect(bundle.overview).toBeNull()
|
||||
expect(bundle.municipalities).toHaveLength(0)
|
||||
expect(bundle.scopeTotal).toBe(0)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue