import { useEffect, useRef } from 'react'
import { describe, it, expect } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import { IconButton } from '@mui/material'
import StarIcon from '@mui/icons-material/Star'
import { Toolbox } from './toolbox'
function makeChildren(n: number) {
return Array.from({ length: n }).map((_, i) => (
))
}
/**
* Visible iff the item's Toolbox wrapper has `data-toolbox-item-visible`
* set to `'true'`. Items always mount (single-mount via portal) —
* visibility is the only thing that flips when the popper opens/closes.
*/
function isItemVisible(label: string): boolean {
const el = screen.getByLabelText(label)
const wrapper = el.closest('[data-toolbox-item-visible]')
if (!wrapper) throw new Error(`No toolbox wrapper for "${label}"`)
return wrapper.getAttribute('data-toolbox-item-visible') === 'true'
}
describe('', () => {
it('renders nothing when there are no children', () => {
const { container } = render({null})
expect(container.firstChild).toBeNull()
})
it('renders every child inline when visibleCount is omitted', () => {
render({makeChildren(4)})
for (let i = 1; i <= 4; i++) {
expect(screen.getByLabelText(`Tool ${i}`)).toBeTruthy()
}
})
it('renders every child inline when count <= visibleCount', () => {
render({makeChildren(3)})
expect(screen.queryByLabelText('More actions')).toBeNull()
expect(screen.getByLabelText('Tool 1')).toBeTruthy()
expect(screen.getByLabelText('Tool 3')).toBeTruthy()
})
it('closed with overflow → first N visible inline, rest in DOM but hidden', () => {
render({makeChildren(5)})
// Every item is mounted exactly once.
for (let i = 1; i <= 5; i++) {
expect(screen.getAllByLabelText(`Tool ${i}`).length).toBe(1)
}
// Visibility: first 2 visible, rest display:none.
expect(isItemVisible('Tool 1')).toBe(true)
expect(isItemVisible('Tool 2')).toBe(true)
expect(isItemVisible('Tool 3')).toBe(false)
expect(isItemVisible('Tool 4')).toBe(false)
expect(isItemVisible('Tool 5')).toBe(false)
})
it('open with overflow → all items visible (popover) without remounting any of them', () => {
let mountCount = 0
function Counted() {
const counted = useRef(false)
useEffect(() => {
if (counted.current) return
counted.current = true
mountCount += 1
}, [])
return (
)
}
render(
,
)
expect(mountCount).toBe(1)
expect(isItemVisible('Counted')).toBe(false)
fireEvent.click(screen.getByLabelText('More actions'))
// Still mounted exactly once — no remount on open.
expect(mountCount).toBe(1)
expect(isItemVisible('Counted')).toBe(true)
fireEvent.click(screen.getByLabelText('Close'))
// And still no remount on close.
expect(mountCount).toBe(1)
expect(isItemVisible('Counted')).toBe(false)
})
it('swaps the trigger label to "Close" when the Paper is open', () => {
render({makeChildren(4)})
const trigger = screen.getByLabelText('More actions')
expect(trigger.getAttribute('aria-pressed')).toBe('false')
fireEvent.click(trigger)
// Same button now exposes the close label.
expect(screen.getByLabelText('Close')).toBe(trigger)
expect(trigger.getAttribute('aria-pressed')).toBe('true')
})
it('honors a custom trigger label', () => {
render(
{makeChildren(3)}
,
)
expect(screen.getByLabelText('Extra')).toBeTruthy()
})
it('keeps a hidden marker between visible items in the inline preview', () => {
render(
,
)
// Every item is mounted exactly once. Inline visibility: A, divider, B
// (divider sits between visible items); Tool C is hidden inline.
expect(screen.getAllByLabelText('Tool A').length).toBe(1)
expect(screen.getAllByLabelText('Divider').length).toBe(1)
expect(screen.getAllByLabelText('Tool B').length).toBe(1)
expect(screen.getAllByLabelText('Tool C').length).toBe(1)
expect(isItemVisible('Tool A')).toBe(true)
expect(isItemVisible('Divider')).toBe(true)
expect(isItemVisible('Tool B')).toBe(true)
expect(isItemVisible('Tool C')).toBe(false)
})
it('drops trailing hidden items past the visible budget — no orphan dividers inline', () => {
render(
,
)
// Inline visibility: A + B; the trailing divider is dropped from the
// inline-visible set so it doesn't appear as an orphan separator.
expect(isItemVisible('Tool A')).toBe(true)
expect(isItemVisible('Tool B')).toBe(true)
const divider = screen.getByTestId('trailing-divider')
const wrapper = divider.closest('[data-toolbox-item-visible]')
expect(wrapper?.getAttribute('data-toolbox-item-visible')).toBe('false')
expect(isItemVisible('Tool C')).toBe(false)
})
it('marks the trigger active (className) while the Paper is open so the wrapper hover-fade keeps it visible', () => {
render({makeChildren(4)})
const trigger = screen.getByLabelText('More actions')
expect(trigger.className).not.toMatch(/\bactive\b/)
fireEvent.click(trigger)
expect(trigger.className).toMatch(/\bactive\b/)
})
it('auto-closes and falls back to inline when visibleCount flips to undefined while the popper is open', () => {
function Wrap({ count }: { count: number | undefined }) {
return {makeChildren(5)}
}
const { rerender } = render()
fireEvent.click(screen.getByLabelText('More actions'))
// Popper open, all items visible inside it.
for (let i = 1; i <= 5; i++) {
expect(isItemVisible(`Tool ${i}`)).toBe(true)
}
// Caller drops `visibleCount` (e.g. fullscreen opens). Overflow vanishes
// and so does the popper — items should keep showing inline, *not* be
// stranded in the detached paper node.
rerender()
expect(screen.queryByLabelText('More actions')).toBeNull()
expect(screen.queryByLabelText('Close')).toBeNull()
for (let i = 1; i <= 5; i++) {
expect(isItemVisible(`Tool ${i}`)).toBe(true)
}
})
})