80 lines
3.4 KiB
TypeScript
80 lines
3.4 KiB
TypeScript
import type { AuthSession, Track } from '@pipu/core';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { Player } from './player';
|
|
|
|
const track: Track = {
|
|
id: 'a', title: 'A', duration_ms: 200_000, artists: [],
|
|
album: { id: 'album', title: 'Album', year: null, cover_url: null, cover_colors: [] },
|
|
genres: [], disc_no: null, track_no: null, available: true,
|
|
stream_url: '/s/a', reaction: null, features: null,
|
|
};
|
|
const ok = (data: unknown) => ({ data, response: new Response() });
|
|
|
|
function fixture() {
|
|
localStorage.removeItem('pipu.player');
|
|
const client = {
|
|
GET: vi.fn().mockResolvedValue(ok({
|
|
track_id: 'a', queue: ['a'], queue_index: 0, position_ms: 42_000,
|
|
volume: 0.8, shuffle: false, repeat_mode: 'off', updated_at: '2026-10-21',
|
|
})),
|
|
POST: vi.fn().mockResolvedValue(ok({ items: [track] })),
|
|
PUT: vi.fn().mockResolvedValue(ok({})),
|
|
};
|
|
const audio = document.createElement('audio');
|
|
const player = new Player({ client, state: { status: 'signed_out' } } as unknown as AuthSession, audio);
|
|
return { player, audio, client };
|
|
}
|
|
|
|
afterEach(() => { vi.restoreAllMocks(); localStorage.removeItem('pipu.player'); });
|
|
|
|
describe('restored audio preparation', () => {
|
|
it('buffers at the saved position without playback or listening events, then reuses the source', async () => {
|
|
const { player, audio, client } = fixture();
|
|
const play = vi.spyOn(audio, 'play');
|
|
const source = vi.spyOn(audio, 'src', 'set');
|
|
await player.restore();
|
|
expect(audio.getAttribute('src')).toBe('/s/a');
|
|
expect(audio.currentTime).toBe(42);
|
|
expect(audio.preload).toBe('auto');
|
|
expect(play).not.toHaveBeenCalled();
|
|
expect(client.PUT).not.toHaveBeenCalled();
|
|
expect(player.getState()).toMatchObject({ playing: false, loading: false, positionMs: 42_000 });
|
|
player.onHide();
|
|
expect(client.POST).toHaveBeenCalledTimes(1); // Only the track lookup, no listening events.
|
|
player.play();
|
|
expect(play).toHaveBeenCalledTimes(1);
|
|
expect(source).toHaveBeenCalledTimes(1);
|
|
expect(player.getState().playing).toBe(true);
|
|
player.dispose();
|
|
});
|
|
|
|
it('does not autoplay or skip after a preload failure; Play retries the source', async () => {
|
|
const { player, audio } = fixture();
|
|
const play = vi.spyOn(audio, 'play');
|
|
await player.restore();
|
|
audio.dispatchEvent(new Event('error'));
|
|
audio.dispatchEvent(new Event('ended'));
|
|
expect(play).not.toHaveBeenCalled();
|
|
expect(player.getState()).toMatchObject({ track: { id: 'a' }, positionMs: 42_000, playing: false });
|
|
player.play();
|
|
expect(audio.getAttribute('src')).toBe('/s/a');
|
|
expect(play).toHaveBeenCalledTimes(1);
|
|
player.dispose();
|
|
});
|
|
|
|
it.each(['selection', 'dispose'])('ignores a restore lookup completed after %s', async (action) => {
|
|
const { player, client, audio } = fixture();
|
|
let resolve!: (value: unknown) => void;
|
|
client.POST.mockReturnValueOnce(new Promise((done) => { resolve = done; }));
|
|
const restoring = player.restore();
|
|
await vi.waitFor(() => expect(client.POST).toHaveBeenCalled());
|
|
if (action === 'selection') player.playList([{ ...track, id: 'b', stream_url: '/s/b' }], 0, { type: 'search' });
|
|
else player.dispose();
|
|
resolve(ok({ items: [track] }));
|
|
await restoring;
|
|
expect(audio.getAttribute('src')).toBe(action === 'selection' ? '/s/b' : null);
|
|
if (action === 'selection') player.dispose();
|
|
});
|
|
});
|