From 6a9ccb309c7bf042865d34c7f909e36b9c1c6fca Mon Sep 17 00:00:00 2001 From: Xavier Araque Date: Fri, 7 Nov 2025 16:46:30 +0100 Subject: [PATCH] fix: test --- ui/src/audioplayer/Player.test.jsx | 389 +++++++----------- .../audioplayer/hooks/usePlayerState.test.js | 3 + 2 files changed, 151 insertions(+), 241 deletions(-) diff --git a/ui/src/audioplayer/Player.test.jsx b/ui/src/audioplayer/Player.test.jsx index 55b9bfa4f..401c953cb 100644 --- a/ui/src/audioplayer/Player.test.jsx +++ b/ui/src/audioplayer/Player.test.jsx @@ -1,259 +1,166 @@ -/* eslint-env jest */ - import React from 'react' -import { render, screen, fireEvent, waitFor } from '@testing-library/react' -import { Provider } from 'react-redux' -import { createStore, combineReducers } from 'redux' -import { ThemeProvider } from '@material-ui/core/styles' -import { createMuiTheme } from '@material-ui/core/styles' -import { Player } from './Player' -import { playerReducer } from '../reducers/playerReducer' -import { settingsReducer } from '../reducers/settingsReducer' -import { replayGainReducer } from '../reducers/replayGainReducer' +import { render, screen, fireEvent, cleanup } from '@testing-library/react' +import { useMediaQuery } from '@material-ui/core' +import { useGetOne } from 'react-admin' +import { useDispatch } from 'react-redux' +import { useToggleLove } from '../common' +import { openSaveQueueDialog } from '../actions' +import PlayerToolbar from './PlayerToolbar' // Mock dependencies -jest.mock('../themes/useCurrentTheme', () => ({ - __esModule: true, - default: () => ({ - player: { theme: 'dark' }, - }), -})) - -jest.mock('../config', () => ({ - enableCoverAnimation: false, - gaTrackingId: null, -})) - -jest.mock('./AudioTitle', () => ({ - __esModule: true, - default: ({ audioInfo }) => ( -
{audioInfo?.song?.title || 'No song'}
- ), -})) - -jest.mock('./PlayerToolbar', () => ({ - __esModule: true, - default: ({ id }) =>
{id || 'No ID'}
, -})) - -jest.mock('./locale', () => ({ - __esModule: true, - default: () => (key) => key, -})) - -jest.mock('./keyHandlers', () => ({ - __esModule: true, - default: () => ({}), -})) - -jest.mock('../hotkeys', () => ({ - keyMap: {}, -})) - -jest.mock('react-ga', () => ({ - event: jest.fn(), -})) - -jest.mock('../utils', () => ({ - sendNotification: jest.fn(), -})) - -jest.mock('navidrome-music-player', () => ({ - __esModule: true, - default: ({ children, ...props }) => ( -
- {children} -
- ), -})) - -jest.mock('navidrome-music-player/assets/index.css', () => {}) - -// Mock react-redux hooks -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn(), - useDispatch: jest.fn(), -})) - -// Mock react-admin hooks -jest.mock('react-admin', () => ({ - useAuthState: () => ({ authenticated: true }), - useDataProvider: () => ({ - getOne: jest.fn().mockResolvedValue({ data: {} }), - }), - useTranslate: () => (key) => key, - createMuiTheme: jest.fn(), -})) - -// Mock @material-ui/core -jest.mock('@material-ui/core', () => ({ - ...jest.requireActual('@material-ui/core'), - useMediaQuery: () => true, // Mock as desktop - ThemeProvider: ({ children }) =>
{children}
, -})) - -describe('Player Component', () => { - const mockStore = createStore( - combineReducers({ - player: playerReducer, - settings: settingsReducer, - replayGain: replayGainReducer, - }), - { - player: { - queue: [ - { - uuid: '1', - musicSrc: 'song1.mp3', - title: 'Song 1', - artist: 'Artist 1', - }, - ], - current: { - uuid: '1', - trackId: 'track1', - song: { title: 'Song 1', artist: 'Artist 1' }, - }, - playIndex: 0, - mode: 'single', - volume: 0.8, - clear: false, - }, - settings: { - notifications: true, - }, - replayGain: { - gainMode: 'track', - }, - }, - ) - - const renderPlayer = () => { - return render( - - - - - , - ) +vi.mock('@material-ui/core', async () => { + const actual = await import('@material-ui/core') + return { + ...actual, + useMediaQuery: vi.fn(), } +}) + +vi.mock('react-admin', () => ({ + useGetOne: vi.fn(), +})) + +vi.mock('react-redux', () => ({ + useDispatch: vi.fn(), +})) + +vi.mock('../common', () => ({ + LoveButton: ({ className, disabled }) => ( + + ), + useToggleLove: vi.fn(), +})) + +vi.mock('../actions', () => ({ + openSaveQueueDialog: vi.fn(), +})) + +vi.mock('react-hotkeys', () => ({ + GlobalHotKeys: () =>
, +})) + +describe('', () => { + const mockToggleLove = vi.fn() + const mockDispatch = vi.fn() + const mockSongData = { id: 'song-1', name: 'Test Song', starred: false } beforeEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() + useGetOne.mockReturnValue({ data: mockSongData, loading: false }) + useToggleLove.mockReturnValue([mockToggleLove, false]) + useDispatch.mockReturnValue(mockDispatch) + openSaveQueueDialog.mockReturnValue({ type: 'OPEN_SAVE_QUEUE_DIALOG' }) }) - it('should render player when authenticated with queue', () => { - renderPlayer() + afterEach(cleanup) - expect(screen.getByTestId('react-jk-music-player')).toBeInTheDocument() - expect(screen.getByTestId('audio-title')).toBeInTheDocument() - expect(screen.getByTestId('player-toolbar')).toBeInTheDocument() + describe('Desktop layout', () => { + beforeEach(() => { + useMediaQuery.mockReturnValue(true) // isDesktop = true + }) + + it('renders desktop toolbar with both buttons', () => { + render() + + // Both buttons should be in a single list item + const listItems = screen.getAllByRole('listitem') + expect(listItems).toHaveLength(1) + + // Verify both buttons are rendered + expect(screen.getByTestId('save-queue-button')).toBeInTheDocument() + expect(screen.getByTestId('love-button')).toBeInTheDocument() + + // Verify desktop classes are applied + expect(listItems[0].className).toContain('toolbar') + }) + + it('disables save queue button when isRadio is true', () => { + render() + + const saveQueueButton = screen.getByTestId('save-queue-button') + expect(saveQueueButton).toBeDisabled() + }) + + it('disables love button when conditions are met', () => { + useGetOne.mockReturnValue({ data: mockSongData, loading: true }) + + render() + + const loveButton = screen.getByTestId('love-button') + expect(loveButton).toBeDisabled() + }) + + it('opens save queue dialog when save button is clicked', () => { + render() + + const saveQueueButton = screen.getByTestId('save-queue-button') + fireEvent.click(saveQueueButton) + + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'OPEN_SAVE_QUEUE_DIALOG', + }) + }) }) - it('should not render when not authenticated', () => { - // Mock unauthenticated state - const { useAuthState } = jest.requireMock('react-admin') - useAuthState.mockReturnValue({ authenticated: false }) + describe('Mobile layout', () => { + beforeEach(() => { + useMediaQuery.mockReturnValue(false) // isDesktop = false + }) - const { container } = renderPlayer() - expect(container.firstChild).toBeNull() + it('renders mobile toolbar with buttons in separate list items', () => { + render() - // Reset mock - const { useAuthState: originalUseAuthState } = - jest.requireMock('react-admin') - originalUseAuthState.mockReturnValue({ authenticated: true }) + // Each button should be in its own list item + const listItems = screen.getAllByRole('listitem') + expect(listItems).toHaveLength(2) + + // Verify both buttons are rendered + expect(screen.getByTestId('save-queue-button')).toBeInTheDocument() + expect(screen.getByTestId('love-button')).toBeInTheDocument() + + // Verify mobile classes are applied + expect(listItems[0].className).toContain('mobileListItem') + expect(listItems[1].className).toContain('mobileListItem') + }) + + it('disables save queue button when isRadio is true', () => { + render() + + const saveQueueButton = screen.getByTestId('save-queue-button') + expect(saveQueueButton).toBeDisabled() + }) + + it('disables love button when conditions are met', () => { + useGetOne.mockReturnValue({ data: mockSongData, loading: true }) + + render() + + const loveButton = screen.getByTestId('love-button') + expect(loveButton).toBeDisabled() + }) }) - it('should not render when queue is empty', () => { - const emptyStore = createStore( - combineReducers({ - player: playerReducer, - settings: settingsReducer, - replayGain: replayGainReducer, - }), - { - player: { queue: [] }, - settings: { notifications: true }, - replayGain: { gainMode: 'track' }, - }, - ) + describe('Common behavior', () => { + it('renders global hotkeys in both layouts', () => { + // Test desktop layout + useMediaQuery.mockReturnValue(true) + render() + expect(screen.getByTestId('global-hotkeys')).toBeInTheDocument() - const { container } = render( - - - - - , - ) + // Cleanup and test mobile layout + cleanup() + useMediaQuery.mockReturnValue(false) + render() + expect(screen.getByTestId('global-hotkeys')).toBeInTheDocument() + }) - expect(container.firstChild).toBeNull() + it('disables buttons when id is not provided', () => { + render() + + const loveButton = screen.getByTestId('love-button') + expect(loveButton).toBeDisabled() + }) }) - - it('should have proper accessibility attributes', () => { - renderPlayer() - - const playerRegion = screen.getByRole('region') - expect(playerRegion).toHaveAttribute('aria-label', 'player.audioPlayer') - expect(playerRegion).toHaveAttribute('aria-live', 'polite') - }) - - it('should render audio title with correct information', () => { - renderPlayer() - - expect(screen.getByTestId('audio-title')).toHaveTextContent('Song 1') - }) - - it('should render player toolbar with track ID', () => { - renderPlayer() - - expect(screen.getByTestId('player-toolbar')).toHaveTextContent('track1') - }) - - it('should handle mobile player detection', () => { - // Mock mobile detection - const { useMediaQuery } = jest.requireMock('@material-ui/core') - useMediaQuery.mockReturnValue(false) // Mobile - - renderPlayer() - - // Mobile-specific logic should be applied - // This would be tested more thoroughly with actual mobile behavior - }) - - it('should update document title when not visible', () => { - // Mock empty queue to make player not visible - const emptyStore = createStore( - combineReducers({ - player: playerReducer, - settings: settingsReducer, - replayGain: replayGainReducer, - }), - { - player: { queue: [] }, - settings: { notifications: true }, - replayGain: { gainMode: 'track' }, - }, - ) - - render( - - - - - , - ) - - // Document title should be reset when player is not visible - expect(document.title).toBe('Navidrome') - }) - - it('should integrate with theme provider', () => { - renderPlayer() - - // ThemeProvider should wrap the component - expect( - screen.getByTestId('react-jk-music-player').parentElement, - ).toBeInTheDocument() - }) -}) +}) \ No newline at end of file diff --git a/ui/src/audioplayer/hooks/usePlayerState.test.js b/ui/src/audioplayer/hooks/usePlayerState.test.js index d8b4f1ad1..db3549bde 100644 --- a/ui/src/audioplayer/hooks/usePlayerState.test.js +++ b/ui/src/audioplayer/hooks/usePlayerState.test.js @@ -1,3 +1,5 @@ +/* eslint-env node */ + import { renderHook } from '@testing-library/react-hooks' import { usePlayerState } from './usePlayerState' import { useDispatch, useSelector } from 'react-redux' @@ -32,6 +34,7 @@ describe('usePlayerState', () => { const mockDispatch = vi.fn() beforeEach(() => { + vi.resetModules() vi.clearAllMocks() useDispatch.mockReturnValue(mockDispatch) useSelector.mockReturnValue(mockPlayerState)