// @vitest-environment jsdom
/**
* What the page claims: the title toggles by click and by key and says its
* state; the rows are hidden, not removed, while closed; a `storageKey` is
* read on mount and written on change. The slide is Chrome's to show
* (measured 2026-09-09).
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { CollapsibleSection } from './CollapsibleSection'
import { MenuItem } from './Menu'
afterEach(cleanup)
beforeEach(() => window.localStorage.clear())
const rows = (
<>
Item one
Item two
>
)
const title = () => screen.getByRole('button', { name: 'Section' })
describe('CollapsibleSection', () => {
it('is open by default: the title says so, and the rows are there', () => {
render({rows})
expect(title().getAttribute('aria-expanded')).toBe('true')
expect(screen.getByText('Item one').closest('[hidden]')).toBeNull()
})
it('a click on the title closes it, and the rows are hidden rather than gone', async () => {
render({rows})
await userEvent.click(title())
expect(title().getAttribute('aria-expanded')).toBe('false')
expect(screen.getByText('Item one').closest('[hidden]')).not.toBeNull()
})
it('Enter and Space on the title toggle it', async () => {
render({rows})
title().focus()
await userEvent.keyboard('{Enter}')
expect(title().getAttribute('aria-expanded')).toBe('false')
await userEvent.keyboard(' ')
expect(title().getAttribute('aria-expanded')).toBe('true')
})
it('starts closed with defaultOpen false', () => {
render({rows})
expect(title().getAttribute('aria-expanded')).toBe('false')
})
it('an action beside the title acts without toggling', async () => {
const add = vi.fn()
render(
, tooltip: 'Add', onClick: add }]}>
{rows}
,
)
await userEvent.click(screen.getByRole('button', { name: 'Add' }))
expect(add).toHaveBeenCalledTimes(1)
expect(title().getAttribute('aria-expanded')).toBe('true')
})
it('remembers under storageKey: written on change, read on mount', async () => {
const { unmount } = render({rows})
await userEvent.click(title())
expect(window.localStorage.getItem('test.section')).toBe('closed')
unmount()
render({rows})
expect(title().getAttribute('aria-expanded')).toBe('false')
})
it('the caller can own the state', async () => {
const onOpenChange = vi.fn()
render({rows})
await userEvent.click(title())
expect(onOpenChange).toHaveBeenCalledWith(true)
expect(title().getAttribute('aria-expanded')).toBe('false')
})
})
/** Opened before its rows are in (0.65.1, OPT-11): the title says open, the panel waits. */
describe('CollapsibleSection, pending', () => {
it('the title says open and busy while the rows stay shut, then they open', () => {
const { rerender } = render({rows})
expect(title().getAttribute('aria-expanded')).toBe('true')
expect(title().getAttribute('aria-busy')).toBe('true')
expect(screen.getByText('Item one').closest('[hidden]')).not.toBeNull()
rerender({rows})
expect(title().getAttribute('aria-expanded')).toBe('true')
expect(title().getAttribute('aria-busy')).toBeNull()
expect(screen.getByText('Item one').closest('[hidden]')).toBeNull()
})
it('a click while pending closes it', async () => {
const onOpenChange = vi.fn()
render({rows})
await userEvent.click(title())
expect(onOpenChange).toHaveBeenCalledWith(false)
})
it('a closed section is not busy, and a click opens it', async () => {
const onOpenChange = vi.fn()
render({rows})
expect(title().getAttribute('aria-expanded')).toBe('false')
expect(title().getAttribute('aria-busy')).toBeNull()
await userEvent.click(title())
expect(onOpenChange).toHaveBeenCalledWith(true)
})
})
/** What it hands its header (0.56.0): the folder's ⋮, the ⋮'s name, and the icon before the title. */
describe('CollapsibleSection, its header', () => {
it('passes menu and menuLabel on: the ⋮ is named by menuLabel and opens the menu', async () => {
render(} menuLabel="Section options">{rows})
await userEvent.click(screen.getByRole('button', { name: 'Section options' }))
expect(await screen.findByRole('menuitem', { name: 'Rename' })).not.toBeNull()
// The ⋮ acts; it does not fold the section.
expect(title().getAttribute('aria-expanded')).toBe('true')
})
it('with menu and no menuLabel, the ⋮ is named for the title', () => {
render(}>{rows})
expect(screen.getByRole('button', { name: 'More options for Section' })).not.toBeNull()
})
it('passes icon on: it is drawn inside the title’s toggle, before the words', () => {
render(}>{rows})
const icon = screen.getByTestId('icon')
expect(title().contains(icon)).toBe(true)
expect(icon.compareDocumentPosition(screen.getByText('Section')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
})
})