import React from 'react' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, it, expect, beforeEach, vi } from 'vitest' import LibrarySelector from './LibrarySelector' // Mock dependencies const mockDispatch = vi.fn() const mockDataProvider = { getOne: vi.fn(), } const mockIdentity = { username: 'testuser' } const mockRefresh = vi.fn() const mockTranslate = vi.fn((key, options = {}) => { const translations = { 'menu.librarySelector.allLibraries': `All Libraries (${options.count || 0})`, 'menu.librarySelector.multipleLibraries': `${options.selected || 0} of ${options.total || 0} Libraries`, 'menu.librarySelector.none': 'None', 'menu.librarySelector.selectLibraries': 'Select Libraries', } return translations[key] || key }) vi.mock('react-redux', () => ({ useDispatch: () => mockDispatch, useSelector: vi.fn(), })) vi.mock('react-admin', () => ({ useDataProvider: () => mockDataProvider, useGetIdentity: () => ({ identity: mockIdentity }), useTranslate: () => mockTranslate, useRefresh: () => mockRefresh, })) // Mock Material-UI components vi.mock('@material-ui/core', () => ({ Box: ({ children, className, ...props }) => (
{children}
), Chip: ({ label, onClick, onDelete, deleteIcon, icon, ...props }) => ( ), ClickAwayListener: ({ children, onClickAway }) => (
{children}
), Collapse: ({ children, in: inProp }) => inProp ?
{children}
: null, FormControl: ({ children }) =>
{children}
, FormGroup: ({ children }) =>
{children}
, FormControlLabel: ({ control, label }) => ( ), Checkbox: ({ checked, indeterminate, onChange, size, className, ...props }) => ( { if (el) el.indeterminate = indeterminate }} onChange={onChange} className={className} {...props} /> ), Typography: ({ children, variant, ...props }) => ( {children} ), Paper: ({ children, className }) => (
{children}
), Popper: ({ open, children, anchorEl, placement, className }) => open ? (
{children}
) : null, makeStyles: (styles) => () => { if (typeof styles === 'function') { return styles({ spacing: (value) => `${value * 8}px`, palette: { divider: '#ccc' }, shape: { borderRadius: 4 }, }) } return styles }, })) vi.mock('@material-ui/icons', () => ({ ExpandMore: () => , ExpandLess: () => , LibraryMusic: () => 🎵, })) // Mock actions vi.mock('../actions', () => ({ setSelectedLibraries: (libraries) => ({ type: 'SET_SELECTED_LIBRARIES', data: libraries, }), setUserLibraries: (libraries) => ({ type: 'SET_USER_LIBRARIES', data: libraries, }), })) describe('LibrarySelector', () => { const mockLibraries = [ { id: '1', name: 'Music Library', path: '/music' }, { id: '2', name: 'Podcasts', path: '/podcasts' }, { id: '3', name: 'Audiobooks', path: '/audiobooks' }, ] const defaultState = { userLibraries: mockLibraries, selectedLibraries: ['1'], } let mockUseSelector beforeEach(async () => { vi.clearAllMocks() const { useSelector } = await import('react-redux') mockUseSelector = vi.mocked(useSelector) mockDataProvider.getOne.mockResolvedValue({ data: { libraries: mockLibraries }, }) // Setup localStorage mock Object.defineProperty(window, 'localStorage', { value: { getItem: vi.fn(() => null), // Default to null to prevent API calls setItem: vi.fn(), }, writable: true, }) }) const renderLibrarySelector = (selectorState = defaultState) => { mockUseSelector.mockImplementation((selector) => selector({ library: selectorState }), ) return render() } describe('when user has no libraries', () => { it('should not render anything', () => { const { container } = renderLibrarySelector({ userLibraries: [], selectedLibraries: [], }) expect(container.firstChild).toBeNull() }) }) describe('when user has only one library', () => { it('should not render anything', () => { const singleLibrary = [mockLibraries[0]] const { container } = renderLibrarySelector({ userLibraries: singleLibrary, selectedLibraries: ['1'], }) expect(container.firstChild).toBeNull() }) }) describe('when user has multiple libraries', () => { it('should render the chip with correct label when one library is selected', () => { renderLibrarySelector() expect(screen.getByRole('button')).toBeInTheDocument() expect(screen.getByText('1 of 3 Libraries')).toBeInTheDocument() expect(screen.getByTestId('library-music')).toBeInTheDocument() expect(screen.getByTestId('expand-more')).toBeInTheDocument() }) it('should render the chip with "All Libraries" when all libraries are selected', () => { renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: ['1', '2', '3'], }) expect(screen.getByText('All Libraries (3)')).toBeInTheDocument() }) it('should render the chip with "None" when no libraries are selected', () => { renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: [], }) expect(screen.getByText('None (0 of 3)')).toBeInTheDocument() }) it('should show expand less icon when dropdown is open', async () => { const user = userEvent.setup() renderLibrarySelector() const chipButton = screen.getByRole('button') await user.click(chipButton) expect(screen.getByTestId('expand-less')).toBeInTheDocument() }) it('should open dropdown when chip is clicked', async () => { const user = userEvent.setup() renderLibrarySelector() const chipButton = screen.getByRole('button') await user.click(chipButton) expect(screen.getByTestId('popper')).toBeInTheDocument() expect(screen.getByText('Select Libraries:')).toBeInTheDocument() }) it('should display all library names in dropdown', async () => { const user = userEvent.setup() renderLibrarySelector() const chipButton = screen.getByRole('button') await user.click(chipButton) expect(screen.getByText('Music Library')).toBeInTheDocument() expect(screen.getByText('Podcasts')).toBeInTheDocument() expect(screen.getByText('Audiobooks')).toBeInTheDocument() }) it('should not display library paths', async () => { const user = userEvent.setup() renderLibrarySelector() const chipButton = screen.getByRole('button') await user.click(chipButton) expect(screen.queryByText('/music')).not.toBeInTheDocument() expect(screen.queryByText('/podcasts')).not.toBeInTheDocument() expect(screen.queryByText('/audiobooks')).not.toBeInTheDocument() }) describe('master checkbox', () => { it('should be checked when all libraries are selected', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: ['1', '2', '3'], }) const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const masterCheckbox = checkboxes[0] // First checkbox is the master checkbox expect(masterCheckbox.checked).toBe(true) expect(masterCheckbox.indeterminate).toBe(false) }) it('should be unchecked when no libraries are selected', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: [], }) const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const masterCheckbox = checkboxes[0] expect(masterCheckbox.checked).toBe(false) expect(masterCheckbox.indeterminate).toBe(false) }) it('should be indeterminate when some libraries are selected', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: ['1', '2'], }) const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const masterCheckbox = checkboxes[0] expect(masterCheckbox.checked).toBe(false) expect(masterCheckbox.indeterminate).toBe(true) }) it('should select all libraries when clicked and none are selected', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: [], }) // Clear the dispatch mock after initial mount (it sets user libraries) mockDispatch.mockClear() const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const masterCheckbox = checkboxes[0] // Use fireEvent.click to trigger the onChange event fireEvent.click(masterCheckbox) expect(mockDispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_LIBRARIES', data: ['1', '2', '3'], }) }) it('should deselect all libraries when clicked and all are selected', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: ['1', '2', '3'], }) // Clear the dispatch mock after initial mount (it sets user libraries) mockDispatch.mockClear() const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const masterCheckbox = checkboxes[0] fireEvent.click(masterCheckbox) expect(mockDispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_LIBRARIES', data: [], }) }) it('should select all libraries when clicked and some are selected', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: ['1'], }) // Clear the dispatch mock after initial mount (it sets user libraries) mockDispatch.mockClear() const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const masterCheckbox = checkboxes[0] fireEvent.click(masterCheckbox) expect(mockDispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_LIBRARIES', data: ['1', '2', '3'], }) }) }) describe('individual library checkboxes', () => { it('should show correct checked state for each library', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: ['1', '3'], }) const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') // Skip master checkbox (index 0) expect(checkboxes[1].checked).toBe(true) // Music Library expect(checkboxes[2].checked).toBe(false) // Podcasts expect(checkboxes[3].checked).toBe(true) // Audiobooks }) it('should toggle library selection when individual checkbox is clicked', async () => { const user = userEvent.setup() renderLibrarySelector() // Clear the dispatch mock after initial mount (it sets user libraries) mockDispatch.mockClear() const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const podcastsCheckbox = checkboxes[2] // Podcasts checkbox fireEvent.click(podcastsCheckbox) expect(mockDispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_LIBRARIES', data: ['1', '2'], }) }) it('should remove library from selection when clicking checked library', async () => { const user = userEvent.setup() renderLibrarySelector({ userLibraries: mockLibraries, selectedLibraries: ['1', '2'], }) // Clear the dispatch mock after initial mount (it sets user libraries) mockDispatch.mockClear() const chipButton = screen.getByRole('button') await user.click(chipButton) const checkboxes = screen.getAllByRole('checkbox') const musicCheckbox = checkboxes[1] // Music Library checkbox fireEvent.click(musicCheckbox) expect(mockDispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_LIBRARIES', data: ['2'], }) }) }) it('should close dropdown when clicking away', async () => { const user = userEvent.setup() renderLibrarySelector() // Open dropdown const chipButton = screen.getByRole('button') await user.click(chipButton) expect(screen.getByTestId('popper')).toBeInTheDocument() // Click away const clickAwayListener = screen.getByTestId('click-away-listener') fireEvent.mouseDown(clickAwayListener) await waitFor(() => { expect(screen.queryByTestId('popper')).not.toBeInTheDocument() }) // Should trigger refresh when closing expect(mockRefresh).toHaveBeenCalledTimes(1) }) it('should load user libraries on mount', async () => { // Override localStorage mock to return a userId for this test window.localStorage.getItem.mockReturnValue('user123') mockDataProvider.getOne.mockResolvedValue({ data: { libraries: mockLibraries }, }) renderLibrarySelector({ userLibraries: [], selectedLibraries: [] }) await waitFor(() => { expect(mockDataProvider.getOne).toHaveBeenCalledWith('user', { id: 'user123', }) }) expect(mockDispatch).toHaveBeenCalledWith({ type: 'SET_USER_LIBRARIES', data: mockLibraries, }) }) it('should handle API error gracefully', async () => { // Override localStorage mock to return a userId for this test window.localStorage.getItem.mockReturnValue('user123') const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockDataProvider.getOne.mockRejectedValue(new Error('API Error')) renderLibrarySelector({ userLibraries: [], selectedLibraries: [] }) await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith( 'Could not load user libraries (this may be expected for non-admin users):', expect.any(Error), ) }) consoleSpy.mockRestore() }) it('should not load libraries when userId is not available', () => { window.localStorage.getItem.mockReturnValue(null) renderLibrarySelector({ userLibraries: [], selectedLibraries: [] }) expect(mockDataProvider.getOne).not.toHaveBeenCalled() }) }) })