test(miss-52): sheet cells without the shared host fail loud

- CampaignCellEditOverlay: dev-mode console.error when a variant=sheet
  cell mounts without CampaignListSheetProvider (prod keeps the
  lazy-mount fallback for resilience; unit tests exercise it, so the
  invariant is development-only).
- sharedSheetHostConventions.unit.spec.ts: any tsx rendering
  <MunicipalityPortfolioCell / <LeadershipStateDeputyRelationCell must
  reference CampaignListSheetProvider — the next chip table cannot
  reintroduce per-cell Drawers by omission.
This commit is contained in:
Cursor Agent 2026-07-31 05:50:41 +00:00 committed by Francisco Ignácio Fontoura Solla
parent 6f42bc3bf3
commit c83cf28497
2 changed files with 54 additions and 1 deletions

View file

@ -1,6 +1,6 @@
'use client'
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { CampaignHoverTooltip } from '@/components/campaign/shared/CampaignHoverTooltip'
@ -110,6 +110,18 @@ export const CampaignCellEditOverlay = ({
const sharedSheet = useCampaignListSheet()
const wasOpenRef = useRef(open)
// Miss #52: without the provider every opened cell keeps its own Drawer
// root. The fallback below stays for prod resilience, but in dev the
// missing provider is called out the moment a sheet cell mounts.
useEffect(() => {
if (variant !== 'sheet' || sharedSheet) return
if (process.env.NODE_ENV !== 'development') return
console.error(
'CampaignCellEditOverlay: variant="sheet" sem CampaignListSheetProvider — ' +
'cada célula aberta mantém um Drawer próprio (miss #52). Envolva a lista no provider.',
)
}, [variant, sharedSheet])
/**
* The region is mounted from the first open onwards and never unmounted
* again — a live region has to exist BEFORE its text changes or most screen

View file

@ -0,0 +1,41 @@
import { readdirSync, readFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
// Miss #52 (2026-07-30): the municipalities list hydrated ~125 idle Drawer
// roots (25 rows × 5 sheet controls) and prod tripped React #130 — CSS
// `display:none` does not remove hydration cost. The fix pattern is ONE
// shared Drawer per list surface via `CampaignListSheetProvider`, with cell
// bodies portaled into it. The provider reached the municípios mobile cards
// and this pass (2026-07-31) the three chip-relation lists (lideranças,
// dobradinhas, assessores) — this guard keeps the next chip-cell table from
// mounting sheet overlays providerless.
describe('shared sheet host for chip-relation cells (miss #52)', () => {
const componentRoots = ['src/components', 'src/app'] as const
const chipCellUsage = /<MunicipalityPortfolioCell|<LeadershipStateDeputyRelationCell/
it('wraps every chip-relation table in CampaignListSheetProvider', () => {
const offenders: string[] = []
for (const root of componentRoots) {
const files = readdirSync(resolve(process.cwd(), root), {
recursive: true,
withFileTypes: true,
})
.filter((entry) => entry.isFile() && entry.name.endsWith('.tsx'))
.map((entry) => resolve(entry.parentPath, entry.name))
for (const file of files) {
const source = readFileSync(file, 'utf8')
if (!chipCellUsage.test(source)) continue
if (source.includes('CampaignListSheetProvider')) continue
offenders.push(relative(process.cwd(), file))
}
}
expect(
offenders,
'chip-relation cells open sheet overlays on coarse pointers — wrap the table in CampaignListSheetProvider (one shared Drawer per list)',
).toEqual([])
})
})