Skip to content

Commit e3dd6c0

Browse files
Add some player tests
1 parent ceb21fe commit e3dd6c0

File tree

6 files changed

+705
-0
lines changed

6 files changed

+705
-0
lines changed

tests/unit/ForwardButton.test.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { render, screen, fireEvent } from '@testing-library/preact';
2+
import { beforeEach, describe, expect, it } from 'vitest';
3+
import ForwardButton from '../../src/components/player/ForwardButton';
4+
import { createRef } from 'preact';
5+
6+
// Mock audio element
7+
const mockAudioElement = {
8+
currentTime: 0,
9+
duration: 180
10+
};
11+
12+
describe('ForwardButton', () => {
13+
let audioPlayerRef: any;
14+
15+
beforeEach(() => {
16+
mockAudioElement.currentTime = 0;
17+
audioPlayerRef = createRef();
18+
audioPlayerRef.current = mockAudioElement;
19+
});
20+
21+
it('renders forward button with correct label', () => {
22+
render(<ForwardButton audioPlayer={audioPlayerRef} />);
23+
24+
const button = screen.getByRole('button');
25+
expect(button).toBeInTheDocument();
26+
expect(button).toHaveAttribute('aria-label', 'Fast-forward 10 seconds');
27+
});
28+
29+
it('advances audio by 10 seconds when clicked', () => {
30+
render(<ForwardButton audioPlayer={audioPlayerRef} />);
31+
32+
const button = screen.getByRole('button');
33+
34+
expect(mockAudioElement.currentTime).toBe(0);
35+
36+
fireEvent.click(button);
37+
expect(mockAudioElement.currentTime).toBe(10);
38+
39+
fireEvent.click(button);
40+
expect(mockAudioElement.currentTime).toBe(20);
41+
});
42+
43+
it('handles null audio player gracefully', () => {
44+
const nullRef = createRef();
45+
nullRef.current = null;
46+
47+
render(<ForwardButton audioPlayer={nullRef} />);
48+
49+
const button = screen.getByRole('button');
50+
51+
// Should render without crashing
52+
expect(button).toBeInTheDocument();
53+
54+
// Clicking should not crash when audio player is null
55+
expect(() => fireEvent.click(button)).not.toThrow();
56+
});
57+
58+
it('has proper styling classes', () => {
59+
render(<ForwardButton audioPlayer={audioPlayerRef} />);
60+
61+
const button = screen.getByRole('button');
62+
expect(button).toHaveClass('forward-button');
63+
expect(button).toHaveClass('gradient-icon');
64+
expect(button).toHaveClass('relative');
65+
expect(button).toHaveClass('rounded-full');
66+
expect(button).toHaveClass('focus:outline-hidden');
67+
});
68+
69+
it('allows forwarding from any current position', () => {
70+
mockAudioElement.currentTime = 45;
71+
72+
render(<ForwardButton audioPlayer={audioPlayerRef} />);
73+
74+
const button = screen.getByRole('button');
75+
76+
fireEvent.click(button);
77+
expect(mockAudioElement.currentTime).toBe(55);
78+
});
79+
80+
it('adds 10 seconds even near track end (browser handles clamping)', () => {
81+
mockAudioElement.currentTime = 175; // Near end of 180 second track
82+
83+
render(<ForwardButton audioPlayer={audioPlayerRef} />);
84+
85+
const button = screen.getByRole('button');
86+
87+
fireEvent.click(button);
88+
// Current implementation just adds 10 seconds without bounds checking
89+
// In a real browser, the audio element would clamp this to duration
90+
expect(mockAudioElement.currentTime).toBe(185);
91+
});
92+
});

tests/unit/MuteButton.test.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { render, screen, fireEvent } from '@testing-library/preact';
2+
import { beforeEach, describe, expect, it } from 'vitest';
3+
import MuteButton from '../../src/components/player/MuteButton';
4+
import { isMuted } from '../../src/components/state';
5+
6+
describe('MuteButton', () => {
7+
beforeEach(() => {
8+
isMuted.value = false;
9+
});
10+
11+
it('renders mute button when not muted', () => {
12+
render(<MuteButton />);
13+
14+
const button = screen.getByRole('button');
15+
expect(button).toBeInTheDocument();
16+
expect(button).toHaveAttribute('aria-label', 'Mute');
17+
expect(button).toHaveClass('mute-button');
18+
expect(button).not.toHaveClass('unmute-button');
19+
});
20+
21+
it('renders unmute button when muted', () => {
22+
isMuted.value = true;
23+
24+
render(<MuteButton />);
25+
26+
const button = screen.getByRole('button');
27+
expect(button).toHaveAttribute('aria-label', 'Unmute');
28+
expect(button).toHaveClass('unmute-button');
29+
expect(button).not.toHaveClass('mute-button');
30+
});
31+
32+
it('toggles mute state when clicked', () => {
33+
render(<MuteButton />);
34+
35+
const button = screen.getByRole('button');
36+
37+
expect(isMuted.value).toBe(false);
38+
expect(button).toHaveAttribute('aria-label', 'Mute');
39+
40+
fireEvent.click(button);
41+
42+
expect(isMuted.value).toBe(true);
43+
expect(button).toHaveAttribute('aria-label', 'Unmute');
44+
45+
fireEvent.click(button);
46+
47+
expect(isMuted.value).toBe(false);
48+
expect(button).toHaveAttribute('aria-label', 'Mute');
49+
});
50+
51+
it('has proper styling classes', () => {
52+
render(<MuteButton />);
53+
54+
const button = screen.getByRole('button');
55+
expect(button).toHaveClass('gradient-icon');
56+
expect(button).toHaveClass('group');
57+
expect(button).toHaveClass('relative');
58+
});
59+
60+
it('updates class when mute state changes externally', () => {
61+
const { rerender } = render(<MuteButton />);
62+
63+
const button = screen.getByRole('button');
64+
expect(button).toHaveClass('mute-button');
65+
66+
// Change state externally
67+
isMuted.value = true;
68+
rerender(<MuteButton />);
69+
70+
expect(button).toHaveClass('unmute-button');
71+
expect(button).not.toHaveClass('mute-button');
72+
});
73+
74+
it('maintains proper button type', () => {
75+
render(<MuteButton />);
76+
77+
const button = screen.getByRole('button');
78+
expect(button).toHaveAttribute('type', 'button');
79+
});
80+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { render, screen, fireEvent } from '@testing-library/preact';
2+
import { beforeEach, describe, expect, it } from 'vitest';
3+
import PlaybackRateButton from '../../src/components/player/PlaybackRateButton';
4+
import { createRef } from 'preact';
5+
6+
// Mock audio element
7+
const mockAudioElement = {
8+
playbackRate: 1,
9+
currentTime: 0,
10+
duration: 180
11+
};
12+
13+
describe('PlaybackRateButton', () => {
14+
let audioPlayerRef: any;
15+
16+
beforeEach(() => {
17+
mockAudioElement.playbackRate = 1;
18+
audioPlayerRef = createRef();
19+
audioPlayerRef.current = mockAudioElement;
20+
});
21+
22+
it('renders with initial playback rate of 1x', () => {
23+
render(<PlaybackRateButton audioPlayer={audioPlayerRef} />);
24+
25+
const button = screen.getByRole('button');
26+
expect(button).toBeInTheDocument();
27+
expect(button).toHaveAttribute('aria-label', 'Playback rate');
28+
expect(screen.getByText('1')).toBeInTheDocument();
29+
expect(screen.getByText('x')).toBeInTheDocument();
30+
});
31+
32+
it('cycles through playback rates when clicked', () => {
33+
render(<PlaybackRateButton audioPlayer={audioPlayerRef} />);
34+
35+
const button = screen.getByRole('button');
36+
37+
// Initial state: 1x
38+
expect(screen.getByText('1')).toBeInTheDocument();
39+
expect(mockAudioElement.playbackRate).toBe(1);
40+
41+
// Click 1: should go to 1.2x
42+
fireEvent.click(button);
43+
expect(screen.getByText('1.2')).toBeInTheDocument();
44+
expect(mockAudioElement.playbackRate).toBe(1.2);
45+
46+
// Click 2: should go to 1.5x
47+
fireEvent.click(button);
48+
expect(screen.getByText('1.5')).toBeInTheDocument();
49+
expect(mockAudioElement.playbackRate).toBe(1.5);
50+
51+
// Click 3: should go to 2x
52+
fireEvent.click(button);
53+
expect(screen.getByText('2')).toBeInTheDocument();
54+
expect(mockAudioElement.playbackRate).toBe(2);
55+
56+
// Click 4: should cycle back to 1x
57+
fireEvent.click(button);
58+
expect(screen.getByText('1')).toBeInTheDocument();
59+
expect(mockAudioElement.playbackRate).toBe(1);
60+
});
61+
62+
it('handles null audio player gracefully', () => {
63+
const nullRef = createRef();
64+
nullRef.current = null;
65+
66+
render(<PlaybackRateButton audioPlayer={nullRef} />);
67+
68+
const button = screen.getByRole('button');
69+
70+
// Should render without crashing
71+
expect(button).toBeInTheDocument();
72+
expect(screen.getByText('1')).toBeInTheDocument();
73+
74+
// Clicking should not crash when audio player is null
75+
expect(() => fireEvent.click(button)).not.toThrow();
76+
expect(screen.getByText('1.2')).toBeInTheDocument();
77+
});
78+
79+
it('has proper styling classes', () => {
80+
render(<PlaybackRateButton audioPlayer={audioPlayerRef} />);
81+
82+
const button = screen.getByRole('button');
83+
expect(button).toHaveClass('gradient-icon');
84+
expect(button).toHaveClass('relative');
85+
expect(button).toHaveClass('rounded-md');
86+
expect(button).toHaveClass('transition-colors');
87+
expect(button).toHaveClass('focus:outline-hidden');
88+
});
89+
90+
it('displays rate with x suffix correctly', () => {
91+
render(<PlaybackRateButton audioPlayer={audioPlayerRef} />);
92+
93+
const button = screen.getByRole('button');
94+
95+
// Test different rates maintain x suffix
96+
fireEvent.click(button); // 1.2x
97+
expect(screen.getByText('1.2')).toBeInTheDocument();
98+
expect(screen.getByText('x')).toBeInTheDocument();
99+
100+
fireEvent.click(button); // 1.5x
101+
expect(screen.getByText('1.5')).toBeInTheDocument();
102+
expect(screen.getByText('x')).toBeInTheDocument();
103+
104+
fireEvent.click(button); // 2x
105+
expect(screen.getByText('2')).toBeInTheDocument();
106+
expect(screen.getByText('x')).toBeInTheDocument();
107+
});
108+
109+
it('maintains state independently across renders', () => {
110+
const { rerender } = render(
111+
<PlaybackRateButton audioPlayer={audioPlayerRef} />
112+
);
113+
114+
const button = screen.getByRole('button');
115+
116+
// Change to 1.5x
117+
fireEvent.click(button);
118+
fireEvent.click(button);
119+
expect(screen.getByText('1.5')).toBeInTheDocument();
120+
121+
// Rerender should maintain the same state
122+
rerender(<PlaybackRateButton audioPlayer={audioPlayerRef} />);
123+
expect(screen.getByText('1.5')).toBeInTheDocument();
124+
});
125+
});

0 commit comments

Comments
 (0)