File size: 6,410 Bytes
44a2550
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/**
 * 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(<PlaybackControls />);

    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(<PlaybackControls onPlay={mockOnPlay} />);

    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(<PlaybackControls onPause={mockOnPause} isPlaying />);

    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(<PlaybackControls onStop={mockOnStop} />);

    const stopButton = screen.getByLabelText(/stop/i);
    await user.click(stopButton);

    expect(mockOnStop).toHaveBeenCalledTimes(1);
  });

  it('should disable play button when already playing', () => {
    render(<PlaybackControls isPlaying />);

    const playButton = screen.getByLabelText(/play/i);
    expect(playButton).toBeDisabled();
  });

  it('should show pause button only when playing', () => {
    const { rerender } = render(<PlaybackControls isPlaying={false} />);

    const pauseButton = screen.getByLabelText(/pause/i);
    expect(pauseButton).toBeDisabled();

    rerender(<PlaybackControls isPlaying />);
    expect(pauseButton).not.toBeDisabled();
  });

  it('should render tempo control', () => {
    render(<PlaybackControls tempo={120} />);

    expect(screen.getByLabelText(/tempo/i)).toBeInTheDocument();
    expect(screen.getByDisplayValue('120')).toBeInTheDocument();
  });

  it('should update tempo when slider moved', async () => {
    render(<PlaybackControls tempo={120} onTempoChange={mockOnTempoChange} />);

    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(<PlaybackControls tempo={120} minTempo={40} maxTempo={240} />);

    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(<PlaybackControls currentTime={30} duration={180} />);

    // 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(<PlaybackControls currentTime={30} duration={180} />);

    const seekBar = screen.getByRole('slider', { name: /seek/i });
    expect(seekBar).toBeInTheDocument();
  });

  it('should seek when seek bar moved', async () => {
    render(
      <PlaybackControls
        currentTime={30}
        duration={180}
        onSeek={mockOnSeek}
      />
    );

    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(<PlaybackControls loading />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/play/i)).toBeDisabled();
  });

  it('should disable controls when no audio loaded', () => {
    render(<PlaybackControls audioLoaded={false} />);

    expect(screen.getByLabelText(/play/i)).toBeDisabled();
    expect(screen.getByLabelText(/pause/i)).toBeDisabled();
    expect(screen.getByLabelText(/stop/i)).toBeDisabled();
  });

  it('should show volume control', () => {
    render(<PlaybackControls showVolumeControl />);

    expect(screen.getByLabelText(/volume/i)).toBeInTheDocument();
  });

  it('should update volume when volume slider moved', async () => {
    const mockOnVolumeChange = vi.fn();
    render(
      <PlaybackControls
        showVolumeControl
        volume={0.8}
        onVolumeChange={mockOnVolumeChange}
      />
    );

    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(<PlaybackControls onLoopToggle={mockOnLoopToggle} />);

    const loopButton = screen.getByLabelText(/loop/i);
    await user.click(loopButton);

    expect(mockOnLoopToggle).toHaveBeenCalledWith(true);
  });

  it('should show loop indicator when loop enabled', () => {
    render(<PlaybackControls loop />);

    const loopButton = screen.getByLabelText(/loop/i);
    expect(loopButton).toHaveClass(/active|enabled/);
  });

  it('should format time correctly', () => {
    render(<PlaybackControls currentTime={125} duration={3665} />);

    // 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(
      <PlaybackControls
        onPlay={mockOnPlay}
        onPause={mockOnPause}
        supportKeyboardShortcuts
      />
    );

    // Spacebar should toggle play/pause
    await user.keyboard(' ');
    expect(mockOnPlay).toHaveBeenCalled();
  });
});