Merge pull request #119 from fsolla/cursor/pool-i114-36b6c1a5-2d0b

B100 — Bottom drawer mobile — dock visível + recolhe no scroll
This commit is contained in:
Francisco Solla 2026-08-01 08:49:14 -03:00 committed by GitHub
commit a7394d3b9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 330 additions and 92 deletions

View file

@ -83,13 +83,14 @@ export const E2E_AFFECTED_MANIFEST = [
{
prefixes: [
'src/components/campaign/shell',
'src/components/ui/Drawer',
`${CAMPAIGN_APP}/layout.tsx`,
'src/utilities/campaignPwa',
'src/lib/campaignQuickAction',
'src/lib/campaignReferenceQuickActions',
'src/lib/campaignPaths.ts',
],
specs: ['campaign-pwa', 'campaignWizardChrome'],
specs: ['campaign-pwa', 'campaignWizardChrome', 'campaignMunicipalities'],
},
// Domains without a dedicated e2e family still wake campaign home smoke so
// the affected classifier cannot return mode=none on an unmapped domain dir.

View file

@ -12,12 +12,5 @@ export const CampaignGlobalSearchProvider = ({ children }: { children: ReactNode
return <HomeSearchProvider value={searchState}>{children}</HomeSearchProvider>
}
/** Input + result groups — requires `CampaignGlobalSearchProvider` unless using the mount. */
/** Input + result groups — requires `CampaignGlobalSearchProvider` ancestor. */
export const CampaignGlobalSearchBody = CampaignStaffGlobalSearchBody
/** Provider + default global search body (drawer, smoke tests). */
export const CampaignGlobalSearchMount = () => (
<CampaignGlobalSearchProvider>
<CampaignGlobalSearchBody />
</CampaignGlobalSearchProvider>
)

View file

@ -2,11 +2,13 @@
import type { ReactNode } from 'react'
import { CampaignGlobalSearchProvider } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
import {
CampaignContentScroll,
CampaignQuickActionsHost,
useQuickActionsChromeActive,
} from '@/components/campaign/shell/CampaignQuickActionsHost'
import { CampaignQuickActionsSnapProvider } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import type { CampaignRole } from '@/lib/campaignRoles'
export const CampaignAppScrollChrome = ({
@ -16,12 +18,18 @@ export const CampaignAppScrollChrome = ({
role: CampaignRole
children: ReactNode
}) => {
const quickActionsPeek = useQuickActionsChromeActive(role)
const quickActionsActive = useQuickActionsChromeActive(role)
if (!quickActionsActive) {
return <CampaignContentScroll quickActionsPeek={false}>{children}</CampaignContentScroll>
}
return (
<>
<CampaignContentScroll quickActionsPeek={quickActionsPeek}>{children}</CampaignContentScroll>
<CampaignQuickActionsHost role={role} />
</>
<CampaignQuickActionsSnapProvider>
<CampaignGlobalSearchProvider>
<CampaignContentScroll quickActionsPeek>{children}</CampaignContentScroll>
<CampaignQuickActionsHost role={role} />
</CampaignGlobalSearchProvider>
</CampaignQuickActionsSnapProvider>
)
}

View file

@ -1,15 +1,16 @@
'use client'
import { useCallback, useState } from 'react'
import { useCallback } from 'react'
import { CampaignGlobalSearchMount } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
import { CampaignGlobalSearchBody } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip'
import { useCampaignQuickActionsSnap } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/Drawer'
import {
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_EXPANDED,
QUICK_ACTIONS_SNAP_DOCK,
QUICK_ACTIONS_SNAP_POINTS,
quickActionsSnapIsExpanded,
quickActionsSnapIsDock,
type QuickActionsSnapPoint,
} from '@/lib/campaignQuickActionSnap'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
@ -22,36 +23,34 @@ export const CampaignQuickActionsDrawer = ({
}: {
actions: readonly CampaignQuickAction[]
}) => {
const [snapPoint, setSnapPoint] = useState<QuickActionsSnapPoint | null>(
QUICK_ACTIONS_SNAP_COLLAPSED,
)
const expanded = quickActionsSnapIsExpanded(snapPoint)
const { snapPoint, setSnapPoint, isDock } = useCampaignQuickActionsSnap()
const showActions = actions.length > 0
const handleSnapPointChange = useCallback(
(next: QuickActionsSnapPoint | string | number | null) => {
if (next === QUICK_ACTIONS_SNAP_COLLAPSED || next === QUICK_ACTIONS_SNAP_EXPANDED) {
if (next === QUICK_ACTIONS_SNAP_COLLAPSED || next === QUICK_ACTIONS_SNAP_DOCK) {
setSnapPoint(next)
return
}
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
},
[],
[setSnapPoint],
)
const toggleSnap = useCallback(() => {
setSnapPoint((current) =>
quickActionsSnapIsExpanded(current)
? QUICK_ACTIONS_SNAP_COLLAPSED
: QUICK_ACTIONS_SNAP_EXPANDED,
quickActionsSnapIsDock(current) ? QUICK_ACTIONS_SNAP_COLLAPSED : QUICK_ACTIONS_SNAP_DOCK,
)
}, [])
}, [setSnapPoint])
const handleOpenChange = useCallback((open: boolean) => {
if (!open) {
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
}
}, [])
const handleOpenChange = useCallback(
(open: boolean) => {
if (!open) {
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
}
},
[setSnapPoint],
)
return (
<Drawer
@ -66,29 +65,17 @@ export const CampaignQuickActionsDrawer = ({
>
<DrawerContent
id="CampaignQuickActionsDrawer"
className={cn(
'border-t border-border bg-background text-foreground shadow-[0_-4px_24px_-8px_rgb(0_0_0/0.12)] print:hidden',
!expanded && 'pointer-events-auto',
)}
className="border-t border-border bg-background text-foreground shadow-[0_-4px_24px_-8px_rgb(0_0_0/0.12)] print:hidden"
>
<DrawerTitle className="sr-only">Ações rápidas</DrawerTitle>
<button
type="button"
onClick={toggleSnap}
aria-expanded={expanded}
aria-controls="quickActionContext"
className="flex w-full shrink-0 cursor-grab flex-col items-center border-0 bg-transparent px-4 pt-2 pb-1 active:cursor-grabbing"
aria-label={expanded ? 'Ocultar ações rápidas' : 'Mostrar ações rápidas'}
>
<span aria-hidden className="mb-1 block h-1 w-12 rounded-full bg-muted" />
</button>
<div
id="quickActionContext"
className={cn(
'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain px-4 pb-[max(1rem,env(safe-area-inset-bottom,0px))]',
!expanded && 'hidden',
'flex min-h-0 flex-col gap-4 overflow-hidden px-4 pt-2',
isDock ? 'flex-1' : 'hidden',
)}
data-snap={expanded ? 'expanded' : 'collapsed'}
data-snap={isDock ? 'dock' : 'collapsed'}
aria-hidden={!isDock}
>
{showActions ? (
<CampaignHomeActionStrip
@ -102,8 +89,18 @@ export const CampaignQuickActionsDrawer = ({
className="w-full"
/>
) : null}
{expanded ? <CampaignGlobalSearchMount /> : null}
<CampaignGlobalSearchBody />
</div>
<button
type="button"
onClick={toggleSnap}
aria-expanded={isDock}
aria-controls="quickActionContext"
className="flex w-full shrink-0 cursor-grab flex-col items-center border-0 bg-transparent px-4 pt-2 pb-[max(0.25rem,env(safe-area-inset-bottom,0px))] active:cursor-grabbing"
aria-label={isDock ? 'Ocultar ações rápidas' : 'Mostrar ações rápidas'}
>
<span aria-hidden className="mb-1 block h-1 w-12 rounded-full bg-muted" />
</button>
</DrawerContent>
</Drawer>
)

View file

@ -1,39 +1,85 @@
'use client'
import { usePathname } from 'next/navigation'
import type { ReactNode } from 'react'
import { useCallback, useEffect, useRef, type ReactNode } from 'react'
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
import { useCampaignQuickActionContext } from '@/components/campaign/shell/CampaignQuickActionContext'
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
import { useCampaignQuickActionsSnap } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import { useIsMobile } from '@/hooks/use-mobile'
import { shouldMountQuickActionsDrawer } from '@/lib/campaignQuickActionMount'
import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry'
import { QUICK_ACTIONS_SNAP_COLLAPSED } from '@/lib/campaignQuickActionSnap'
import {
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_DOCK,
} from '@/lib/campaignQuickActionSnap'
import type { CampaignRole } from '@/lib/campaignRoles'
import { cn } from '@/lib/utils'
const CampaignContentScrollWithPeek = ({ children }: { children: ReactNode }) => {
const scrollRef = useRef<HTMLDivElement>(null)
const { isDock, setSnapPoint } = useCampaignQuickActionsSnap()
const { uiFocused } = useHomeSearch()
const peekHeight = isDock ? QUICK_ACTIONS_SNAP_DOCK : QUICK_ACTIONS_SNAP_COLLAPSED
const collapseIfScrolled = useCallback(() => {
if (uiFocused) return
const scrollport = scrollRef.current
if (!scrollport) return
if (scrollport.scrollTop > QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX) {
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
}
}, [setSnapPoint, uiFocused])
useEffect(() => {
const scrollport = scrollRef.current
if (!scrollport) return
scrollport.addEventListener('scroll', collapseIfScrolled, { passive: true })
return () => {
scrollport.removeEventListener('scroll', collapseIfScrolled)
}
}, [collapseIfScrolled])
return (
<div
ref={scrollRef}
data-slot="campaign-content-scroll"
className={cn(
'min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 md:p-6 print:h-auto print:overflow-visible print:p-0',
'pb-[calc(1rem+var(--campaign-quick-actions-peek))] md:pb-6',
)}
style={{ '--campaign-quick-actions-peek': peekHeight } as React.CSSProperties}
>
{children}
</div>
)
}
export const CampaignContentScroll = ({
children,
quickActionsPeek,
}: {
children: ReactNode
quickActionsPeek: boolean
}) => (
<div
data-slot="campaign-content-scroll"
className={cn(
'min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 md:p-6 print:h-auto print:overflow-visible print:p-0',
quickActionsPeek && 'pb-[calc(1rem+var(--campaign-quick-actions-peek))] md:pb-6',
)}
style={
quickActionsPeek
? ({ '--campaign-quick-actions-peek': QUICK_ACTIONS_SNAP_COLLAPSED } as React.CSSProperties)
: undefined
}
>
{children}
</div>
)
}) => {
if (!quickActionsPeek) {
return (
<div
data-slot="campaign-content-scroll"
className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 md:p-6 print:h-auto print:overflow-visible print:p-0"
>
{children}
</div>
)
}
return <CampaignContentScrollWithPeek>{children}</CampaignContentScrollWithPeek>
}
export const CampaignQuickActionsHost = ({ role }: { role: CampaignRole }) => {
const pathname = usePathname()

View file

@ -0,0 +1,61 @@
'use client'
import { usePathname } from 'next/navigation'
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type Dispatch,
type ReactNode,
type SetStateAction,
} from 'react'
import {
QUICK_ACTIONS_SNAP_DOCK,
quickActionsSnapIsDock,
type QuickActionsSnapPoint,
} from '@/lib/campaignQuickActionSnap'
type CampaignQuickActionsSnapContextValue = {
snapPoint: QuickActionsSnapPoint | null
setSnapPoint: Dispatch<SetStateAction<QuickActionsSnapPoint | null>>
isDock: boolean
}
const CampaignQuickActionsSnapContext = createContext<CampaignQuickActionsSnapContextValue | null>(
null,
)
export const CampaignQuickActionsSnapProvider = ({ children }: { children: ReactNode }) => {
const pathname = usePathname()
const [snapPoint, setSnapPoint] = useState<QuickActionsSnapPoint | null>(QUICK_ACTIONS_SNAP_DOCK)
useEffect(() => {
setSnapPoint(QUICK_ACTIONS_SNAP_DOCK)
}, [pathname])
const value = useMemo(
(): CampaignQuickActionsSnapContextValue => ({
snapPoint,
setSnapPoint,
isDock: quickActionsSnapIsDock(snapPoint),
}),
[snapPoint],
)
return (
<CampaignQuickActionsSnapContext.Provider value={value}>
{children}
</CampaignQuickActionsSnapContext.Provider>
)
}
export const useCampaignQuickActionsSnap = (): CampaignQuickActionsSnapContextValue => {
const value = useContext(CampaignQuickActionsSnapContext)
if (!value) {
throw new Error('CampaignQuickActionsSnapProvider is required')
}
return value
}

View file

@ -137,7 +137,7 @@ const DrawerContent = ({ className, children, ...props }: DrawerPrimitive.Popup.
'data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)]',
'data-[swipe-axis=y]:inset-x-0 data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height)',
'data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row',
'data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]',
'data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))] data-[swipe-direction=down]:data-snap-points:pb-[max(0px,calc(var(--drawer-snap-point-offset)+var(--drawer-swipe-movement-y)))] data-[swipe-direction=down]:data-snap-points:data-starting-style:pb-0 data-[swipe-direction=down]:data-snap-points:data-ending-style:pb-0',
'data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]',
'data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:origin-left data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]',
'data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:origin-right data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]',

View file

@ -1,15 +1,18 @@
/** Collapsed peek — swipe handle only (replaces legacy bottom-nav `pb-24`). */
export const QUICK_ACTIONS_SNAP_COLLAPSED = '3rem' as const
/** Expanded — actions strip + global search inside the drawer. */
export const QUICK_ACTIONS_SNAP_EXPANDED = 0.55 as const
/** Dock — action strip + global search visible on load and after navigation. */
export const QUICK_ACTIONS_SNAP_DOCK = '12rem' as const
/** Scroll down past this threshold on `campaign-content-scroll` collapses the drawer. */
export const QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX = 24
export const QUICK_ACTIONS_SNAP_POINTS = [
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_EXPANDED,
QUICK_ACTIONS_SNAP_DOCK,
] as const
export type QuickActionsSnapPoint = (typeof QUICK_ACTIONS_SNAP_POINTS)[number]
export const quickActionsSnapIsExpanded = (snap: QuickActionsSnapPoint | null): boolean =>
snap === QUICK_ACTIONS_SNAP_EXPANDED
export const quickActionsSnapIsDock = (snap: QuickActionsSnapPoint | null): boolean =>
snap === QUICK_ACTIONS_SNAP_DOCK

View file

@ -406,3 +406,47 @@ test.describe('Municípios — cards no celular (B42)', () => {
await expect(page.getByRole('heading', { name: municipality.name })).toBeVisible()
})
})
/**
* B100: bottom drawer loads docked with search visible, collapses on page scroll,
* and reopens only from the handle — not when scrolling back to the top.
*/
test.describe('Municípios — bottom drawer mobile (B100)', () => {
test.use({ viewport: { width: 390, height: 844 } })
test('dock on load, collapse on scroll down, reopen on handle tap', async ({
campaign,
page,
}) => {
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenador Drawer'),
})
const municipality = await fixtures.claimMunicipality()
await campaign.login(page, coordinator.email!, coordinator.password)
await page.goto(`${campaign.baseURL}/campanha/municipios/${municipality.slug}`)
await expect(page.getByRole('heading', { name: municipality.name })).toBeVisible()
const search = page.getByLabel('Buscar na campanha')
await expect(search).toBeInViewport()
const scrollport = page.locator('[data-slot="campaign-content-scroll"]')
await scrollport.evaluate((el) => {
el.scrollTop = 80
el.dispatchEvent(new Event('scroll'))
})
await expect(page.getByRole('button', { name: 'Mostrar ações rápidas' })).toBeVisible()
await expect(search).not.toBeInViewport()
await scrollport.evaluate((el) => {
el.scrollTop = 0
el.dispatchEvent(new Event('scroll'))
})
await expect(search).not.toBeInViewport()
await page.getByRole('button', { name: 'Mostrar ações rápidas' }).click()
await expect(search).toBeInViewport()
})
})

View file

@ -38,8 +38,8 @@ import {
import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry'
import {
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_EXPANDED,
quickActionsSnapIsExpanded,
QUICK_ACTIONS_SNAP_DOCK,
quickActionsSnapIsDock,
} from '@/lib/campaignQuickActionSnap'
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
@ -395,9 +395,9 @@ describe('campaignQuickActionDemands resolvers', () => {
})
describe('campaignQuickActionSnap', () => {
it('detects expanded snap', () => {
expect(quickActionsSnapIsExpanded(QUICK_ACTIONS_SNAP_EXPANDED)).toBe(true)
expect(quickActionsSnapIsExpanded(QUICK_ACTIONS_SNAP_COLLAPSED)).toBe(false)
expect(quickActionsSnapIsExpanded(null)).toBe(false)
it('detects dock snap', () => {
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_DOCK)).toBe(true)
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_COLLAPSED)).toBe(false)
expect(quickActionsSnapIsDock(null)).toBe(false)
})
})

View file

@ -1,13 +1,30 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import type { ReactNode } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { CampaignGlobalSearchProvider } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
import { CampaignContentScroll } from '@/components/campaign/shell/CampaignQuickActionsHost'
import {
CampaignQuickActionsSnapProvider,
useCampaignQuickActionsSnap,
} from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import { postCampaignJson } from '@/lib/campaignJsonRequest'
import {
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_DOCK,
quickActionsSnapIsDock,
} from '@/lib/campaignQuickActionSnap'
vi.mock('@/lib/campaignJsonRequest', () => ({
postCampaignJson: vi.fn(),
}))
vi.mock('next/navigation', () => ({
usePathname: () => '/campanha/municipios/foo',
}))
const idleSuggestPayload = {
status: 'success' as const,
resultKind: 'suggest' as const,
@ -20,29 +37,38 @@ const idleSuggestPayload = {
demands: [],
}
const renderQuickActionsChrome = (ui: ReactNode) =>
render(
<CampaignQuickActionsSnapProvider>
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
</CampaignQuickActionsSnapProvider>,
)
const SnapReadout = () => {
const { snapPoint } = useCampaignQuickActionsSnap()
return <div data-testid="snap">{snapPoint ?? 'null'}</div>
}
afterEach(() => {
cleanup()
vi.mocked(postCampaignJson).mockReset()
})
describe('CampaignQuickActionsDrawer (B91)', () => {
it('shows global search in the expanded snap', () => {
describe('CampaignQuickActionsDrawer (B100)', () => {
it('loads in dock snap with global search visible', () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: idleSuggestPayload,
})
render(<CampaignQuickActionsDrawer actions={[]} />)
expect(screen.queryByLabelText('Buscar na campanha')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
const context = document.getElementById('quickActionContext')
expect(context?.className).not.toContain('hidden')
expect(context?.getAttribute('data-snap')).toBe('expanded')
expect(context?.className.split(/\s+/)).not.toContain('hidden')
expect(context?.getAttribute('data-snap')).toBe('dock')
expect(screen.getByLabelText('Buscar na campanha')).toBeTruthy()
expect(screen.getByRole('region', { name: 'Resultados da busca' })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Ocultar ações rápidas' })).toBeTruthy()
})
it('posts home-search when the drawer query is active', async () => {
@ -51,9 +77,8 @@ describe('CampaignQuickActionsDrawer (B91)', () => {
payload: idleSuggestPayload,
})
render(<CampaignQuickActionsDrawer actions={[]} />)
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
fireEvent.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
fireEvent.change(screen.getByLabelText('Buscar na campanha'), { target: { value: 'cairu' } })
await waitFor(() => {
@ -64,4 +89,64 @@ describe('CampaignQuickActionsDrawer (B91)', () => {
)
})
})
it('toggles between dock and collapsed from the handle', () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: idleSuggestPayload,
})
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
fireEvent.click(screen.getByRole('button', { name: 'Ocultar ações rápidas' }))
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe(
'collapsed',
)
expect(document.getElementById('quickActionContext')?.className).toContain('hidden')
expect(screen.getByRole('button', { name: 'Mostrar ações rápidas' })).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe('dock')
})
})
describe('CampaignContentScroll quick-actions collapse (B100)', () => {
it('collapses on content scroll down but not when scrolling back to top', () => {
renderQuickActionsChrome(
<>
<CampaignContentScroll quickActionsPeek>
<div style={{ height: '2000px' }} />
</CampaignContentScroll>
<SnapReadout />
</>,
)
const scrollport = document.querySelector('[data-slot="campaign-content-scroll"]')
expect(scrollport).toBeTruthy()
expect(screen.getByTestId('snap').textContent).toBe(QUICK_ACTIONS_SNAP_DOCK)
Object.defineProperty(scrollport, 'scrollTop', {
configurable: true,
value: QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX + 1,
})
fireEvent.scroll(scrollport!)
expect(screen.getByTestId('snap').textContent).toBe(QUICK_ACTIONS_SNAP_COLLAPSED)
Object.defineProperty(scrollport, 'scrollTop', {
configurable: true,
value: 0,
})
fireEvent.scroll(scrollport!)
expect(screen.getByTestId('snap').textContent).toBe(QUICK_ACTIONS_SNAP_COLLAPSED)
})
})
describe('campaignQuickActionSnap', () => {
it('detects dock snap', () => {
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_DOCK)).toBe(true)
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_COLLAPSED)).toBe(false)
expect(quickActionsSnapIsDock(null)).toBe(false)
})
})