2025-12-03 21:56:50 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
|
|
|
import userEvent from '@testing-library/user-event';
|
|
|
|
|
import { MiniPlayer } from './MiniPlayer';
|
|
|
|
|
import { usePlayer } from '../hooks/usePlayer';
|
|
|
|
|
import type { Track } from '../types';
|
|
|
|
|
|
|
|
|
|
// Mock usePlayer
|
|
|
|
|
vi.mock('../hooks/usePlayer', () => ({
|
|
|
|
|
usePlayer: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Mock child components
|
|
|
|
|
vi.mock('./TrackInfo', () => ({
|
|
|
|
|
TrackInfo: ({ track }: { track: Track | null }) => (
|
|
|
|
|
<div data-testid="track-info">{track?.title || 'No track'}</div>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('./PlayPauseButton', () => ({
|
2025-12-13 02:34:34 +00:00
|
|
|
PlayPauseButton: ({
|
|
|
|
|
isPlaying,
|
|
|
|
|
onClick,
|
|
|
|
|
}: {
|
|
|
|
|
isPlaying: boolean;
|
|
|
|
|
onClick: () => void;
|
|
|
|
|
}) => (
|
2025-12-03 21:56:50 +00:00
|
|
|
<button data-testid="play-pause" onClick={onClick}>
|
|
|
|
|
{isPlaying ? 'Pause' : 'Play'}
|
|
|
|
|
</button>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('./NextPreviousButtons', () => ({
|
|
|
|
|
NextPreviousButtons: ({
|
|
|
|
|
onNext,
|
|
|
|
|
onPrevious,
|
|
|
|
|
canGoNext,
|
|
|
|
|
canGoPrevious,
|
|
|
|
|
}: {
|
|
|
|
|
onNext: () => void;
|
|
|
|
|
onPrevious: () => void;
|
|
|
|
|
canGoNext: boolean;
|
|
|
|
|
canGoPrevious: boolean;
|
|
|
|
|
}) => (
|
|
|
|
|
<div data-testid="next-previous">
|
|
|
|
|
<button onClick={onPrevious} disabled={!canGoPrevious}>
|
|
|
|
|
Previous
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={onNext} disabled={!canGoNext}>
|
|
|
|
|
Next
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('./ProgressBar', () => ({
|
|
|
|
|
ProgressBar: ({
|
|
|
|
|
currentTime,
|
|
|
|
|
duration,
|
|
|
|
|
onSeek,
|
|
|
|
|
}: {
|
|
|
|
|
currentTime: number;
|
|
|
|
|
duration: number;
|
|
|
|
|
onSeek: (time: number) => void;
|
|
|
|
|
}) => (
|
|
|
|
|
<div data-testid="progress-bar" onClick={() => onSeek(30)}>
|
|
|
|
|
{currentTime} / {duration}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('./VolumeControl', () => ({
|
|
|
|
|
VolumeControl: ({
|
|
|
|
|
volume,
|
|
|
|
|
muted,
|
|
|
|
|
onVolumeChange,
|
|
|
|
|
onMuteToggle,
|
|
|
|
|
}: {
|
|
|
|
|
volume: number;
|
|
|
|
|
muted: boolean;
|
|
|
|
|
onVolumeChange: (vol: number) => void;
|
|
|
|
|
onMuteToggle: () => void;
|
|
|
|
|
}) => (
|
|
|
|
|
<div data-testid="volume-control">
|
|
|
|
|
<button onClick={onMuteToggle}>{muted ? 'Unmute' : 'Mute'}</button>
|
|
|
|
|
<button onClick={() => onVolumeChange(50)}>Set Volume</button>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const mockTrack: Track = {
|
|
|
|
|
id: 1,
|
|
|
|
|
title: 'Test Track',
|
|
|
|
|
artist: 'Test Artist',
|
|
|
|
|
duration: 180,
|
|
|
|
|
url: 'https://example.com/track.mp3',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const defaultPlayerState = {
|
|
|
|
|
currentTrack: mockTrack,
|
|
|
|
|
isPlaying: false,
|
|
|
|
|
currentTime: 0,
|
|
|
|
|
duration: 180,
|
|
|
|
|
volume: 100,
|
|
|
|
|
muted: false,
|
|
|
|
|
queue: [mockTrack],
|
|
|
|
|
currentIndex: 0,
|
|
|
|
|
repeat: 'off' as const,
|
|
|
|
|
shuffle: false,
|
|
|
|
|
play: vi.fn(),
|
|
|
|
|
pause: vi.fn(),
|
|
|
|
|
resume: vi.fn(),
|
|
|
|
|
stop: vi.fn(),
|
|
|
|
|
next: vi.fn(),
|
|
|
|
|
previous: vi.fn(),
|
|
|
|
|
seek: vi.fn(),
|
|
|
|
|
setVolume: vi.fn(),
|
|
|
|
|
toggleMute: vi.fn(),
|
|
|
|
|
toggleShuffle: vi.fn(),
|
|
|
|
|
setRepeat: vi.fn(),
|
|
|
|
|
addToQueue: vi.fn(),
|
|
|
|
|
clearQueue: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('MiniPlayer', () => {
|
|
|
|
|
const mockOnToggle = vi.fn();
|
|
|
|
|
const mockOnClose = vi.fn();
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue(defaultPlayerState as any);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not render when isVisible is false', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={false} onToggle={mockOnToggle} />);
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.queryByRole('region', { name: 'Mini lecteur audio' }),
|
|
|
|
|
).not.toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not render when no track is playing', () => {
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
currentTrack: null,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.queryByRole('region', { name: 'Mini lecteur audio' }),
|
|
|
|
|
).not.toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should render when visible and track is playing', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.getByRole('region', { name: 'Mini lecteur audio' }),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display track info', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
expect(screen.getByTestId('track-info')).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText('Test Track')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display play/pause button', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
expect(screen.getByTestId('play-pause')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call pause when play button is clicked and playing', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const mockPause = vi.fn();
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
isPlaying: true,
|
|
|
|
|
pause: mockPause,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const playPauseButton = screen.getByTestId('play-pause');
|
|
|
|
|
await user.click(playPauseButton);
|
|
|
|
|
|
|
|
|
|
expect(mockPause).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call resume when play button is clicked and paused', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const mockResume = vi.fn();
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
isPlaying: false,
|
|
|
|
|
resume: mockResume,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const playPauseButton = screen.getByTestId('play-pause');
|
|
|
|
|
await user.click(playPauseButton);
|
|
|
|
|
|
|
|
|
|
expect(mockResume).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display next/previous buttons', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
expect(screen.getByTestId('next-previous')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call next when next button is clicked', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const mockNext = vi.fn();
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
next: mockNext,
|
|
|
|
|
queue: [mockTrack, { ...mockTrack, id: 2 }],
|
|
|
|
|
currentIndex: 0,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const nextButton = screen.getByText('Next');
|
|
|
|
|
await user.click(nextButton);
|
|
|
|
|
|
|
|
|
|
expect(mockNext).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call previous when previous button is clicked', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const mockPrevious = vi.fn();
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
previous: mockPrevious,
|
|
|
|
|
queue: [mockTrack, { ...mockTrack, id: 2 }],
|
|
|
|
|
currentIndex: 1,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const previousButton = screen.getByText('Previous');
|
|
|
|
|
await user.click(previousButton);
|
|
|
|
|
|
|
|
|
|
expect(mockPrevious).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display progress bar', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
expect(screen.getByTestId('progress-bar')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call seek when progress bar is clicked', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const mockSeek = vi.fn();
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
seek: mockSeek,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const progressBar = screen.getByTestId('progress-bar');
|
|
|
|
|
await user.click(progressBar);
|
|
|
|
|
|
|
|
|
|
expect(mockSeek).toHaveBeenCalledWith(30);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display volume control', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
expect(screen.getByTestId('volume-control')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call toggle when toggle button is clicked', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
|
|
|
|
|
const toggleButton = screen.getByLabelText('Agrandir le lecteur');
|
|
|
|
|
await user.click(toggleButton);
|
|
|
|
|
|
|
|
|
|
expect(mockOnToggle).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display close button when onClose is provided', () => {
|
2025-12-13 02:34:34 +00:00
|
|
|
render(
|
|
|
|
|
<MiniPlayer
|
|
|
|
|
isVisible={true}
|
|
|
|
|
onToggle={mockOnToggle}
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
expect(screen.getByLabelText('Fermer le mini lecteur')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not display close button when onClose is not provided', () => {
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.queryByLabelText('Fermer le mini lecteur'),
|
|
|
|
|
).not.toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call onClose when close button is clicked', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
2025-12-13 02:34:34 +00:00
|
|
|
render(
|
|
|
|
|
<MiniPlayer
|
|
|
|
|
isVisible={true}
|
|
|
|
|
onToggle={mockOnToggle}
|
|
|
|
|
onClose={mockOnClose}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
const closeButton = screen.getByLabelText('Fermer le mini lecteur');
|
|
|
|
|
await user.click(closeButton);
|
|
|
|
|
|
|
|
|
|
expect(mockOnClose).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should have fixed position at bottom by default', () => {
|
2025-12-13 02:34:34 +00:00
|
|
|
const { container } = render(
|
|
|
|
|
<MiniPlayer isVisible={true} onToggle={mockOnToggle} />,
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
const miniPlayer = container.querySelector('[role="region"]');
|
|
|
|
|
expect(miniPlayer).toHaveClass('fixed', 'bottom-0');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should have fixed position at top when position prop is top', () => {
|
|
|
|
|
const { container } = render(
|
2025-12-13 02:34:34 +00:00
|
|
|
<MiniPlayer isVisible={true} onToggle={mockOnToggle} position="top" />,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
const miniPlayer = container.querySelector('[role="region"]');
|
|
|
|
|
expect(miniPlayer).toHaveClass('fixed', 'top-0');
|
|
|
|
|
expect(miniPlayer).not.toHaveClass('bottom-0');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should apply custom className', () => {
|
|
|
|
|
const { container } = render(
|
2025-12-13 02:34:34 +00:00
|
|
|
<MiniPlayer
|
|
|
|
|
isVisible={true}
|
|
|
|
|
onToggle={mockOnToggle}
|
|
|
|
|
className="custom-class"
|
|
|
|
|
/>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
const miniPlayer = container.querySelector('[role="region"]');
|
|
|
|
|
expect(miniPlayer).toHaveClass('custom-class');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should disable next button when cannot go next', () => {
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
queue: [mockTrack],
|
|
|
|
|
currentIndex: 0,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const nextButton = screen.getByText('Next');
|
|
|
|
|
expect(nextButton).toBeDisabled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should disable previous button when cannot go previous', () => {
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
queue: [mockTrack],
|
|
|
|
|
currentIndex: 0,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const previousButton = screen.getByText('Previous');
|
|
|
|
|
expect(previousButton).toBeDisabled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should enable next button when can go next', () => {
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
queue: [mockTrack, { ...mockTrack, id: 2 }],
|
|
|
|
|
currentIndex: 0,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const nextButton = screen.getByText('Next');
|
|
|
|
|
expect(nextButton).not.toBeDisabled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should enable previous button when can go previous', () => {
|
|
|
|
|
vi.mocked(usePlayer).mockReturnValue({
|
|
|
|
|
...defaultPlayerState,
|
|
|
|
|
queue: [mockTrack, { ...mockTrack, id: 2 }],
|
|
|
|
|
currentIndex: 1,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(<MiniPlayer isVisible={true} onToggle={mockOnToggle} />);
|
|
|
|
|
const previousButton = screen.getByText('Previous');
|
|
|
|
|
expect(previousButton).not.toBeDisabled();
|
|
|
|
|
});
|
|
|
|
|
});
|