Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ test-results/

# macOS-specific files
.DS_Store
package-lock.json
17 changes: 9 additions & 8 deletions src/components/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Slider from './player/Slider';
export default function Player() {
const audioPlayer = useRef<HTMLAudioElement | null>(null);
const progressRef = useRef<number | null>(null);
const [progress, setProgress] = useState(0);
const [currentTime, setCurrentTime] = useState(0);

if (currentEpisode.value === null) {
return;
Expand All @@ -21,9 +21,9 @@ export default function Player() {

function whilePlaying() {
if (audioPlayer.current?.duration) {
const percentage =
(audioPlayer.current.currentTime / audioPlayer.current.duration) * 100;
setProgress(percentage);
const time = audioPlayer.current.currentTime;
const percentage = (time / audioPlayer.current.duration) * 100;
setCurrentTime(time);

const slider = document.querySelector('.slider');
const particles = document.querySelector('.ship-particles');
Expand Down Expand Up @@ -65,11 +65,12 @@ export default function Player() {
}, [isPlaying.value]);

useEffect(() => {
if (progress >= 99.99) {
const duration = audioPlayer.current?.duration ?? 0;
if (duration > 0 && currentTime >= duration - 0.01) {
isPlaying.value = false;
setProgress(0);
setCurrentTime(0);
}
}, [progress]);
}, [currentTime]);

return (
<div class="player fixed inset-x-0 bottom-0 z-50 lg:left-112 xl:left-120">
Expand Down Expand Up @@ -102,7 +103,7 @@ export default function Player() {
</div>
<ForwardButton audioPlayer={audioPlayer} />
</div>
<Slider audioPlayer={audioPlayer} progress={progress} />
<Slider audioPlayer={audioPlayer} currentTime={currentTime} />
<div class="flex items-center gap-4">
<div class="flex items-center">
<PlaybackRateButton audioPlayer={audioPlayer} />
Expand Down
29 changes: 12 additions & 17 deletions src/components/player/Slider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import './styles.css';

type Props = {
audioPlayer: MutableRef<HTMLAudioElement | null>;
progress: number;
currentTime: number;
};

function parseTime(seconds: number) {
Expand All @@ -23,11 +23,10 @@ function formatTime(seconds: Array<number>, totalSeconds = seconds) {
.join(':');
}

export default function Slider({ audioPlayer, progress }: Props) {
let currentTime = parseTime(
Math.floor(audioPlayer.current?.currentTime ?? 0)
);
let totalTime = parseTime(Math.floor(audioPlayer.current?.duration ?? 0));
export default function Slider({ audioPlayer, currentTime }: Props) {
let currentTimeFormatted = parseTime(Math.floor(currentTime));
let duration = Math.floor(audioPlayer.current?.duration ?? 0);
let totalTime = parseTime(duration);

return (
<div class="absolute inset-x-0 bottom-full flex flex-auto touch-none items-center gap-6 md:relative">
Expand All @@ -36,20 +35,16 @@ export default function Slider({ audioPlayer, progress }: Props) {
role="slider"
aria-label="audio timeline"
aria-valuemin={0}
aria-valuemax={Math.floor(audioPlayer.current?.duration ?? 0)}
aria-valuenow={Math.floor(audioPlayer.current?.currentTime ?? 0)}
aria-valuetext={`${Math.floor(
audioPlayer.current?.currentTime ?? 0
)} seconds`}
aria-valuemax={duration}
aria-valuenow={Math.floor(currentTime)}
aria-valuetext={`${Math.floor(currentTime)} seconds`}
class="slider group"
type="range"
max="100"
value={progress}
max={duration}
value={Math.floor(currentTime)}
onInput={(e: InputEvent) => {
if (audioPlayer?.current) {
const value =
(Number((e.target as HTMLInputElement).value) / 100) *
audioPlayer.current.duration;
const value = Number((e.target as HTMLInputElement).value);
audioPlayer.current.currentTime = value;
}
}}
Expand All @@ -62,7 +57,7 @@ export default function Slider({ audioPlayer, progress }: Props) {
<div className="col-start-1 row-start-1 h-1 w-1 rounded-full"></div>
</div>
<span class="hidden text-sm text-nowrap tabular-nums md:inline-block">
{formatTime(currentTime, totalTime)} / {formatTime(totalTime)}
{formatTime(currentTimeFormatted, totalTime)} / {formatTime(totalTime)}
</span>
</div>
);
Expand Down
38 changes: 19 additions & 19 deletions tests/unit/Slider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ describe('Slider', () => {
});

it('renders slider with correct attributes', () => {
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);

const slider = screen.getByRole('slider');
expect(slider).toBeInTheDocument();
expect(slider).toHaveAttribute('aria-label', 'audio timeline');
expect(slider).toHaveAttribute('aria-orientation', 'horizontal');
expect(slider).toHaveAttribute('type', 'range');
expect(slider).toHaveAttribute('max', '100');
expect(slider).toHaveAttribute('max', '180');
});

it('displays correct aria values', () => {
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);

const slider = screen.getByRole('slider');
expect(slider).toHaveAttribute('aria-valuemin', '0');
Expand All @@ -41,12 +41,12 @@ describe('Slider', () => {
});

it('updates current time when slider is moved', () => {
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);

const slider = screen.getByRole('slider') as HTMLInputElement;

// Simulate moving slider to 50% (90 seconds into 180 second track)
fireEvent.input(slider, { target: { value: '50' } });
// Simulate moving slider to 90 seconds
fireEvent.input(slider, { target: { value: '90' } });

expect(mockAudioElement.currentTime).toBe(90);
});
Expand All @@ -55,7 +55,7 @@ describe('Slider', () => {
const nullRef = createRef();
nullRef.current = null;

render(<Slider audioPlayer={nullRef} progress={0} />);
render(<Slider audioPlayer={nullRef} currentTime={0} />);

const slider = screen.getByRole('slider');
expect(slider).toBeInTheDocument();
Expand All @@ -65,7 +65,7 @@ describe('Slider', () => {

it('displays time information on desktop', () => {
const { container } = render(
<Slider audioPlayer={audioPlayerRef} progress={33.33} />
<Slider audioPlayer={audioPlayerRef} currentTime={60} />
);

// Time display should be present (but may be hidden on mobile)
Expand All @@ -79,7 +79,7 @@ describe('Slider', () => {
mockAudioElement.duration = 0;
mockAudioElement.currentTime = 0;

render(<Slider audioPlayer={audioPlayerRef} progress={0} />);
render(<Slider audioPlayer={audioPlayerRef} currentTime={0} />);

const slider = screen.getByRole('slider');
expect(slider).toHaveAttribute('aria-valuemax', '0');
Expand All @@ -92,7 +92,7 @@ describe('Slider', () => {
mockAudioElement.duration = 7200; // 2:00:00

const { container } = render(
<Slider audioPlayer={audioPlayerRef} progress={50.85} />
<Slider audioPlayer={audioPlayerRef} currentTime={3661} />
);

// Check that time is formatted correctly with hours
Expand All @@ -106,7 +106,7 @@ describe('Slider', () => {
mockAudioElement.duration = 600; // 10:00

const { container } = render(
<Slider audioPlayer={audioPlayerRef} progress={10.83} />
<Slider audioPlayer={audioPlayerRef} currentTime={65} />
);

// Check that time is formatted without unnecessary leading zeros
Expand All @@ -116,7 +116,7 @@ describe('Slider', () => {
});

it('renders particle animation elements', () => {
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);

const particles = document.querySelector('.ship-particles');
expect(particles).toBeInTheDocument();
Expand All @@ -126,21 +126,21 @@ describe('Slider', () => {
expect(dots).toHaveLength(5);
});

it('calculates seek position correctly for different progress values', () => {
render(<Slider audioPlayer={audioPlayerRef} progress={0} />);
it('calculates seek position correctly for different time values', () => {
render(<Slider audioPlayer={audioPlayerRef} currentTime={0} />);

const slider = screen.getByRole('slider') as HTMLInputElement;

// Test 0% progress
// Test seeking to 0 seconds
fireEvent.input(slider, { target: { value: '0' } });
expect(mockAudioElement.currentTime).toBe(0);

// Test 100% progress
fireEvent.input(slider, { target: { value: '100' } });
// Test seeking to 180 seconds (end of track)
fireEvent.input(slider, { target: { value: '180' } });
expect(mockAudioElement.currentTime).toBe(180);

// Test 25% progress
fireEvent.input(slider, { target: { value: '25' } });
// Test seeking to 45 seconds
fireEvent.input(slider, { target: { value: '45' } });
expect(mockAudioElement.currentTime).toBe(45);
});
});