B54 — Busca global: layout responsivo dos resultados (#60)

* feat(campanha): responsive grid and viewport cap for home search results (B54)

Adds HomeSearchResultsLayout with 1/2/3-column breakpoints and allocateHitBudget so Municípios keep priority while total hits stay within per-viewport limits.

Closes #8

* fix(campanha): un-export knip-unused home search budget helpers
This commit is contained in:
Francisco Solla 2026-07-30 18:45:44 -03:00 committed by GitHub
parent 784d4cd2e3
commit 142b0963e0
13 changed files with 495 additions and 16 deletions

View file

@ -1,7 +1,7 @@
# Busca global — layout dos resultados (grid + cap de viewport)
Status: rascunho
Atualizado em: 2026-07-29
Status: entregue (2026-07-30)
Atualizado em: 2026-07-30 — as-built: `HomeSearchResultsLayout` (grid 1/2/3 col + `HomeSearchHitBudgetContext`); cap v1 constante por breakpoint (`HOME_SEARCH_HIT_BUDGET` 8/12/15) via `allocateHitBudget` com prioridade Municípios; grupos B48–B53 fatiam via `useHomeSearchHitLimit`.
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B54 — busca global)
Impeccable: B — layout responsivo da região de resultados B47
Appetite: ~0,5–0,75 dia eng; CSS grid + política de cap; sem provider novo

View file

@ -11,6 +11,7 @@ import { HomeSearchAdvisorGroup } from '@/components/campaign/dashboard/HomeSear
import { HomeSearchDemandGroup } from '@/components/campaign/dashboard/HomeSearchDemandGroup'
import { HomeSearchLeadershipGroup } from '@/components/campaign/dashboard/HomeSearchLeadershipGroup'
import { HomeSearchMunicipalityGroup } from '@/components/campaign/dashboard/HomeSearchMunicipalityGroup'
import { HomeSearchResultsLayout } from '@/components/campaign/dashboard/HomeSearchResultsLayout'
import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell'
import { HomeSearchStateDeputyGroup } from '@/components/campaign/dashboard/HomeSearchStateDeputyGroup'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
@ -50,12 +51,14 @@ export default async function CampaignHomePage() {
actions={actions}
searchResults={
<HomeSearchResultsShell>
<HomeSearchMunicipalityGroup />
<HomeSearchLeadershipGroup />
<HomeSearchAdvisorGroup />
<HomeSearchActivityGroup />
<HomeSearchStateDeputyGroup />
<HomeSearchDemandGroup />
<HomeSearchResultsLayout>
<HomeSearchMunicipalityGroup />
<HomeSearchLeadershipGroup />
<HomeSearchAdvisorGroup />
<HomeSearchActivityGroup />
<HomeSearchStateDeputyGroup />
<HomeSearchDemandGroup />
</HomeSearchResultsLayout>
</HomeSearchResultsShell>
}
summarySlot={summarySlot}

View file

@ -1,23 +1,28 @@
'use client'
import { useHomeSearchHitLimit } from '@/components/campaign/dashboard/HomeSearchHitBudgetContext'
import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow'
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { homeSearchActivityGroupHasHits } from '@/lib/campaignHomeSearchHits'
import { sliceHomeSearchHits } from '@/lib/homeSearchHitBudget'
import { HOME_SEARCH_GROUP_HEADING_CLASS, HOME_SEARCH_GROUP_LIST_CLASS } from '@/lib/homeSearchUi'
export const HomeSearchActivityGroup = () => {
const { results } = useHomeSearchResults()
const hitLimit = useHomeSearchHitLimit('activities')
if (results.status !== 'success') return null
if (!homeSearchActivityGroupHasHits(results.data)) return null
const { activities } = results.data
const visibleActivities = sliceHomeSearchHits(activities, hitLimit)
if (visibleActivities.length === 0) return null
return (
<section aria-label="Atividades" className="flex flex-col gap-0.5">
<h2 className={HOME_SEARCH_GROUP_HEADING_CLASS}>Atividades</h2>
<ul className={HOME_SEARCH_GROUP_LIST_CLASS}>
{activities.map((hit) => (
{visibleActivities.map((hit) => (
<li key={hit.id}>
<HomeSearchHitRow
href={`/campanha/atividades/${hit.slug}`}

View file

@ -1,23 +1,29 @@
'use client'
import { useHomeSearchHitLimit } from '@/components/campaign/dashboard/HomeSearchHitBudgetContext'
import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow'
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { formatHomeSearchMunicipalityCount } from '@/lib/campaignHomeSearchHits'
import { sliceHomeSearchHits } from '@/lib/homeSearchHitBudget'
import { HOME_SEARCH_GROUP_HEADING_CLASS, HOME_SEARCH_GROUP_LIST_CLASS } from '@/lib/homeSearchUi'
export const HomeSearchAdvisorGroup = () => {
const { results } = useHomeSearchResults()
const hitLimit = useHomeSearchHitLimit('advisors')
if (results.status !== 'success') return null
const { advisors } = results.data
if (advisors.length === 0) return null
const visibleAdvisors = sliceHomeSearchHits(advisors, hitLimit)
if (visibleAdvisors.length === 0) return null
return (
<section aria-label="Assessores" className="flex flex-col gap-0.5">
<h2 className={HOME_SEARCH_GROUP_HEADING_CLASS}>Assessores</h2>
<ul className={HOME_SEARCH_GROUP_LIST_CLASS}>
{advisors.map((hit) => (
{visibleAdvisors.map((hit) => (
<li key={hit.id}>
<HomeSearchHitRow
href={`/campanha/assessores/${hit.id}`}

View file

@ -1,23 +1,28 @@
'use client'
import { useHomeSearchHitLimit } from '@/components/campaign/dashboard/HomeSearchHitBudgetContext'
import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow'
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { homeSearchDemandGroupHasHits } from '@/lib/campaignHomeSearchHits'
import { sliceHomeSearchHits } from '@/lib/homeSearchHitBudget'
import { HOME_SEARCH_GROUP_HEADING_CLASS, HOME_SEARCH_GROUP_LIST_CLASS } from '@/lib/homeSearchUi'
export const HomeSearchDemandGroup = () => {
const { results } = useHomeSearchResults()
const hitLimit = useHomeSearchHitLimit('demands')
if (results.status !== 'success') return null
if (!homeSearchDemandGroupHasHits(results.data)) return null
const { demands } = results.data
const visibleDemands = sliceHomeSearchHits(demands, hitLimit)
if (visibleDemands.length === 0) return null
return (
<section aria-label="Demandas" className="flex flex-col gap-0.5">
<h2 className={HOME_SEARCH_GROUP_HEADING_CLASS}>Demandas</h2>
<ul className={HOME_SEARCH_GROUP_LIST_CLASS}>
{demands.map((hit) => (
{visibleDemands.map((hit) => (
<li key={hit.id}>
<HomeSearchHitRow
href={`/campanha/demandas/${hit.slug}`}

View file

@ -0,0 +1,77 @@
'use client'
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import {
type HomeSearchGroupHitLimits,
type HomeSearchGroupId,
type HomeSearchViewportTier,
} from '@/lib/homeSearchHitBudget'
const HomeSearchHitBudgetContext = createContext<HomeSearchGroupHitLimits | null>(null)
export const HomeSearchHitBudgetProvider = ({
limits,
children,
}: {
limits: HomeSearchGroupHitLimits
children: ReactNode
}) => (
<HomeSearchHitBudgetContext.Provider value={limits}>
{children}
</HomeSearchHitBudgetContext.Provider>
)
export const useHomeSearchHitLimit = (groupId: HomeSearchGroupId): number | undefined => {
const limits = useContext(HomeSearchHitBudgetContext)
return limits?.[groupId]
}
const VIEWPORT_TIER_QUERIES: { tier: HomeSearchViewportTier; query: string }[] = [
{ tier: 'desktop', query: '(min-width: 1024px)' },
{ tier: 'tablet', query: '(min-width: 768px)' },
]
const resolveHomeSearchViewportTier = (
matchesDesktop: boolean,
matchesTablet: boolean,
): HomeSearchViewportTier => {
if (matchesDesktop) return 'desktop'
if (matchesTablet) return 'tablet'
return 'mobile'
}
const readHomeSearchViewportTier = (): HomeSearchViewportTier => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return 'mobile'
}
return resolveHomeSearchViewportTier(
window.matchMedia(VIEWPORT_TIER_QUERIES[0].query).matches,
window.matchMedia(VIEWPORT_TIER_QUERIES[1].query).matches,
)
}
export const useHomeSearchViewportTier = (): HomeSearchViewportTier => {
const [tier, setTier] = useState<HomeSearchViewportTier>(readHomeSearchViewportTier)
useEffect(() => {
if (typeof window.matchMedia !== 'function') return
const desktopMql = window.matchMedia(VIEWPORT_TIER_QUERIES[0].query)
const tabletMql = window.matchMedia(VIEWPORT_TIER_QUERIES[1].query)
const sync = () => {
setTier(resolveHomeSearchViewportTier(desktopMql.matches, tabletMql.matches))
}
sync()
desktopMql.addEventListener('change', sync)
tabletMql.addEventListener('change', sync)
return () => {
desktopMql.removeEventListener('change', sync)
tabletMql.removeEventListener('change', sync)
}
}, [])
return tier
}

View file

@ -1,23 +1,29 @@
'use client'
import { useHomeSearchHitLimit } from '@/components/campaign/dashboard/HomeSearchHitBudgetContext'
import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow'
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { homeSearchLeadershipGroupHasHits } from '@/lib/campaignHomeSearchHits'
import { sliceHomeSearchHits } from '@/lib/homeSearchHitBudget'
import { HOME_SEARCH_GROUP_HEADING_CLASS, HOME_SEARCH_GROUP_LIST_CLASS } from '@/lib/homeSearchUi'
export const HomeSearchLeadershipGroup = () => {
const { results } = useHomeSearchResults()
const hitLimit = useHomeSearchHitLimit('leaderships')
if (results.status !== 'success') return null
const { leaderships } = results.data
if (!homeSearchLeadershipGroupHasHits(results.data)) return null
const visibleLeaderships = sliceHomeSearchHits(leaderships, hitLimit)
if (visibleLeaderships.length === 0) return null
return (
<section aria-label="Lideranças" className="flex flex-col gap-0.5">
<h2 className={HOME_SEARCH_GROUP_HEADING_CLASS}>Lideranças</h2>
<ul className={HOME_SEARCH_GROUP_LIST_CLASS}>
{leaderships.map((hit) => (
{visibleLeaderships.map((hit) => (
<li key={`leadership-${hit.id}`}>
<HomeSearchHitRow
href={`/campanha/liderancas/${hit.id}`}

View file

@ -1,28 +1,37 @@
'use client'
import { useHomeSearchHitLimit } from '@/components/campaign/dashboard/HomeSearchHitBudgetContext'
import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow'
import { HomeSearchMunicipalityVoteTrailing } from '@/components/campaign/dashboard/HomeSearchMunicipalityVoteTrailing'
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { homeSearchMunicipalityGroupHasHits } from '@/lib/campaignHomeSearchHits'
import { formatElectionNumber } from '@/lib/electionFormat'
import { sliceHomeSearchMunicipalityGroup } from '@/lib/homeSearchHitBudget'
import { HOME_SEARCH_GROUP_HEADING_CLASS, HOME_SEARCH_GROUP_LIST_CLASS } from '@/lib/homeSearchUi'
import { buildTerritoryPageHref } from '@/lib/territoryAnchor'
export const HomeSearchMunicipalityGroup = () => {
const { results, resultKind } = useHomeSearchResults()
const hitLimit = useHomeSearchHitLimit('municipalities')
if (results.status !== 'success') return null
const { municipalities, territories } = results.data
if (!homeSearchMunicipalityGroupHasHits(results.data)) return null
const includeTerritories = resultKind === 'search'
const { municipalities: visibleMunicipalities, territories: visibleTerritories } =
sliceHomeSearchMunicipalityGroup(municipalities, territories, hitLimit, includeTerritories)
if (visibleMunicipalities.length === 0 && visibleTerritories.length === 0) return null
const sectionTitle = resultKind === 'suggest' ? 'Sugestões' : 'Municípios'
return (
<section aria-label={sectionTitle} className="flex flex-col gap-0.5">
<h2 className={HOME_SEARCH_GROUP_HEADING_CLASS}>{sectionTitle}</h2>
<ul className={HOME_SEARCH_GROUP_LIST_CLASS}>
{municipalities.map((hit) => (
{visibleMunicipalities.map((hit) => (
<li key={`municipality-${hit.slug}`}>
<HomeSearchHitRow
href={`/campanha/municipios/${hit.slug}`}
@ -33,8 +42,8 @@ export const HomeSearchMunicipalityGroup = () => {
/>
</li>
))}
{resultKind === 'search'
? territories.map((hit) => (
{includeTerritories
? visibleTerritories.map((hit) => (
<li key={`territory-${hit.region}`}>
<HomeSearchHitRow
href={buildTerritoryPageHref(hit.region)}

View file

@ -0,0 +1,46 @@
'use client'
import type { ReactNode } from 'react'
import {
HomeSearchHitBudgetProvider,
useHomeSearchViewportTier,
} from '@/components/campaign/dashboard/HomeSearchHitBudgetContext'
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import {
allocateHitBudget,
buildHomeSearchGroupHitCounts,
homeSearchHitBudgetForTier,
type HomeSearchGroupHitCounts,
} from '@/lib/homeSearchHitBudget'
const EMPTY_GROUP_COUNTS: HomeSearchGroupHitCounts = {
municipalities: 0,
leaderships: 0,
advisors: 0,
activities: 0,
stateDeputies: 0,
demands: 0,
}
export const HomeSearchResultsLayout = ({ children }: { children: ReactNode }) => {
const { results } = useHomeSearchResults()
const viewportTier = useHomeSearchViewportTier()
const budget = homeSearchHitBudgetForTier(viewportTier)
const limits =
results.status === 'success'
? allocateHitBudget(buildHomeSearchGroupHitCounts(results.data), budget)
: allocateHitBudget(EMPTY_GROUP_COUNTS, budget)
return (
<HomeSearchHitBudgetProvider limits={limits}>
<div
data-slot="home-search-results-layout"
className="flex flex-col gap-4 md:grid md:grid-cols-2 md:gap-4 lg:grid-cols-3"
>
{children}
</div>
</HomeSearchHitBudgetProvider>
)
}

View file

@ -1,11 +1,13 @@
'use client'
import { useHomeSearchHitLimit } from '@/components/campaign/dashboard/HomeSearchHitBudgetContext'
import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow'
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import {
formatHomeSearchMunicipalityCount,
homeSearchStateDeputyGroupHasHits,
} from '@/lib/campaignHomeSearchHits'
import { sliceHomeSearchHits } from '@/lib/homeSearchHitBudget'
import { HOME_SEARCH_GROUP_HEADING_CLASS, HOME_SEARCH_GROUP_LIST_CLASS } from '@/lib/homeSearchUi'
const formatStateDeputySecondary = (
@ -20,17 +22,21 @@ const formatStateDeputySecondary = (
export const HomeSearchStateDeputyGroup = () => {
const { results } = useHomeSearchResults()
const hitLimit = useHomeSearchHitLimit('stateDeputies')
if (results.status !== 'success') return null
const { stateDeputies } = results.data
if (!homeSearchStateDeputyGroupHasHits(results.data)) return null
const visibleStateDeputies = sliceHomeSearchHits(stateDeputies, hitLimit)
if (visibleStateDeputies.length === 0) return null
return (
<section aria-label="Dobradinhas" className="flex flex-col gap-0.5">
<h2 className={HOME_SEARCH_GROUP_HEADING_CLASS}>Dobradinhas</h2>
<ul className={HOME_SEARCH_GROUP_LIST_CLASS}>
{stateDeputies.map((hit) => (
{visibleStateDeputies.map((hit) => (
<li key={`state-deputy-${hit.slug}`}>
<HomeSearchHitRow
href={`/campanha/dobradinhas/${hit.slug}`}

View file

@ -0,0 +1,105 @@
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
/** v1 cap — constant per breakpoint (B54); revisit with ResizeObserver if critique finds blind cuts. */
export const HOME_SEARCH_HIT_BUDGET = {
mobile: 8,
tablet: 12,
desktop: 15,
} as const
export type HomeSearchViewportTier = keyof typeof HOME_SEARCH_HIT_BUDGET
export type HomeSearchGroupId =
| 'municipalities'
| 'leaderships'
| 'advisors'
| 'activities'
| 'stateDeputies'
| 'demands'
const HOME_SEARCH_GROUP_ORDER: readonly HomeSearchGroupId[] = [
'municipalities',
'leaderships',
'advisors',
'activities',
'stateDeputies',
'demands',
]
export type HomeSearchGroupHitCounts = Record<HomeSearchGroupId, number>
export type HomeSearchGroupHitLimits = Record<HomeSearchGroupId, number>
const zeroLimits = (): HomeSearchGroupHitLimits =>
Object.fromEntries(HOME_SEARCH_GROUP_ORDER.map((id) => [id, 0])) as HomeSearchGroupHitLimits
export const homeSearchHitBudgetForTier = (tier: HomeSearchViewportTier): number =>
HOME_SEARCH_HIT_BUDGET[tier]
export const buildHomeSearchGroupHitCounts = (
data: HomeSearchSuccessResponse,
): HomeSearchGroupHitCounts => ({
municipalities:
data.municipalities.length + (data.resultKind === 'search' ? data.territories.length : 0),
leaderships: data.leaderships.length,
advisors: data.advisors.length,
activities: data.activities.length,
stateDeputies: data.stateDeputies.length,
demands: data.demands.length,
})
/**
* Municípios consume the budget first; remaining slots fill secondary groups in
* display order (not round-robin — B54).
*/
export const allocateHitBudget = (
counts: HomeSearchGroupHitCounts,
budget: number,
): HomeSearchGroupHitLimits => {
const limits = zeroLimits()
if (budget <= 0) return limits
let remaining = budget
const municipalitySlots = Math.min(counts.municipalities, remaining)
limits.municipalities = municipalitySlots
remaining -= municipalitySlots
for (const id of HOME_SEARCH_GROUP_ORDER) {
if (id === 'municipalities' || remaining <= 0) continue
const groupHits = counts[id]
if (groupHits <= 0) continue
const slots = Math.min(groupHits, remaining)
limits[id] = slots
remaining -= slots
}
return limits
}
export const sliceHomeSearchHits = <T>(hits: readonly T[], limit: number | undefined): T[] =>
limit === undefined ? [...hits] : hits.slice(0, limit)
export const sliceHomeSearchMunicipalityGroup = (
municipalities: HomeSearchSuccessResponse['municipalities'],
territories: HomeSearchSuccessResponse['territories'],
limit: number | undefined,
includeTerritories: boolean,
): {
municipalities: HomeSearchSuccessResponse['municipalities']
territories: HomeSearchSuccessResponse['territories']
} => {
if (limit === undefined) {
return {
municipalities: [...municipalities],
territories: includeTerritories ? [...territories] : [],
}
}
const visibleMunicipalities = municipalities.slice(0, limit)
const remaining = limit - visibleMunicipalities.length
const visibleTerritories =
includeTerritories && remaining > 0 ? territories.slice(0, remaining) : []
return { municipalities: visibleMunicipalities, territories: visibleTerritories }
}

View file

@ -0,0 +1,162 @@
import { describe, expect, it } from 'vitest'
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
import {
allocateHitBudget,
buildHomeSearchGroupHitCounts,
HOME_SEARCH_HIT_BUDGET,
sliceHomeSearchMunicipalityGroup,
} from '@/lib/homeSearchHitBudget'
const baseResponse = (
overrides: Partial<HomeSearchSuccessResponse> = {},
): HomeSearchSuccessResponse => ({
status: 'success',
resultKind: 'search',
municipalities: [],
territories: [],
advisors: [],
leaderships: [],
stateDeputies: [],
activities: [],
demands: [],
...overrides,
})
describe('allocateHitBudget', () => {
it('gives municipalities priority over secondary groups', () => {
const counts = buildHomeSearchGroupHitCounts(
baseResponse({
municipalities: Array.from({ length: 10 }, (_, index) => ({
kind: 'municipality' as const,
slug: `m-${index}`,
name: `M${index}`,
region: 'R',
priority: null,
votePosition2022: null,
})),
demands: Array.from({ length: 10 }, (_, index) => ({
id: index,
slug: `d-${index}`,
title: `Demand ${index}`,
secondary: 'x',
})),
}),
)
const limits = allocateHitBudget(counts, 8)
expect(limits.municipalities).toBe(8)
expect(limits.demands).toBe(0)
})
it('fills secondary groups in display order when budget remains', () => {
const counts = buildHomeSearchGroupHitCounts(
baseResponse({
municipalities: [
{
kind: 'municipality',
slug: 'cairu',
name: 'Cairu',
region: 'Recôncavo',
priority: null,
votePosition2022: null,
},
],
leaderships: [
{
kind: 'leadership',
id: 1,
name: 'Leader A',
municipalitiesSummary: 'Cairu',
},
{
kind: 'leadership',
id: 2,
name: 'Leader B',
municipalitiesSummary: 'Salvador',
},
],
demands: [
{ id: 1, slug: 'd-1', title: 'Demand 1', secondary: 'x' },
{ id: 2, slug: 'd-2', title: 'Demand 2', secondary: 'x' },
],
}),
)
const limits = allocateHitBudget(counts, 4)
expect(limits.municipalities).toBe(1)
expect(limits.leaderships).toBe(2)
expect(limits.demands).toBe(1)
expect(limits.advisors).toBe(0)
})
it('counts territories toward the municipality group in search mode', () => {
const counts = buildHomeSearchGroupHitCounts(
baseResponse({
municipalities: [],
territories: [
{ kind: 'territory', region: 'Recôncavo', votes2022: 1000 },
{ kind: 'territory', region: 'Sertão', votes2022: 500 },
],
}),
)
const limits = allocateHitBudget(counts, HOME_SEARCH_HIT_BUDGET.mobile)
expect(counts.municipalities).toBe(2)
expect(limits.municipalities).toBe(2)
})
})
describe('sliceHomeSearchMunicipalityGroup', () => {
it('slices municipalities before territories within the shared limit', () => {
const municipalities = [
{
kind: 'municipality' as const,
slug: 'a',
name: 'A',
region: 'R',
priority: null,
votePosition2022: null,
},
{
kind: 'municipality' as const,
slug: 'b',
name: 'B',
region: 'R',
priority: null,
votePosition2022: null,
},
]
const territories = [{ kind: 'territory' as const, region: 'TI', votes2022: 1 }]
const sliced = sliceHomeSearchMunicipalityGroup(municipalities, territories, 2, true)
expect(sliced.municipalities).toHaveLength(2)
expect(sliced.territories).toHaveLength(0)
})
it('uses remaining slots for territories after municipalities', () => {
const municipalities = [
{
kind: 'municipality' as const,
slug: 'a',
name: 'A',
region: 'R',
priority: null,
votePosition2022: null,
},
]
const territories = [
{ kind: 'territory' as const, region: 'TI 1', votes2022: 1 },
{ kind: 'territory' as const, region: 'TI 2', votes2022: 2 },
]
const sliced = sliceHomeSearchMunicipalityGroup(municipalities, territories, 3, true)
expect(sliced.municipalities).toHaveLength(1)
expect(sliced.territories).toHaveLength(2)
})
})

View file

@ -0,0 +1,49 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { HomeSearchResultsProvider } from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { HomeSearchResultsLayout } from '@/components/campaign/dashboard/HomeSearchResultsLayout'
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
const searchResultsValue = (
data: Partial<HomeSearchSuccessResponse> = {},
resultKind: HomeSearchSuccessResponse['resultKind'] = 'search',
) => ({
results: {
status: 'success' as const,
data: {
status: 'success' as const,
resultKind,
municipalities: [],
territories: [],
advisors: [],
leaderships: [],
stateDeputies: [],
activities: [],
demands: [],
...data,
},
},
isFetching: false,
resultKind,
})
describe('HomeSearchResultsLayout', () => {
it('renders a responsive grid container for result groups', () => {
const { container } = render(
<HomeSearchResultsProvider value={searchResultsValue()}>
<HomeSearchResultsLayout>
<p>Group A</p>
<p>Group B</p>
</HomeSearchResultsLayout>
</HomeSearchResultsProvider>,
)
const layout = container.querySelector('[data-slot="home-search-results-layout"]')
expect(layout?.className).toContain('flex-col')
expect(layout?.className).toContain('md:grid-cols-2')
expect(layout?.className).toContain('lg:grid-cols-3')
expect(screen.getByText('Group A')).toBeTruthy()
expect(screen.getByText('Group B')).toBeTruthy()
})
})