fix: test

This commit is contained in:
Xavier Araque 2025-11-07 16:46:30 +01:00
parent efeaa80de9
commit 6a9ccb309c
2 changed files with 151 additions and 241 deletions

View File

@ -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 }) => (
<div data-testid="audio-title">{audioInfo?.song?.title || 'No song'}</div>
),
}))
jest.mock('./PlayerToolbar', () => ({
__esModule: true,
default: ({ id }) => <div data-testid="player-toolbar">{id || 'No ID'}</div>,
}))
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 }) => (
<div data-testid="react-jk-music-player" {...props}>
{children}
</div>
),
}))
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 }) => <div>{children}</div>,
}))
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(
<Provider store={mockStore}>
<ThemeProvider theme={createMuiTheme()}>
<Player />
</ThemeProvider>
</Provider>,
)
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 }) => (
<button data-testid="love-button" className={className} disabled={disabled}>
Love
</button>
),
useToggleLove: vi.fn(),
}))
vi.mock('../actions', () => ({
openSaveQueueDialog: vi.fn(),
}))
vi.mock('react-hotkeys', () => ({
GlobalHotKeys: () => <div data-testid="global-hotkeys" />,
}))
describe('<PlayerToolbar />', () => {
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(<PlayerToolbar id="song-1" />)
// 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(<PlayerToolbar id="song-1" isRadio={true} />)
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(<PlayerToolbar id="song-1" />)
const loveButton = screen.getByTestId('love-button')
expect(loveButton).toBeDisabled()
})
it('opens save queue dialog when save button is clicked', () => {
render(<PlayerToolbar id="song-1" />)
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(<PlayerToolbar id="song-1" />)
// 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(<PlayerToolbar id="song-1" isRadio={true} />)
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(<PlayerToolbar id="song-1" />)
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(<PlayerToolbar id="song-1" />)
expect(screen.getByTestId('global-hotkeys')).toBeInTheDocument()
const { container } = render(
<Provider store={emptyStore}>
<ThemeProvider theme={createMuiTheme()}>
<Player />
</ThemeProvider>
</Provider>,
)
// Cleanup and test mobile layout
cleanup()
useMediaQuery.mockReturnValue(false)
render(<PlayerToolbar id="song-1" />)
expect(screen.getByTestId('global-hotkeys')).toBeInTheDocument()
})
expect(container.firstChild).toBeNull()
it('disables buttons when id is not provided', () => {
render(<PlayerToolbar />)
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(
<Provider store={emptyStore}>
<ThemeProvider theme={createMuiTheme()}>
<Player />
</ThemeProvider>
</Provider>,
)
// 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()
})
})
})

View File

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