/** * Tests for PlaybackControls component. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import PlaybackControls from '../../components/PlaybackControls'; describe('PlaybackControls Component', () => { const mockOnPlay = vi.fn(); const mockOnPause = vi.fn(); const mockOnStop = vi.fn(); const mockOnTempoChange = vi.fn(); const mockOnSeek = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); it('should render play, pause, and stop buttons', () => { render(); expect(screen.getByLabelText(/play/i)).toBeInTheDocument(); expect(screen.getByLabelText(/pause/i)).toBeInTheDocument(); expect(screen.getByLabelText(/stop/i)).toBeInTheDocument(); }); it('should call onPlay when play button clicked', async () => { const user = userEvent.setup(); render(); const playButton = screen.getByLabelText(/play/i); await user.click(playButton); expect(mockOnPlay).toHaveBeenCalledTimes(1); }); it('should call onPause when pause button clicked', async () => { const user = userEvent.setup(); render(); const pauseButton = screen.getByLabelText(/pause/i); await user.click(pauseButton); expect(mockOnPause).toHaveBeenCalledTimes(1); }); it('should call onStop when stop button clicked', async () => { const user = userEvent.setup(); render(); const stopButton = screen.getByLabelText(/stop/i); await user.click(stopButton); expect(mockOnStop).toHaveBeenCalledTimes(1); }); it('should disable play button when already playing', () => { render(); const playButton = screen.getByLabelText(/play/i); expect(playButton).toBeDisabled(); }); it('should show pause button only when playing', () => { const { rerender } = render(); const pauseButton = screen.getByLabelText(/pause/i); expect(pauseButton).toBeDisabled(); rerender(); expect(pauseButton).not.toBeDisabled(); }); it('should render tempo control', () => { render(); expect(screen.getByLabelText(/tempo/i)).toBeInTheDocument(); expect(screen.getByDisplayValue('120')).toBeInTheDocument(); }); it('should update tempo when slider moved', async () => { render(); const tempoSlider = screen.getByLabelText(/tempo/i); fireEvent.change(tempoSlider, { target: { value: '140' } }); expect(mockOnTempoChange).toHaveBeenCalledWith(140); }); it('should enforce tempo min and max bounds', () => { render(); const tempoSlider = screen.getByLabelText(/tempo/i) as HTMLInputElement; expect(tempoSlider.min).toBe('40'); expect(tempoSlider.max).toBe('240'); }); it('should display current playback position', () => { render(); // Should show time in format like 0:30 / 3:00 expect(screen.getByText(/0:30/)).toBeInTheDocument(); expect(screen.getByText(/3:00/)).toBeInTheDocument(); }); it('should render seek bar', () => { render(); const seekBar = screen.getByRole('slider', { name: /seek/i }); expect(seekBar).toBeInTheDocument(); }); it('should seek when seek bar moved', async () => { render( ); const seekBar = screen.getByRole('slider', { name: /seek/i }); fireEvent.change(seekBar, { target: { value: '90' } }); expect(mockOnSeek).toHaveBeenCalledWith(90); }); it('should show loading state when initializing', () => { render(); expect(screen.getByText(/loading/i)).toBeInTheDocument(); expect(screen.getByLabelText(/play/i)).toBeDisabled(); }); it('should disable controls when no audio loaded', () => { render(); expect(screen.getByLabelText(/play/i)).toBeDisabled(); expect(screen.getByLabelText(/pause/i)).toBeDisabled(); expect(screen.getByLabelText(/stop/i)).toBeDisabled(); }); it('should show volume control', () => { render(); expect(screen.getByLabelText(/volume/i)).toBeInTheDocument(); }); it('should update volume when volume slider moved', async () => { const mockOnVolumeChange = vi.fn(); render( ); const volumeSlider = screen.getByLabelText(/volume/i); fireEvent.change(volumeSlider, { target: { value: '0.5' } }); expect(mockOnVolumeChange).toHaveBeenCalledWith(0.5); }); it('should toggle loop mode', async () => { const user = userEvent.setup(); const mockOnLoopToggle = vi.fn(); render(); const loopButton = screen.getByLabelText(/loop/i); await user.click(loopButton); expect(mockOnLoopToggle).toHaveBeenCalledWith(true); }); it('should show loop indicator when loop enabled', () => { render(); const loopButton = screen.getByLabelText(/loop/i); expect(loopButton).toHaveClass(/active|enabled/); }); it('should format time correctly', () => { render(); // 125 seconds = 2:05, 3665 seconds = 1:01:05 expect(screen.getByText(/2:05/)).toBeInTheDocument(); expect(screen.getByText(/1:01:05/)).toBeInTheDocument(); }); it('should support keyboard shortcuts', async () => { const user = userEvent.setup(); render( ); // Spacebar should toggle play/pause await user.keyboard(' '); expect(mockOnPlay).toHaveBeenCalled(); }); });