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) } }) })