import { describe, test, expect } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { ToolbarActions } from './toolbar-actions'
import { IconButton, Divider } from '@mui/material'
import { Tooltip } from '../../components/tooltip/tooltip'
import {
FileDownloadOutlined,
ShareOutlined,
EditOutlined,
FilterListOutlined,
} from '@mui/icons-material'
describe('ToolbarActions', () => {
const createAction = (
key: string,
label: string,
Icon: React.ComponentType,
) => (
)
describe('data-toolbar-hidden filtering', () => {
test('filters children with data-toolbar-hidden from visible count', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
,
createAction('share', 'Share', ShareOutlined),
,
createAction('edit', 'Edit', EditOutlined),
]
render({children})
// Should show trigger button because there are 3 visible actions (download, share, edit)
// even though total children count is 5
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
expect(triggerButton).toBeTruthy()
// Preview should include first 2 visible actions plus divider between them
// (Download, Divider, Share - stops after counting 2 non-hidden actions)
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
// Edit button should also be present (in the expanded section)
const editButtons = screen.getAllByLabelText('Edit')
expect(editButtons.length).toBeGreaterThanOrEqual(1)
})
test('includes data-toolbar-hidden children in preview between visible actions', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
,
createAction('share', 'Share', ShareOutlined),
,
createAction('edit', 'Edit', EditOutlined),
]
const { container } = render(
{children},
)
// With visibleCount={2}, preview should include:
// Download, Divider-1, Share (counting 2 visible actions, including divider between them)
// Hidden: Divider-2, Edit
// First divider should be visible (in both preview and expanded)
const allDividers = container.querySelectorAll(
'[data-toolbar-hidden="true"]',
)
expect(allDividers.length).toBeGreaterThanOrEqual(1)
})
test('includes hidden children in expanded view', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
,
createAction('share', 'Share', ShareOutlined),
,
createAction('edit', 'Edit', EditOutlined),
]
const { container } = render(
{children},
)
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
fireEvent.click(triggerButton)
// After expanding, all children including dividers should be present
// Check for dividers with data-toolbar-hidden attribute
// Note: dividers appear in both preview and expanded sections
// Preview: Divider-1 (between Download and Share)
// Expanded: Divider-1 and Divider-2 (all dividers)
// Total: 3 dividers with data-toolbar-hidden attribute
const dividers = container.querySelectorAll(
'[data-toolbar-hidden="true"]',
)
expect(dividers.length).toBe(3) // Divider-1 (preview) + Divider-1 + Divider-2 (expanded)
})
test('does not show trigger when only hidden children exceed visibleCount', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
,
,
]
render({children})
// Should not show trigger button because there are only 2 visible actions
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
// Both visible actions should be shown (appear in both preview and hidden expanded sections)
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
})
})
describe('visibleCount behavior', () => {
test('shows all actions when visibleCount is undefined', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
createAction('filter', 'Filter', FilterListOutlined),
]
render(
{children},
)
// Should not show trigger button
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
// All actions should be visible (appear in both preview and hidden expanded sections)
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
const editButtons = screen.getAllByLabelText('Edit')
const filterButtons = screen.getAllByLabelText('Filter')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
expect(editButtons.length).toBeGreaterThanOrEqual(1)
expect(filterButtons.length).toBeGreaterThanOrEqual(1)
})
test('shows trigger when visibleCount is 1 and there are 2+ actions', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
]
render({children})
// Should show trigger button
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
expect(triggerButton).toBeTruthy()
// Download action should be visible (appears in both preview and expanded)
const downloadButtons = screen.getAllByLabelText('Download')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
// Share button exists in the expanded section
const shareButtons = screen.getAllByLabelText('Share')
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
})
test('shows trigger when visibleCount is 2 and there are 3+ actions', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
render({children})
// Should show trigger button
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
expect(triggerButton).toBeTruthy()
// First two actions should be visible (appear in both preview and expanded)
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
})
test('does not show trigger when actions equal visibleCount', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
]
render({children})
// Should not show trigger button
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
// Both actions should be visible (appear in both preview and hidden expanded sections)
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
})
test('does not show trigger when actions are less than visibleCount', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
]
render({children})
// Should not show trigger button
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
// Single action should be visible (appears in both preview and hidden expanded sections)
const downloadButtons = screen.getAllByLabelText('Download')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
})
})
describe('trigger button behavior', () => {
test('trigger button appears when there are more visible actions than visibleCount', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
render({children})
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
expect(triggerButton).toBeTruthy()
})
test('trigger button does not appear when visibleCount is undefined', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
render(
{children},
)
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
})
test('clicking trigger button expands the actions panel', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
render({children})
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
// Before clicking, trigger button should not have data-active
expect(triggerButton.getAttribute('data-active')).toBe('false')
fireEvent.click(triggerButton)
// After clicking, trigger button should have data-active
expect(triggerButton.getAttribute('data-active')).toBe('true')
// Button should now show close label
const closeButton = screen.getByRole('button', { name: /close/i })
expect(closeButton).toBeTruthy()
})
test('clicking trigger button again collapses the actions panel', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
render({children})
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
// Expand
fireEvent.click(triggerButton)
expect(triggerButton.getAttribute('data-active')).toBe('true')
// Collapse
fireEvent.click(triggerButton)
expect(triggerButton.getAttribute('data-active')).toBe('false')
// Button should show trigger label again
const reopenButton = screen.getByRole('button', {
name: /more actions/i,
})
expect(reopenButton).toBeTruthy()
})
test('uses custom labels when provided', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
render(
{children}
,
)
const triggerButton = screen.getByRole('button', {
name: /show options/i,
})
expect(triggerButton).toBeTruthy()
fireEvent.click(triggerButton)
const closeButton = screen.getByRole('button', { name: /hide options/i })
expect(closeButton).toBeTruthy()
})
})
describe('childrenPreview behavior', () => {
test('childrenPreview shows only visible children up to visibleCount', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
createAction('filter', 'Filter', FilterListOutlined),
]
render({children})
// Only first 2 should be in preview
// Note: Download and Share appear in both preview and expanded sections
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
// Edit and Filter should also be in expanded section
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
fireEvent.click(triggerButton)
// Now all should be visible
const editButtons = screen.getAllByLabelText('Edit')
const filterButtons = screen.getAllByLabelText('Filter')
expect(editButtons.length).toBeGreaterThanOrEqual(1)
expect(filterButtons.length).toBeGreaterThanOrEqual(1)
})
test('childrenPreview includes hidden children but does not count them', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
,
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
const { container } = render(
{children},
)
// Preview should show: Download, Divider, Share (2 visible actions counted, divider included)
// Hidden: Edit action
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
// Divider with data-toolbar-hidden should be in preview (appears in both preview and expanded)
const allDividers = container.querySelectorAll(
'[data-toolbar-hidden="true"]',
)
expect(allDividers.length).toBeGreaterThanOrEqual(1)
// Edit button should also be present in expanded section
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
fireEvent.click(triggerButton)
const editButtons = screen.getAllByLabelText('Edit')
expect(editButtons.length).toBeGreaterThanOrEqual(1)
})
test('childrenPreview shows all visible children when visibleCount is undefined', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
createAction('edit', 'Edit', EditOutlined),
]
render(
{children},
)
// All actions should be in preview (no trigger button)
// Actions appear in both preview and hidden expanded sections
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
const editButtons = screen.getAllByLabelText('Edit')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
expect(editButtons.length).toBeGreaterThanOrEqual(1)
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
})
})
describe('edge cases', () => {
test('renders nothing when children array is empty', () => {
const { container } = render({[]})
expect(container.firstChild).toBeNull()
})
test('renders nothing when children is null', () => {
const { container } = render({null})
expect(container.firstChild).toBeNull()
})
test('handles single child (not array)', () => {
const child = createAction('download', 'Download', FileDownloadOutlined)
render({child})
// Should appear in both preview and expanded sections (even though expanded is hidden)
const downloadButtons = screen.getAllByLabelText('Download')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
})
test('handles all children being hidden', () => {
const children = [
,
,
]
const { container } = render(
{children},
)
// Component still renders container even when no visible children
// (it only checks children.length, not visibleChildren.length)
// This test documents the current behavior
expect(container.firstChild).toBeTruthy()
// But there should be no trigger button since visibleChildren.length is 0
const triggerButtons = screen.queryAllByRole('button', {
name: /more actions/i,
})
expect(triggerButtons.length).toBe(0)
})
test('correctly counts when mix of hidden and visible children', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
,
createAction('share', 'Share', ShareOutlined),
,
createAction('edit', 'Edit', EditOutlined),
,
createAction('filter', 'Filter', FilterListOutlined),
]
render({children})
// Should show trigger because there are 4 visible actions > 3
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
expect(triggerButton).toBeTruthy()
// Preview should show first 3 visible actions + dividers between them
// (Download, Divider, Share, Divider, Edit - stops after counting 3 non-hidden)
const downloadButtons = screen.getAllByLabelText('Download')
const shareButtons = screen.getAllByLabelText('Share')
const editButtons = screen.getAllByLabelText('Edit')
expect(downloadButtons.length).toBeGreaterThanOrEqual(1)
expect(shareButtons.length).toBeGreaterThanOrEqual(1)
expect(editButtons.length).toBeGreaterThanOrEqual(1)
})
test('documentation example: visibleCount={2} with [Action1, Divider, Action2, Divider, Action3]', () => {
const children = [
createAction('action1', 'Action 1', FileDownloadOutlined),
,
createAction('action2', 'Action 2', ShareOutlined),
,
createAction('action3', 'Action 3', EditOutlined),
]
const { container } = render(
{children},
)
// Expected preview: Action1, Divider, Action2 (2 actions counted, divider included)
// Hidden: Divider, Action3
// Should show trigger button since we have 3 visible actions > 2
const triggerButton = screen.getByRole('button', {
name: /more actions/i,
})
expect(triggerButton).toBeTruthy()
// Action1 and Action2 should be visible
const action1Buttons = screen.getAllByLabelText('Action 1')
const action2Buttons = screen.getAllByLabelText('Action 2')
expect(action1Buttons.length).toBeGreaterThanOrEqual(1)
expect(action2Buttons.length).toBeGreaterThanOrEqual(1)
// At least one divider with data-toolbar-hidden should be rendered
const dividers = container.querySelectorAll(
'[data-toolbar-hidden="true"]',
)
expect(dividers.length).toBeGreaterThanOrEqual(1)
// Action3 should be in expanded section
const action3Buttons = screen.getAllByLabelText('Action 3')
expect(action3Buttons.length).toBeGreaterThanOrEqual(1)
})
})
describe('direction prop', () => {
test('applies correct flex direction for left expansion', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
]
const { container } = render(
{children},
)
const root = container.firstChild as HTMLElement
expect(root).toBeTruthy()
// Check that the style contains row-reverse
const style = window.getComputedStyle(root)
expect(style.flexDirection).toBe('row-reverse')
})
test('applies correct flex direction for right expansion (default)', () => {
const children = [
createAction('download', 'Download', FileDownloadOutlined),
createAction('share', 'Share', ShareOutlined),
]
const { container } = render(
{children},
)
const root = container.firstChild as HTMLElement
expect(root).toBeTruthy()
const style = window.getComputedStyle(root)
expect(style.flexDirection).toBe('row')
})
})
})