mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
The four *SortableHead islands already fall back to their sort-label record, so B17's new `label` made the header text a third copy instead of removing the second one — 16 redundant `children` deleted, leaving only `frescor`, which diverges on purpose and now reads the record too. The 3 s "safety net" timer was committing like a close, refresh included, which recreated the per-toggle route render the 400 ms debounce was rejected for: at a reading pace every checkbox landed outside the window. The timer now writes the cookie only; closing repaints. A session that ends where it started refreshes nothing, and a menu open when the route unmounts saves from the cleanup, since Radix reports no close event for a sidebar navigation. Both pinned red first. Also: `columnVisibility` required rather than optional for the tests' benefit, the redundant per-list cap dropped, `resolveVisibleColumns` no longer copies, and B29's unused `LeadershipListAccessFilter` export un-exported — knip was failing on main too.
186 lines
5.9 KiB
TypeScript
186 lines
5.9 KiB
TypeScript
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { createElement } from 'react'
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { CampaignColumnPicker } from '@/components/campaign/shared/CampaignColumnPicker'
|
|
|
|
const refresh = vi.fn()
|
|
|
|
// Spread the real module, like the sibling specs: replacing it wholesale would
|
|
// break the day the picker renders anything reading `usePathname`.
|
|
vi.mock('next/navigation', async (importActual) => ({
|
|
...(await importActual()),
|
|
useRouter: () => ({ refresh }),
|
|
}))
|
|
|
|
/**
|
|
* jsdom scopes `document.cookie` by the document URL, and the real cookie is
|
|
* written with `path=/campanha` — so a jar that ignores attributes is what
|
|
* lets these tests read back what the picker wrote.
|
|
*/
|
|
let cookieJar = ''
|
|
|
|
const COLUMNS = [
|
|
{ id: 'name', label: 'Município', mandatory: true },
|
|
{ id: 'kind', label: 'Tipo' },
|
|
{ id: 'trend', label: 'Tendência' },
|
|
]
|
|
|
|
const picker = (hiddenColumnIds: string[]) =>
|
|
createElement(CampaignColumnPicker, {
|
|
listId: 'municipios' as const,
|
|
columns: COLUMNS,
|
|
hiddenColumnIds,
|
|
})
|
|
|
|
const menuTrigger = () => screen.getByRole('button', { name: /Mostrar ou ocultar colunas/ })
|
|
|
|
const openMenu = () => fireEvent.click(menuTrigger())
|
|
|
|
/** Closing is the commit path — the idle timer is only a safety net. */
|
|
const closeMenu = () => fireEvent.click(menuTrigger())
|
|
|
|
beforeAll(() => {
|
|
Object.defineProperty(document, 'cookie', {
|
|
configurable: true,
|
|
get: () => cookieJar,
|
|
set: (value: string) => {
|
|
const [pair = ''] = value.split('; ')
|
|
const separator = pair.indexOf('=')
|
|
const name = pair.slice(0, separator)
|
|
const others = cookieJar.split('; ').filter((entry) => entry && !entry.startsWith(`${name}=`))
|
|
cookieJar = [...others, pair].join('; ')
|
|
},
|
|
})
|
|
})
|
|
|
|
beforeEach(() => {
|
|
cookieJar = ''
|
|
refresh.mockClear()
|
|
})
|
|
|
|
afterEach(cleanup)
|
|
|
|
afterAll(() => {
|
|
Reflect.deleteProperty(document, 'cookie')
|
|
})
|
|
|
|
describe('CampaignColumnPicker', () => {
|
|
it('batches a whole editing session into one cookie write and one refresh', async () => {
|
|
vi.useFakeTimers({ shouldAdvanceTime: true })
|
|
|
|
try {
|
|
render(picker([]))
|
|
openMenu()
|
|
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
|
|
// Reading the next label takes longer than any debounce worth having:
|
|
// a second between checkboxes must still be one refresh, not two.
|
|
await vi.advanceTimersByTimeAsync(1_000)
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tendência' }))
|
|
await vi.advanceTimersByTimeAsync(1_000)
|
|
|
|
expect(refresh).not.toHaveBeenCalled()
|
|
closeMenu()
|
|
|
|
expect(refresh).toHaveBeenCalledTimes(1)
|
|
expect(cookieJar).toContain('campaign_columns=municipios:kind~trend')
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
|
|
it('saves without repainting when the menu is left open', async () => {
|
|
vi.useFakeTimers({ shouldAdvanceTime: true })
|
|
|
|
try {
|
|
render(picker([]))
|
|
openMenu()
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
|
|
|
|
await vi.advanceTimersByTimeAsync(3_000)
|
|
|
|
// The idle timer is durability, not the commit: a slow reader who takes
|
|
// more than the interval between checkboxes must not pay a route render
|
|
// per column, which is the failure the 400 ms debounce was rejected for.
|
|
expect(cookieJar).toContain('campaign_columns=municipios:kind')
|
|
expect(refresh).not.toHaveBeenCalled()
|
|
|
|
closeMenu()
|
|
expect(refresh).toHaveBeenCalledTimes(1)
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
|
|
it('does not refresh a session that ends where it started', () => {
|
|
render(picker(['kind']))
|
|
openMenu()
|
|
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
|
|
closeMenu()
|
|
|
|
expect(refresh).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('saves a pending edit when the list unmounts with the menu open', () => {
|
|
const { unmount } = render(picker([]))
|
|
openMenu()
|
|
|
|
// Radix reports no close event here, so without the unmount flush this
|
|
// choice would be lost to a sidebar navigation with no feedback at all.
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
|
|
unmount()
|
|
|
|
expect(cookieJar).toContain('campaign_columns=municipios:kind')
|
|
})
|
|
|
|
it('never lets the mandatory column be unchecked', () => {
|
|
render(picker([]))
|
|
openMenu()
|
|
|
|
const mandatory = screen.getByRole('checkbox', { name: /^Município/ })
|
|
|
|
expect(mandatory.getAttribute('aria-checked')).toBe('true')
|
|
expect(mandatory).toHaveProperty('disabled', true)
|
|
})
|
|
|
|
it('keeps an edit made while the previous one is still in flight', async () => {
|
|
const { rerender } = render(picker([]))
|
|
openMenu()
|
|
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tipo' }))
|
|
closeMenu()
|
|
await waitFor(() => expect(refresh).toHaveBeenCalledTimes(1))
|
|
|
|
// The second toggle opens a new window …
|
|
openMenu()
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Tendência' }))
|
|
// … and the RSC payload of the FIRST one lands inside it. Adopting it here
|
|
// would uncheck nothing and re-commit `kind` alone, dropping `trend`.
|
|
rerender(picker(['kind']))
|
|
|
|
expect(screen.getByRole('checkbox', { name: 'Tendência' }).getAttribute('aria-checked')).toBe(
|
|
'false',
|
|
)
|
|
|
|
closeMenu()
|
|
await waitFor(() => expect(refresh).toHaveBeenCalledTimes(2))
|
|
expect(cookieJar).toContain('campaign_columns=municipios:kind~trend')
|
|
})
|
|
|
|
it('adopts the server payload once nothing is pending', async () => {
|
|
const { rerender } = render(picker([]))
|
|
|
|
rerender(picker(['kind']))
|
|
openMenu()
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByRole('checkbox', { name: 'Tipo' }).getAttribute('aria-checked')).toBe(
|
|
'false',
|
|
),
|
|
)
|
|
expect(refresh).not.toHaveBeenCalled()
|
|
})
|
|
})
|