OPS13 — remove className/Tailwind literal pins from unit specs

Replace layout/spacing utility string assertions with stable contract
pins (data-slot, data-retracted, exported constant identity, ARIA).
Delete cosmetic-only padding/bleed/grid pins per plan remover-pins-classname.

Closes #240

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-02 16:47:27 +00:00
parent f66381e09f
commit 0c605f4375
No known key found for this signature in database
9 changed files with 25 additions and 96 deletions

View file

@ -86,17 +86,14 @@ describe('campaign visual foundation', () => {
expect(html).toContain('aria-valuenow="42"')
})
it.each(['sm', 'default', 'lg'] as const)(
'keeps the %s toggle at least 44 by 44 pixels',
(size) => {
const html = renderToStaticMarkup(createElement(Toggle, { size }, 'Filtro'))
it.each(['sm', 'default', 'lg'] as const)('renders the %s toggle with a button role', (size) => {
const html = renderToStaticMarkup(createElement(Toggle, { size }, 'Filtro'))
expect(html).toContain('min-h-11')
expect(html).toContain('min-w-11')
},
)
expect(html).toContain('type="button"')
expect(html).toContain('Filtro')
})
it('keeps toggle group items at least 44 by 44 pixels', () => {
it('renders toggle group items with the shared slot contract', () => {
const html = renderToStaticMarkup(
createElement(
ToggleGroup,
@ -106,8 +103,7 @@ describe('campaign visual foundation', () => {
)
expect(html).toContain('data-slot="toggle-group-item"')
expect(html).toContain('min-h-11')
expect(html).toContain('min-w-11')
expect(html).toContain('Engajado')
})
it('renders campaign scope with a visible, accessible label', () => {
@ -133,28 +129,18 @@ describe('campaign visual foundation', () => {
'utf8',
)
expect(layoutSource).toContain(
'className="h-svh min-h-0 overflow-hidden print:h-auto print:overflow-visible"',
)
expect(layoutSource).toContain('CampaignAppScrollChrome')
expect(scrollChromeSource).toContain('data-slot="campaign-content-scroll"')
expect(scrollChromeSource).toContain('min-h-0 flex-1 overflow-y-auto overscroll-contain')
expect(layoutSource).toContain(
'hidden min-h-11 shrink-0 items-center gap-2 border-b border-border px-4 md:flex print:hidden',
)
expect(sidebarSource).toContain('collapsible="offcanvas"')
expect(sidebarSource).toContain('print:hidden')
})
it('uses light sidebar tokens and readable logout states', () => {
const styles = readFileSync(resolve(process.cwd(), 'src/app/(frontend)/styles.css'), 'utf8')
it('uses sidebar semantic tokens and readable logout states', () => {
const sidebarSource = readFileSync(
resolve(process.cwd(), 'src/components/campaign/shell/CampaignSidebar.tsx'),
'utf8',
)
expect(styles).toContain('--sidebar: #fafaf9')
expect(styles).toContain('--sidebar-foreground: #1c1917')
expect(styles).toContain('--sidebar-accent-foreground: #1c1917')
expect(sidebarSource).toContain('text-sidebar-foreground')
expect(sidebarSource).toContain('hover:text-sidebar-accent-foreground')
})
@ -285,10 +271,10 @@ describe('campaign visual foundation', () => {
const theadHtml = html.slice(html.indexOf('<thead'), html.indexOf('</thead>'))
expect(theadHtml.match(/data-slot="tooltip-trigger"/g)).toHaveLength(10)
// B41: horizontal scroll + sticky Município (Territory pattern).
expect(html).toMatch(/data-slot="table-container"[^>]*class="[^"]*overflow-x-auto/)
expect(theadHtml).toMatch(/sticky left-0 z-20[^"]*min-w-56[^"]*bg-background/)
expect(tbodyHtml).toMatch(/sticky left-0 z-\[5\][^"]*min-w-56[^"]*bg-background/)
// B41: horizontal scroll table with a pinned Município column header.
expect(html).toContain('data-slot="table-container"')
expect(theadHtml).toContain('Município')
expect(tbodyHtml).toContain('Seabra')
})
/**

View file

@ -154,7 +154,6 @@ describe('campaign login composition', () => {
),
).toBeTruthy()
expect(submit.getAttribute('type')).toBe('submit')
expect(submit.className).toContain('w-full')
})
it('switches identifier inputMode toward phone or email as the user types', () => {

View file

@ -91,13 +91,11 @@ describe('CampaignHomeActionButton', () => {
expect(screen.getByLabelText('Em breve').getAttribute('aria-disabled')).toBe('true')
})
it('uses a wider control column for two-line labels (B109)', () => {
renderActionButton({ label: 'Registrar', icon: BarChart3 })
it('applies the shared action control class contract (B109)', () => {
const onClick = vi.fn()
renderActionButton({ label: 'Registrar', icon: BarChart3, onClick })
const control = screen.getByRole('button', { name: 'Registrar' })
expect(control.className).toContain('box-content')
expect(control.className).toContain('w-[5.5rem]')
expect(actionControlClassName).toContain('box-content')
expect(actionControlClassName).toContain('w-[5.5rem]')
expect(control.className).toBe(actionControlClassName)
})
it('opens the description drawer on long-press when the pointer is coarse', () => {
@ -188,14 +186,9 @@ describe('CampaignHomeActionStrip', () => {
)
const scroller = getScroller(container)
expect(scroller?.className).toContain('overflow-x-auto')
expect(scroller?.className).toContain('scrollbar-width:none')
expect(scroller).toBeTruthy()
const list = scroller?.querySelector('ul[role="list"]')
expect(list).toBeTruthy()
expect(list?.className).toContain('gap-0')
expect(list?.className).toContain('px-4')
expect(list?.className).toContain('md:px-0')
expect(list?.className).not.toContain('gap-2')
})
it('scrolls horizontally on pointer-fine drag past the threshold', () => {

View file

@ -7,9 +7,8 @@ describe('CampaignHomeLayout', () => {
it('fills the scrollport height on mobile', () => {
const { container } = render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
const root = container.firstElementChild
expect(root?.className).toContain('h-full')
expect(root?.className).toContain('min-h-0')
expect(container.firstElementChild).not.toBeNull()
expect(container.querySelector('[data-slot="home-dock"]')).not.toBeNull()
})
it('renders actions in the home-actions slot inside home-dock', () => {
@ -24,10 +23,8 @@ describe('CampaignHomeLayout', () => {
const { container } = render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
const actions = container.querySelector('[data-slot="home-actions"]')
expect(actions?.className).toContain('-mx-4')
expect(actions?.className).toContain('w-[calc(100%+2rem)]')
expect(actions?.className).toContain('md:mx-0')
expect(actions?.className).toContain('md:w-auto')
expect(actions).not.toBeNull()
expect(actions?.closest('[data-slot="home-actions-chrome"]')).not.toBeNull()
})
it('does not clip horizontal bleed on the actions retraction shell', () => {
@ -36,8 +33,6 @@ describe('CampaignHomeLayout', () => {
const actionsChrome = container.querySelector('[data-slot="home-actions-chrome"]')
const bleedWrapper = actionsChrome?.querySelector('[data-allow-horizontal-bleed="true"]')
expect(bleedWrapper).not.toBeNull()
expect(bleedWrapper?.className).toContain('overflow-y-hidden')
expect(bleedWrapper?.className).not.toContain('overflow-hidden')
})
it('renders search slot inside home-dock when provided', () => {
@ -68,8 +63,6 @@ describe('CampaignHomeLayout', () => {
expect(chrome).not.toBeNull()
expect(spacer).not.toBeNull()
expect(spacer?.getAttribute('aria-hidden')).toBe('true')
expect(spacer?.className).toContain('flex-1')
expect(spacer?.className).toContain('md:hidden')
expect(chrome?.nextElementSibling?.getAttribute('data-slot')).toBe('home-dock')
})
@ -85,12 +78,9 @@ describe('CampaignHomeLayout', () => {
const homeChrome = container.querySelector('[data-slot="home-chrome"]')
expect(homeChrome?.getAttribute('data-retracted')).toBe('true')
expect(homeChrome?.className).toContain('grid-rows-[0fr]')
expect(homeChrome?.className).toContain('opacity-0')
const actionsChrome = container.querySelector('[data-slot="home-actions-chrome"]')
expect(actionsChrome?.getAttribute('data-retracted')).toBe('true')
expect(actionsChrome?.className).toContain('grid-rows-[0fr]')
expect(container.querySelector('[data-slot="home-thumb-spacer"]')).not.toBeNull()
expect(container.querySelector('[data-slot="home-summary"]')).not.toBeNull()

View file

@ -168,8 +168,7 @@ describe('CampaignQuickActionsDrawer (B105)', () => {
const context = document.getElementById('quickActionContext')
const stripBleed = context?.firstElementChild
expect(stripBleed?.className).toContain('-mx-4')
expect(stripBleed?.className).toContain('w-[calc(100%+2rem)]')
expect(stripBleed).not.toBeNull()
expect(stripBleed?.querySelector('[aria-label="Ações rápidas"]')).not.toBeNull()
})

View file

@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'
import { CampaignSearchInput } from '@/components/campaign/shared/CampaignSearchInput'
describe('CampaignSearchInput', () => {
it('renders the shared accessible 44px campaign search composition', () => {
it('renders the shared accessible campaign search composition', () => {
const html = renderToStaticMarkup(
createElement(CampaignSearchInput, {
id: 'leadership-search',
@ -19,8 +19,6 @@ describe('CampaignSearchInput', () => {
expect(html).toContain('data-slot="input-group"')
expect(html).toContain('data-slot="input-group-addon"')
expect(html).toContain('data-slot="input-group-control"')
expect(html).toContain('min-h-11')
expect(html).toContain('rounded-[6px]')
expect(html).toContain('aria-hidden="true"')
expect(html).not.toContain('absolute')
})

View file

@ -101,22 +101,6 @@ describe('CampaignWizardShell', () => {
expect(main.getAttribute('aria-label')).toBe(wizardFlowChromeAriaLabel('Ajustar votos'))
})
it('uses tighter top padding on mobile when step title is omitted', () => {
const { container } = renderWizardShell({
flowTitle: 'Ajustar votos',
stepTitle: null,
isEntryStep: true,
previousHref: '/campanha',
children: null,
})
const main = container.querySelector('main')
expect(main?.className).toMatch(/pt-2/)
expect(main?.className).not.toMatch(/pt-3/)
expect(main?.className).toMatch(/pb-6/)
expect(main?.className).toMatch(/md:py-6/)
})
it('skips title focus when contentFocus is none', () => {
const { rerender } = render(
<SidebarProvider>
@ -205,21 +189,5 @@ describe('CampaignWizardShell', () => {
const caption = container.querySelector('main p.md\\:block')
expect(caption?.textContent).toBe('Cairu')
expect(caption?.className).toMatch(/md:block/)
})
it('uses tighter top padding on mobile main content', () => {
const { container } = renderWizardShell({
flowTitle: 'Ajustar votos',
stepTitle: WIZARD_MUNICIPALITY_STEP_TITLE,
isEntryStep: true,
previousHref: '/campanha',
children: null,
})
const main = container.querySelector('main')
expect(main?.className).toMatch(/pt-3/)
expect(main?.className).toMatch(/pb-6/)
expect(main?.className).toMatch(/md:py-6/)
})
})

View file

@ -40,9 +40,7 @@ describe('HomeSearchResultsLayout', () => {
)
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(layout).not.toBeNull()
expect(screen.getByText('Group A')).toBeTruthy()
expect(screen.getByText('Group B')).toBeTruthy()
})

View file

@ -50,7 +50,6 @@ describe('HomeSearchHitRow', () => {
const bleed = container.firstElementChild
expect(bleed?.className).toBe(HOME_SEARCH_HIT_ROW_WRAPPER_CLASS)
expect(screen.getByRole('link', { name: /Cairu/i }).className).not.toContain('rounded-md')
})
})
@ -105,7 +104,6 @@ describe('HomeSearchMunicipalityGroup', () => {
const heading = screen.getByRole('heading', { name: 'Municípios' })
expect(heading.className).toBe(HOME_SEARCH_GROUP_HEADING_CLASS)
expect(heading.className).not.toContain('uppercase')
const list = container.querySelector('ul')
expect(list?.className).toBe(HOME_SEARCH_GROUP_LIST_CLASS)