mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
B106 — Voltar no header mobile, Android back e skeleton das sugestões
- CampaignHomeSearchChromeContext + publisher no Início - CampaignMobileTopBar troca SidebarTrigger por Voltar quando uiFocused - useHomeSearchFocusHistory com pushState/popstate para back do SO - HomeSearchSuggestSkeleton enquanto isFetching sem hits - Unit tests para history helper, top bar e skeleton Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
parent
3babd78d73
commit
84365ddf54
13 changed files with 407 additions and 58 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
type BiometricEnrollmentOffer,
|
||||
} from '@/components/campaign/shell/BiometricEnrollmentToast'
|
||||
import { CampaignAppScrollChrome } from '@/components/campaign/shell/CampaignAppScrollChrome'
|
||||
import { CampaignHomeSearchChromeProvider } from '@/components/campaign/shell/CampaignHomeSearchChromeContext'
|
||||
import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar'
|
||||
import { CampaignNotificationBellSlot } from '@/components/campaign/shell/CampaignNotificationBellSlot'
|
||||
import { CampaignQuickActionContextProvider } from '@/components/campaign/shell/CampaignQuickActionContext'
|
||||
|
|
@ -75,7 +76,8 @@ export default async function CampaignAppLayout({ children }: { children: React.
|
|||
<CampaignSidebar user={campaignUserShellView(user)} />
|
||||
<SidebarInset className="h-svh min-h-0 overflow-hidden print:h-auto print:overflow-visible">
|
||||
<CampaignWizardChromeProvider>
|
||||
<CampaignQuickActionContextProvider>
|
||||
<CampaignHomeSearchChromeProvider>
|
||||
<CampaignQuickActionContextProvider>
|
||||
<CampaignListPendingBoundary>
|
||||
<CampaignMobileTopBar
|
||||
notificationBell={<CampaignNotificationBellSlot user={user} />}
|
||||
|
|
@ -100,7 +102,8 @@ export default async function CampaignAppLayout({ children }: { children: React.
|
|||
<InstallPwaToast />
|
||||
{biometricEnrollment ? <BiometricEnrollmentToast {...biometricEnrollment} /> : null}
|
||||
</CampaignListPendingBoundary>
|
||||
</CampaignQuickActionContextProvider>
|
||||
</CampaignQuickActionContextProvider>
|
||||
</CampaignHomeSearchChromeProvider>
|
||||
</CampaignWizardChromeProvider>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
|
||||
import { useHomeSearchFocusHistory } from '@/components/campaign/dashboard/useHomeSearchFocusHistory'
|
||||
import {
|
||||
useSetCampaignHomeSearchChrome,
|
||||
type CampaignHomeSearchChromeState,
|
||||
} from '@/components/campaign/shell/CampaignHomeSearchChromeContext'
|
||||
|
||||
/** Publishes Início search chrome to the layout top bar (B106). */
|
||||
export const CampaignHomeSearchChromePublisher = () => {
|
||||
const { clear, uiFocused } = useHomeSearch()
|
||||
|
||||
const collapse = useCallback(() => {
|
||||
clear()
|
||||
}, [clear])
|
||||
|
||||
const chrome = useMemo((): CampaignHomeSearchChromeState | null => {
|
||||
if (!uiFocused) {
|
||||
return null
|
||||
}
|
||||
return { focused: true, collapse }
|
||||
}, [collapse, uiFocused])
|
||||
|
||||
useSetCampaignHomeSearchChrome(chrome)
|
||||
useHomeSearchFocusHistory({ focused: uiFocused, onCollapse: collapse })
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout'
|
||||
import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch'
|
||||
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
|
||||
import { CampaignHomeSearchChromePublisher } from '@/components/campaign/dashboard/CampaignHomeSearchChromePublisher'
|
||||
import {
|
||||
HomeSearchResultsProvider,
|
||||
InitialHomeSearchSuggestProvider,
|
||||
|
|
@ -42,6 +43,7 @@ const CampaignHomeStaffChromeInner = ({
|
|||
|
||||
return (
|
||||
<div className="h-full min-h-0 w-full" data-home-focused={focused || undefined}>
|
||||
<CampaignHomeSearchChromePublisher />
|
||||
<CampaignHomeLayout
|
||||
actions={actions}
|
||||
focused={focused}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,17 @@
|
|||
import type { ReactNode } from 'react'
|
||||
|
||||
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
|
||||
import { HomeSearchSuggestSkeleton } from '@/components/campaign/dashboard/HomeSearchSuggestSkeleton'
|
||||
import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext'
|
||||
import { homeSearchHasAnyHits } from '@/lib/campaignHomeSearchHits'
|
||||
|
||||
export const HomeSearchResultsShell = ({ children }: { children: ReactNode }) => {
|
||||
const { query } = useHomeSearch()
|
||||
const { results, resultKind } = useHomeSearchResults()
|
||||
const { results, resultKind, isFetching } = useHomeSearchResults()
|
||||
|
||||
const hasRenderableHits =
|
||||
results.status === 'success' && homeSearchHasAnyHits(results.data)
|
||||
const showSkeleton = isFetching && !hasRenderableHits && results.status !== 'error'
|
||||
|
||||
const showEmpty =
|
||||
query.isActive &&
|
||||
|
|
@ -18,7 +23,8 @@ export const HomeSearchResultsShell = ({ children }: { children: ReactNode }) =>
|
|||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{showSkeleton ? <HomeSearchSuggestSkeleton /> : null}
|
||||
{!showSkeleton ? children : null}
|
||||
{results.status === 'error' ? (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{results.message}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { HOME_SEARCH_HIT_ROW_WRAPPER_CLASS } from '@/lib/homeSearchUi'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const HOME_SEARCH_SUGGEST_SKELETON_ROW_COUNT = 3
|
||||
|
||||
const HomeSearchSuggestSkeletonRow = () => (
|
||||
<div className={cn(HOME_SEARCH_HIT_ROW_WRAPPER_CLASS, 'flex min-h-11 items-center gap-3 py-2.5')}>
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<Skeleton className="h-4 w-3/5 max-w-[12rem]" />
|
||||
<Skeleton className="h-3 w-2/5 max-w-[8rem]" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
/** B106 — placeholder rows while suggest/search hits are still loading. */
|
||||
export const HomeSearchSuggestSkeleton = () => (
|
||||
<div
|
||||
data-slot="home-search-suggest-skeleton"
|
||||
data-testid="home-search-suggest-skeleton"
|
||||
className="flex flex-col"
|
||||
aria-hidden
|
||||
>
|
||||
{Array.from({ length: HOME_SEARCH_SUGGEST_SKELETON_ROW_COUNT }, (_, index) => (
|
||||
<HomeSearchSuggestSkeletonRow key={index} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { HOME_SEARCH_FOCUS_HISTORY_KEY } from '@/lib/homeSearchFocusHistory'
|
||||
|
||||
/**
|
||||
* B106 — Android / browser back collapses Início search without leaving `/campanha`.
|
||||
* Only active while mounted on the home page publisher.
|
||||
*/
|
||||
export const useHomeSearchFocusHistory = (input: {
|
||||
focused: boolean
|
||||
onCollapse: () => void
|
||||
}): void => {
|
||||
const { focused, onCollapse } = input
|
||||
const historyPushedRef = useRef(false)
|
||||
const closingProgrammaticallyRef = useRef(false)
|
||||
const onCollapseRef = useRef(onCollapse)
|
||||
|
||||
useEffect(() => {
|
||||
onCollapseRef.current = onCollapse
|
||||
}, [onCollapse])
|
||||
|
||||
useEffect(() => {
|
||||
if (!focused || typeof window === 'undefined' || historyPushedRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
window.history.pushState({ [HOME_SEARCH_FOCUS_HISTORY_KEY]: true }, '')
|
||||
historyPushedRef.current = true
|
||||
}, [focused])
|
||||
|
||||
useEffect(() => {
|
||||
if (focused || !historyPushedRef.current || typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
if (window.history.state?.[HOME_SEARCH_FOCUS_HISTORY_KEY]) {
|
||||
closingProgrammaticallyRef.current = true
|
||||
window.history.back()
|
||||
}
|
||||
historyPushedRef.current = false
|
||||
}, [focused])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const handlePopState = () => {
|
||||
if (closingProgrammaticallyRef.current) {
|
||||
closingProgrammaticallyRef.current = false
|
||||
historyPushedRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!historyPushedRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
historyPushedRef.current = false
|
||||
onCollapseRef.current()
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
'use client'
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
} from 'react'
|
||||
|
||||
export type CampaignHomeSearchChromeState = {
|
||||
focused: boolean
|
||||
collapse: () => void
|
||||
}
|
||||
|
||||
type CampaignHomeSearchChromeContextValue = {
|
||||
chrome: CampaignHomeSearchChromeState | null
|
||||
setChrome: Dispatch<SetStateAction<CampaignHomeSearchChromeState | null>>
|
||||
}
|
||||
|
||||
const CampaignHomeSearchChromeContext = createContext<CampaignHomeSearchChromeContextValue | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
export const CampaignHomeSearchChromeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [chrome, setChrome] = useState<CampaignHomeSearchChromeState | null>(null)
|
||||
const value = useMemo(() => ({ chrome, setChrome }), [chrome])
|
||||
|
||||
return (
|
||||
<CampaignHomeSearchChromeContext.Provider value={value}>
|
||||
{children}
|
||||
</CampaignHomeSearchChromeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useCampaignHomeSearchChromeContext = (): CampaignHomeSearchChromeContextValue => {
|
||||
const value = useContext(CampaignHomeSearchChromeContext)
|
||||
if (!value) {
|
||||
throw new Error('CampaignHomeSearchChromeProvider is required')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export const useCampaignHomeSearchChrome = (): CampaignHomeSearchChromeState | null =>
|
||||
useCampaignHomeSearchChromeContext().chrome
|
||||
|
||||
export const useSetCampaignHomeSearchChrome = (
|
||||
state: CampaignHomeSearchChromeState | null,
|
||||
): void => {
|
||||
const { setChrome } = useCampaignHomeSearchChromeContext()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setChrome(state)
|
||||
}, [setChrome, state])
|
||||
|
||||
useLayoutEffect(
|
||||
() => () => {
|
||||
setChrome(null)
|
||||
},
|
||||
[setChrome],
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import type { ReactNode } from 'react'
|
|||
|
||||
import { useCampaignListTransition } from '@/components/campaign/shared/CampaignListPending'
|
||||
import { CampaignWizardNavLink } from '@/components/campaign/shared/CampaignWizardNavLink'
|
||||
import { useCampaignHomeSearchChrome } from '@/components/campaign/shell/CampaignHomeSearchChromeContext'
|
||||
import { useCampaignWizardChrome } from '@/components/campaign/shell/CampaignWizardChromeContext'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SidebarTrigger } from '@/components/ui/Sidebar'
|
||||
|
|
@ -15,6 +16,7 @@ import {
|
|||
wizardFlowChromeAriaLabel,
|
||||
wizardMunicipalityChromeAriaLabel,
|
||||
} from '@/lib/campaignWizardCopy'
|
||||
import { HOME_SEARCH_COLLAPSE_ARIA_LABEL } from '@/lib/campaignHomeSearchContract'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const wizardNavButtonClass =
|
||||
|
|
@ -30,6 +32,7 @@ const wizardTitleSkeletonClass = 'mx-auto motion-reduce:animate-none bg-primary-
|
|||
|
||||
export const CampaignMobileTopBar = ({ notificationBell }: { notificationBell?: ReactNode }) => {
|
||||
const chrome = useCampaignWizardChrome()
|
||||
const homeSearchChrome = useCampaignHomeSearchChrome()
|
||||
const { isPending } = useCampaignListTransition()
|
||||
|
||||
if (chrome) {
|
||||
|
|
@ -123,13 +126,28 @@ export const CampaignMobileTopBar = ({ notificationBell }: { notificationBell?:
|
|||
<header
|
||||
data-slot="campaign-mobile-top-bar"
|
||||
data-mode="app"
|
||||
data-home-search-focused={homeSearchChrome?.focused || undefined}
|
||||
aria-label={WIZARD_APP_TOP_BAR_ARIA_LABEL}
|
||||
className={cn(
|
||||
'flex min-h-14 shrink-0 items-center gap-3 bg-primary px-4 text-primary-foreground md:hidden print:hidden',
|
||||
'pt-[max(0px,env(safe-area-inset-top))]',
|
||||
)}
|
||||
>
|
||||
<SidebarTrigger className="text-primary-foreground" />
|
||||
{homeSearchChrome?.focused ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={wizardNavButtonClass}
|
||||
aria-label={HOME_SEARCH_COLLAPSE_ARIA_LABEL}
|
||||
onClick={homeSearchChrome.collapse}
|
||||
>
|
||||
<ArrowLeft className="size-4 shrink-0" aria-hidden />
|
||||
Voltar
|
||||
</Button>
|
||||
) : (
|
||||
<SidebarTrigger className="text-primary-foreground" />
|
||||
)}
|
||||
<div className="min-w-0 flex-1 leading-tight">
|
||||
<span className="block truncate text-sm font-semibold">Jorge Solla</span>
|
||||
<span className="block truncate text-xs text-primary-foreground/80">Campanha · Bahia</span>
|
||||
|
|
|
|||
|
|
@ -24,3 +24,6 @@ export const normalizeHomeSearchRaw = (raw: string): string => raw.trim()
|
|||
*/
|
||||
export const homeSearchUiFocused = (input: { inputFocused: boolean; isActive: boolean }): boolean =>
|
||||
input.inputFocused || input.isActive
|
||||
|
||||
/** B106 — mobile top bar back control when Início search is expanded. */
|
||||
export const HOME_SEARCH_COLLAPSE_ARIA_LABEL = 'Fechar busca' as const
|
||||
|
|
|
|||
21
src/lib/homeSearchFocusHistory.ts
Normal file
21
src/lib/homeSearchFocusHistory.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/** History state mark for Início mobile search focus (B106). */
|
||||
export const HOME_SEARCH_FOCUS_HISTORY_KEY = 'teqoHomeSearchFocus' as const
|
||||
|
||||
export type HomeSearchFocusHistoryState = {
|
||||
[HOME_SEARCH_FOCUS_HISTORY_KEY]?: true
|
||||
}
|
||||
|
||||
export const isHomeSearchFocusHistoryState = (state: unknown): state is HomeSearchFocusHistoryState =>
|
||||
typeof state === 'object' &&
|
||||
state !== null &&
|
||||
HOME_SEARCH_FOCUS_HISTORY_KEY in state &&
|
||||
(state as HomeSearchFocusHistoryState)[HOME_SEARCH_FOCUS_HISTORY_KEY] === true
|
||||
|
||||
/**
|
||||
* After popstate, the active entry may no longer carry our mark — collapse when
|
||||
* focus mode was open and the browser popped our synthetic entry.
|
||||
*/
|
||||
export const shouldCollapseHomeSearchOnPopstate = (input: {
|
||||
wasHistoryPushed: boolean
|
||||
closingProgrammatically: boolean
|
||||
}): boolean => input.wasHistoryPushed && !input.closingProgrammatically
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch'
|
||||
import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext'
|
||||
import { HomeSearchResultsProvider } from '@/components/campaign/dashboard/HomeSearchResultsContext'
|
||||
import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell'
|
||||
|
||||
const providerValue = {
|
||||
query: { raw: 'ca', debounced: 'ca', isActive: true },
|
||||
|
|
@ -15,6 +17,10 @@ const providerValue = {
|
|||
}
|
||||
|
||||
describe('CampaignHomeSearch', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders search input and results region', () => {
|
||||
render(
|
||||
<HomeSearchProvider value={providerValue}>
|
||||
|
|
@ -29,3 +35,30 @@ describe('CampaignHomeSearch', () => {
|
|||
expect(screen.getByText('Group stub')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('HomeSearchResultsShell', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('shows skeleton rows while fetching without hits (B106)', () => {
|
||||
render(
|
||||
<HomeSearchProvider value={providerValue}>
|
||||
<HomeSearchResultsProvider
|
||||
value={{
|
||||
results: { status: 'loading' },
|
||||
isFetching: true,
|
||||
resultKind: 'idle',
|
||||
}}
|
||||
>
|
||||
<HomeSearchResultsShell>
|
||||
<p>Group stub</p>
|
||||
</HomeSearchResultsShell>
|
||||
</HomeSearchResultsProvider>
|
||||
</HomeSearchProvider>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('home-search-suggest-skeleton')).toBeTruthy()
|
||||
expect(screen.queryByText('Group stub')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
|
|
@ -9,6 +9,11 @@ vi.mock('next/navigation', () => ({
|
|||
}))
|
||||
|
||||
import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar'
|
||||
import {
|
||||
CampaignHomeSearchChromeProvider,
|
||||
useSetCampaignHomeSearchChrome,
|
||||
type CampaignHomeSearchChromeState,
|
||||
} from '@/components/campaign/shell/CampaignHomeSearchChromeContext'
|
||||
import {
|
||||
CampaignWizardChromeProvider,
|
||||
useSetCampaignWizardChrome,
|
||||
|
|
@ -17,27 +22,28 @@ import {
|
|||
import { SidebarProvider } from '@/components/ui/Sidebar'
|
||||
import { stubMatchMedia } from '../helpers/matchMedia'
|
||||
|
||||
const ChromeProbe = ({ chrome }: { chrome: CampaignWizardChromeState | null }) => {
|
||||
const WizardChromeProbe = ({ chrome }: { chrome: CampaignWizardChromeState | null }) => {
|
||||
useSetCampaignWizardChrome(chrome)
|
||||
return null
|
||||
}
|
||||
|
||||
const renderTopBar = (chrome: CampaignWizardChromeState | null) =>
|
||||
render(
|
||||
<SidebarProvider>
|
||||
<CampaignWizardChromeProvider>
|
||||
<ChromeProbe chrome={chrome} />
|
||||
<CampaignMobileTopBar />
|
||||
</CampaignWizardChromeProvider>
|
||||
</SidebarProvider>,
|
||||
)
|
||||
const HomeSearchChromeProbe = ({ chrome }: { chrome: CampaignHomeSearchChromeState | null }) => {
|
||||
useSetCampaignHomeSearchChrome(chrome)
|
||||
return null
|
||||
}
|
||||
|
||||
const renderWizardTopBar = (chrome: CampaignWizardChromeState) =>
|
||||
const renderTopBar = (input: {
|
||||
wizardChrome?: CampaignWizardChromeState | null
|
||||
homeSearchChrome?: CampaignHomeSearchChromeState | null
|
||||
}) =>
|
||||
render(
|
||||
<SidebarProvider>
|
||||
<CampaignWizardChromeProvider>
|
||||
<ChromeProbe chrome={chrome} />
|
||||
<CampaignMobileTopBar />
|
||||
<CampaignHomeSearchChromeProvider>
|
||||
<WizardChromeProbe chrome={input.wizardChrome ?? null} />
|
||||
<HomeSearchChromeProbe chrome={input.homeSearchChrome ?? null} />
|
||||
<CampaignMobileTopBar />
|
||||
</CampaignHomeSearchChromeProvider>
|
||||
</CampaignWizardChromeProvider>
|
||||
</SidebarProvider>,
|
||||
)
|
||||
|
|
@ -53,20 +59,37 @@ describe('CampaignMobileTopBar', () => {
|
|||
})
|
||||
|
||||
it('renders app mode with sidebar trigger when chrome is null', () => {
|
||||
const { container } = renderTopBar(null)
|
||||
const { container } = renderTopBar({})
|
||||
|
||||
const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]')
|
||||
expect(topBar?.getAttribute('data-mode')).toBe('app')
|
||||
expect(screen.getByText('Jorge Solla')).toBeTruthy()
|
||||
expect(screen.queryByRole('link', { name: /Voltar/ })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: 'Fechar busca' })).toBeNull()
|
||||
expect(screen.queryByRole('link', { name: 'Sair da ação' })).toBeNull()
|
||||
})
|
||||
|
||||
it('renders app mode with Voltar when home search is focused (B106)', () => {
|
||||
const collapse = vi.fn()
|
||||
const { container } = renderTopBar({
|
||||
homeSearchChrome: { focused: true, collapse },
|
||||
})
|
||||
|
||||
const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]')
|
||||
expect(topBar?.getAttribute('data-home-search-focused')).toBe('true')
|
||||
expect(screen.queryByRole('button', { name: 'Abrir ou fechar menu da campanha' })).toBeNull()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Fechar busca' }))
|
||||
expect(collapse).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('renders wizard entry mode with dismiss control and flow title', () => {
|
||||
renderWizardTopBar({
|
||||
flowTitle: 'Ajustar votos',
|
||||
stepKind: 'entry',
|
||||
dismissHref: '/campanha',
|
||||
renderTopBar({
|
||||
wizardChrome: {
|
||||
flowTitle: 'Ajustar votos',
|
||||
stepKind: 'entry',
|
||||
dismissHref: '/campanha',
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByText('Ajustar votos')).toBeTruthy()
|
||||
|
|
@ -77,12 +100,14 @@ describe('CampaignMobileTopBar', () => {
|
|||
})
|
||||
|
||||
it('renders wizard continue mode with back link and municipality subtitle', () => {
|
||||
renderWizardTopBar({
|
||||
flowTitle: 'Ajustar votos',
|
||||
municipalityLabel: 'Cairu',
|
||||
stepKind: 'continue',
|
||||
previousHref: '/campanha/acoes/atualizar-votos',
|
||||
dismissHref: '/campanha',
|
||||
renderTopBar({
|
||||
wizardChrome: {
|
||||
flowTitle: 'Ajustar votos',
|
||||
municipalityLabel: 'Cairu',
|
||||
stepKind: 'continue',
|
||||
previousHref: '/campanha/acoes/atualizar-votos',
|
||||
dismissHref: '/campanha',
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByRole('link', { name: /Voltar/ }).getAttribute('href')).toBe(
|
||||
|
|
@ -93,14 +118,16 @@ describe('CampaignMobileTopBar', () => {
|
|||
})
|
||||
|
||||
it('renders skip link instead of dismiss when chained flow defines skip', () => {
|
||||
renderWizardTopBar({
|
||||
flowTitle: 'Registrar sinal',
|
||||
stepKind: 'continue',
|
||||
previousHref: '/campanha/acoes/atualizar-votos?municipio=cairu',
|
||||
dismissHref: '/campanha',
|
||||
skip: {
|
||||
label: 'Pular',
|
||||
href: '/campanha/acoes/mudar-tendencia?municipio=cairu',
|
||||
renderTopBar({
|
||||
wizardChrome: {
|
||||
flowTitle: 'Registrar sinal',
|
||||
stepKind: 'continue',
|
||||
previousHref: '/campanha/acoes/atualizar-votos?municipio=cairu',
|
||||
dismissHref: '/campanha',
|
||||
skip: {
|
||||
label: 'Pular',
|
||||
href: '/campanha/acoes/mudar-tendencia?municipio=cairu',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -114,14 +141,16 @@ describe('CampaignMobileTopBar', () => {
|
|||
const { rerender } = render(
|
||||
<SidebarProvider>
|
||||
<CampaignWizardChromeProvider>
|
||||
<ChromeProbe
|
||||
chrome={{
|
||||
flowTitle: 'Ajustar votos',
|
||||
stepKind: 'entry',
|
||||
dismissHref: '/campanha',
|
||||
}}
|
||||
/>
|
||||
<CampaignMobileTopBar />
|
||||
<CampaignHomeSearchChromeProvider>
|
||||
<WizardChromeProbe
|
||||
chrome={{
|
||||
flowTitle: 'Ajustar votos',
|
||||
stepKind: 'entry',
|
||||
dismissHref: '/campanha',
|
||||
}}
|
||||
/>
|
||||
<CampaignMobileTopBar />
|
||||
</CampaignHomeSearchChromeProvider>
|
||||
</CampaignWizardChromeProvider>
|
||||
</SidebarProvider>,
|
||||
)
|
||||
|
|
@ -131,16 +160,18 @@ describe('CampaignMobileTopBar', () => {
|
|||
rerender(
|
||||
<SidebarProvider>
|
||||
<CampaignWizardChromeProvider>
|
||||
<ChromeProbe
|
||||
chrome={{
|
||||
flowTitle: 'Ajustar votos',
|
||||
municipalityLabel: 'Cairu',
|
||||
stepKind: 'continue',
|
||||
previousHref: '/campanha/acoes/atualizar-votos',
|
||||
dismissHref: '/campanha',
|
||||
}}
|
||||
/>
|
||||
<CampaignMobileTopBar />
|
||||
<CampaignHomeSearchChromeProvider>
|
||||
<WizardChromeProbe
|
||||
chrome={{
|
||||
flowTitle: 'Ajustar votos',
|
||||
municipalityLabel: 'Cairu',
|
||||
stepKind: 'continue',
|
||||
previousHref: '/campanha/acoes/atualizar-votos',
|
||||
dismissHref: '/campanha',
|
||||
}}
|
||||
/>
|
||||
<CampaignMobileTopBar />
|
||||
</CampaignHomeSearchChromeProvider>
|
||||
</CampaignWizardChromeProvider>
|
||||
</SidebarProvider>,
|
||||
)
|
||||
|
|
|
|||
38
tests/unit/homeSearchFocusHistory.unit.spec.ts
Normal file
38
tests/unit/homeSearchFocusHistory.unit.spec.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
HOME_SEARCH_FOCUS_HISTORY_KEY,
|
||||
isHomeSearchFocusHistoryState,
|
||||
shouldCollapseHomeSearchOnPopstate,
|
||||
} from '@/lib/homeSearchFocusHistory'
|
||||
|
||||
describe('homeSearchFocusHistory', () => {
|
||||
it('recognizes the focus history mark', () => {
|
||||
expect(isHomeSearchFocusHistoryState({ [HOME_SEARCH_FOCUS_HISTORY_KEY]: true })).toBe(true)
|
||||
expect(isHomeSearchFocusHistoryState({})).toBe(false)
|
||||
expect(isHomeSearchFocusHistoryState(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('collapses on popstate only when focus history was pushed by us', () => {
|
||||
expect(
|
||||
shouldCollapseHomeSearchOnPopstate({
|
||||
wasHistoryPushed: true,
|
||||
closingProgrammatically: false,
|
||||
}),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
shouldCollapseHomeSearchOnPopstate({
|
||||
wasHistoryPushed: true,
|
||||
closingProgrammatically: true,
|
||||
}),
|
||||
).toBe(false)
|
||||
|
||||
expect(
|
||||
shouldCollapseHomeSearchOnPopstate({
|
||||
wasHistoryPushed: false,
|
||||
closingProgrammatically: false,
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue