Merge pull request #138 from fsolla/cursor/pool-i129-7bc105bb-7d33
B102 — Crash — busca no bottom drawer (focus / digitação)
This commit is contained in:
commit
de9377de85
5 changed files with 129 additions and 12 deletions
|
|
@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
---
|
||||
|
||||
**Recently resolved (2026-08-01):** **B102** (Issue #129) — crash na busca do bottom drawer (focus/digitação): `TooltipProvider` do layout `(app)` envolvia só `{children}` e o drawer (irmão do scrollport) renderizava hits `priority:alta` via `CampaignHoverTooltip` sem provider. Fix: provider envolve `CampaignAppScrollChrome`; pins unit focus→suggest e type→search. Plano: [`crash-busca-bottom-drawer.md`](docs/plans/crash-busca-bottom-drawer.md).
|
||||
|
||||
**Recently resolved (2026-08-01):** **Pool pin `fast=false`** — `resolvePoolModel` agora envia `fast=false` em todo spawn cuja família Cloud anuncia o param (`composer-2.5`, `grok-4.5`, …). Antes o supervisor pedia só `{ id: "composer-2.5" }`; a API Create-Agent resolve o default da variante (`fast=true`) e o usage dashboard mostrava `composer-2.5-fast` em todos os workers do pool. Skills `model-selection` / `agent-pool` + plano do orquestrador atualizados.
|
||||
|
||||
**Recently resolved (2026-08-01):** **C8 F1–F2** (Issue #37) — freshness audit confirmou entrega em código desde 2026-07-19: locks advisory em batch (`acquireTextAdvisoryLocks`), bulk import com `.returning()` + leituras drizzle, overview derivando `total` de `totalDocs`, migration `pg_trgm`, guard `contactSearchQuery` (mín. 2 chars). Plano [`escala-dry-pos-c6.md`](docs/plans/escala-dry-pos-c6.md) fechado; follow-ups de filtros/forms permanecem no C9.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
# Crash — busca no bottom drawer (focus / digitação)
|
||||
|
||||
Status: registrado
|
||||
Status: done
|
||||
Atualizado em: 2026-08-01
|
||||
Issue: #129 (file-miss #133)
|
||||
Issue: #129 (file-miss plan #133 · delivery #135)
|
||||
Priority: P0
|
||||
Model: cursor-grok-4.5-medium
|
||||
Impeccable: B — `CampaignQuickActionsDrawer` + `CampaignGlobalSearchBody`
|
||||
Appetite: ~0,5–1d eng; root-cause + fix + pin focus/type; sem migration
|
||||
Responsável: —
|
||||
|
||||
## Freshness audit (2026-08-01)
|
||||
|
||||
- Arquivos citados existem; assinaturas de `useHomeSearch` / `useHomeSearchResultsState` / drawer intactas.
|
||||
- Sem migration; deps soft B91/B100 done.
|
||||
- **Root cause confirmada (repro unit):** `TooltipProvider` no layout `(app)` envolvia só `{children}` dentro de `CampaignAppScrollChrome`. O drawer monta como **irmão** do scrollport → fora do provider. Focus → `mode:suggest` → hits `priority:'alta'` → `MunicipalityPriorityIndicator` → `CampaignHoverTooltip` → throw ``Tooltip` must be used within `TooltipProvider`` → página cai. Início não crasha porque o drawer não monta lá e a busca vive sob o provider dos children. Digitar com hits prioritários crashava pelo mesmo motivo; o pin B100 só mudava query com payload vazio (sem flag).
|
||||
|
||||
## Design (Impeccable)
|
||||
|
||||
Âncoras: `PRODUCT.md` (Feel the action) · B91/B100 drawer search · tema `campaign`.
|
||||
|
|
@ -66,10 +72,11 @@ Diagnóstico preliminar (código em `main`):
|
|||
- **Fix mínimo no caminho focus→suggest→render; não redesenhar snap.** Geometria/gesto → **B105**. **Rejeitado:** reescrever busca só para o drawer.
|
||||
- **File-miss obrigatório** com causa + PR culpado quando conhecido. **Rejeitado:** só Issue de feature sem harvest.
|
||||
- **i18n:** ids existentes; sem copy nova.
|
||||
- **Fix (as-built):** elevar `TooltipProvider` no layout `(app)` para envolver `CampaignAppScrollChrome` inteiro (scroll + drawer). Harness unit espelha isso.
|
||||
|
||||
## Questões em aberto
|
||||
|
||||
- **Causa raiz exata antes do claim?** **Opções:** A) fechar no `work-issue` com repro primeiro | B) bipartir plan/exec. **Recomendação:** **A** — appetite cabe; hypothese já estreita. _(assumido)_
|
||||
- _(nenhuma — root cause fechada no repro unit; fix as-built acima)_
|
||||
|
||||
## Abordagem proposta
|
||||
|
||||
|
|
|
|||
|
|
@ -86,9 +86,16 @@ export default async function CampaignAppLayout({ children }: { children: React.
|
|||
<CampaignNotificationBellSlot user={user} />
|
||||
</div>
|
||||
</header>
|
||||
<CampaignAppScrollChrome role={user.role}>
|
||||
<TooltipProvider delayDuration={300}>{children}</TooltipProvider>
|
||||
</CampaignAppScrollChrome>
|
||||
{/*
|
||||
Provider must wrap CampaignAppScrollChrome, not only page children:
|
||||
the mobile quick-actions drawer (B91/B100) mounts as a sibling of
|
||||
the scrollport and renders search hits with CampaignHoverTooltip
|
||||
(priority flag). Nested only around {children} left focus→suggest
|
||||
without a provider and crashed the page (B102).
|
||||
*/}
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<CampaignAppScrollChrome role={user.role}>{children}</CampaignAppScrollChrome>
|
||||
</TooltipProvider>
|
||||
<Toaster position="top-center" />
|
||||
<InstallPwaToast />
|
||||
{biometricEnrollment ? <BiometricEnrollmentToast {...biometricEnrollment} /> : null}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
CampaignQuickActionsSnapProvider,
|
||||
useCampaignQuickActionsSnap,
|
||||
} from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
|
||||
import { postCampaignJson } from '@/lib/campaignJsonRequest'
|
||||
import {
|
||||
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
|
||||
|
|
@ -26,9 +28,9 @@ vi.mock('next/navigation', () => ({
|
|||
usePathname: () => '/campanha/municipios/foo',
|
||||
}))
|
||||
|
||||
const idleSuggestPayload = {
|
||||
status: 'success' as const,
|
||||
resultKind: 'suggest' as const,
|
||||
const idleSuggestPayload: HomeSearchSuccessResponse = {
|
||||
status: 'success',
|
||||
resultKind: 'suggest',
|
||||
municipalities: [],
|
||||
territories: [],
|
||||
advisors: [],
|
||||
|
|
@ -38,6 +40,26 @@ const idleSuggestPayload = {
|
|||
demands: [],
|
||||
}
|
||||
|
||||
/** Priority-alta hit — mounts MunicipalityPriorityIndicator → CampaignHoverTooltip. */
|
||||
const suggestWithPriorityPayload: HomeSearchSuccessResponse = {
|
||||
...idleSuggestPayload,
|
||||
municipalities: [
|
||||
{
|
||||
kind: 'municipality',
|
||||
slug: 'cairu',
|
||||
name: 'Cairu',
|
||||
region: 'Baixo Sul',
|
||||
priority: 'alta',
|
||||
votePosition2022: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const searchWithPriorityPayload: HomeSearchSuccessResponse = {
|
||||
...suggestWithPriorityPayload,
|
||||
resultKind: 'search',
|
||||
}
|
||||
|
||||
const matchMediaMock = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -59,11 +81,17 @@ afterEach(() => {
|
|||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/**
|
||||
* Mirrors campaign (app) layout: TooltipProvider wraps chrome that includes the
|
||||
* drawer (B102). Nested only around page children left focus→suggest crashing.
|
||||
*/
|
||||
const renderQuickActionsChrome = (ui: ReactNode) =>
|
||||
render(
|
||||
<CampaignQuickActionsSnapProvider>
|
||||
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
|
||||
</CampaignQuickActionsSnapProvider>,
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<CampaignQuickActionsSnapProvider>
|
||||
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
|
||||
</CampaignQuickActionsSnapProvider>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const SnapReadout = () => {
|
||||
|
|
@ -134,6 +162,60 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('focus posts suggest and renders priority hits without crashing (B102)', async () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: suggestWithPriorityPayload,
|
||||
})
|
||||
|
||||
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
|
||||
|
||||
fireEvent.focus(screen.getByLabelText('Buscar na campanha'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(postCampaignJson).toHaveBeenCalledWith(
|
||||
'/campanha/home-search',
|
||||
{ mode: 'suggest' },
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('region', { name: 'Sugestões' })).toBeTruthy()
|
||||
expect(screen.getByText('Cairu')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Município prioritário')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('typing keeps search results with priority hits mounted (B102)', async () => {
|
||||
vi.mocked(postCampaignJson).mockImplementation(async (_url, body) => {
|
||||
if (typeof body === 'object' && body !== null && 'mode' in body && body.mode === 'search') {
|
||||
return { ok: true, payload: searchWithPriorityPayload }
|
||||
}
|
||||
return { ok: true, payload: idleSuggestPayload }
|
||||
})
|
||||
|
||||
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
|
||||
|
||||
const input = screen.getByLabelText('Buscar na campanha')
|
||||
fireEvent.focus(input)
|
||||
fireEvent.change(input, { target: { value: 'ca' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(postCampaignJson).toHaveBeenCalledWith(
|
||||
'/campanha/home-search',
|
||||
{ mode: 'search', query: 'ca' },
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('region', { name: 'Municípios' })).toBeTruthy()
|
||||
expect(screen.getByText('Cairu')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Município prioritário')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('toggles between dock and collapsed from the handle', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
|
|
|
|||
|
|
@ -667,3 +667,22 @@ describe('public site metadata global access', () => {
|
|||
expect(offenders, 'import and call resolveSiteMetadata from @/utilities/seo').toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('campaign TooltipProvider wraps quick-actions chrome (B102)', () => {
|
||||
// B91/B100 mount the drawer as a sibling of the scrollport inside
|
||||
// CampaignAppScrollChrome. Nesting TooltipProvider only around {children}
|
||||
// left focus→suggest priority hits (CampaignHoverTooltip) without a
|
||||
// provider and crashed the page.
|
||||
it('lifts TooltipProvider around CampaignAppScrollChrome in the app layout', () => {
|
||||
const layoutPath = resolve(repoRoot, 'src/app/(campaign)/campanha/(app)/layout.tsx')
|
||||
expect(existsSync(layoutPath)).toBe(true)
|
||||
const source = readFileSync(layoutPath, 'utf8')
|
||||
|
||||
expect(source).toMatch(
|
||||
/<TooltipProvider[^>]*>\s*<CampaignAppScrollChrome[\s\S]*?<\/CampaignAppScrollChrome>\s*<\/TooltipProvider>/,
|
||||
)
|
||||
expect(source).not.toMatch(
|
||||
/<CampaignAppScrollChrome[^>]*>\s*<TooltipProvider[\s\S]*?<\/TooltipProvider>\s*<\/CampaignAppScrollChrome>/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue