109 lines
5.0 KiB
TypeScript
109 lines
5.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, waitFor, act } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { PlayerProvider, usePlayer } from '../src/player/PlayerContext.js'
|
|
import type { Track } from '../src/types.js'
|
|
|
|
vi.mock('../src/api.js', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../src/api.js')>()
|
|
return { ...actual, api: { ...actual.api, getAlbumTracks: vi.fn(), markPlayed: vi.fn() } }
|
|
})
|
|
|
|
import { api } from '../src/api.js'
|
|
|
|
beforeEach(() => {
|
|
vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue()
|
|
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockReturnValue(undefined)
|
|
})
|
|
|
|
const tracks: Track[] = [
|
|
{ id: 's1', title: 'Overture', duration: 200, track: 1 },
|
|
{ id: 's2', title: 'Theme de Yoyo', duration: 300, track: 2 },
|
|
]
|
|
|
|
function Probe() {
|
|
const { state, load, toggle, next, prev, close } = usePlayer()
|
|
return (
|
|
<div>
|
|
<div>phase:{state.album ? (state.playing ? 'playing' : 'paused') : 'empty'}</div>
|
|
<div>track:{state.tracks[state.index]?.title ?? 'none'}</div>
|
|
<button type="button" onClick={() => load({ id: 'a1', title: 'Motion', artist: 'TCO' })}>load</button>
|
|
<button type="button" onClick={() => toggle()}>toggle</button>
|
|
<button type="button" onClick={() => next()}>next</button>
|
|
<button type="button" onClick={() => prev()}>prev</button>
|
|
<button type="button" onClick={() => close()}>close</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function renderPlayer() {
|
|
return render(
|
|
<PlayerProvider>
|
|
<Probe />
|
|
</PlayerProvider>
|
|
)
|
|
}
|
|
|
|
describe('PlayerProvider', () => {
|
|
it('loads a queue, stamps played, starts at track 1', async () => {
|
|
vi.mocked(api.getAlbumTracks).mockResolvedValue({ id: 'a1', title: 'Motion', artist: 'TCO', tracks } as never)
|
|
vi.mocked(api.markPlayed).mockResolvedValue({ ok: true } as never)
|
|
renderPlayer()
|
|
await userEvent.click(screen.getByRole('button', { name: 'load' }))
|
|
await waitFor(() => expect(screen.getByText('phase:playing')).toBeTruthy())
|
|
expect(screen.getByText('track:Overture')).toBeTruthy()
|
|
expect(api.getAlbumTracks).toHaveBeenCalledWith('a1')
|
|
expect(api.markPlayed).toHaveBeenCalledWith('a1')
|
|
})
|
|
|
|
it('toggle pauses and resumes', async () => {
|
|
vi.mocked(api.getAlbumTracks).mockResolvedValue({ id: 'a1', title: 'Motion', artist: 'TCO', tracks } as never)
|
|
vi.mocked(api.markPlayed).mockResolvedValue({ ok: true } as never)
|
|
renderPlayer()
|
|
await userEvent.click(screen.getByRole('button', { name: 'load' }))
|
|
await waitFor(() => expect(screen.getByText('phase:playing')).toBeTruthy())
|
|
await userEvent.click(screen.getByRole('button', { name: 'toggle' }))
|
|
expect(screen.getByText('phase:paused')).toBeTruthy()
|
|
await userEvent.click(screen.getByRole('button', { name: 'toggle' }))
|
|
expect(screen.getByText('phase:playing')).toBeTruthy()
|
|
})
|
|
|
|
it('next/prev move through the queue and stop at the edges', async () => {
|
|
vi.mocked(api.getAlbumTracks).mockResolvedValue({ id: 'a1', title: 'Motion', artist: 'TCO', tracks } as never)
|
|
vi.mocked(api.markPlayed).mockResolvedValue({ ok: true } as never)
|
|
renderPlayer()
|
|
await userEvent.click(screen.getByRole('button', { name: 'load' }))
|
|
await waitFor(() => expect(screen.getByText('track:Overture')).toBeTruthy())
|
|
await userEvent.click(screen.getByRole('button', { name: 'next' }))
|
|
expect(screen.getByText('track:Theme de Yoyo')).toBeTruthy()
|
|
await userEvent.click(screen.getByRole('button', { name: 'next' }))
|
|
expect(screen.getByText('track:Theme de Yoyo')).toBeTruthy() // last track: no advance
|
|
await userEvent.click(screen.getByRole('button', { name: 'prev' }))
|
|
expect(screen.getByText('track:Overture')).toBeTruthy()
|
|
await userEvent.click(screen.getByRole('button', { name: 'prev' }))
|
|
expect(screen.getByText('track:Overture')).toBeTruthy() // first track: no rewind
|
|
})
|
|
|
|
it('close empties the player', async () => {
|
|
vi.mocked(api.getAlbumTracks).mockResolvedValue({ id: 'a1', title: 'Motion', artist: 'TCO', tracks } as never)
|
|
vi.mocked(api.markPlayed).mockResolvedValue({ ok: true } as never)
|
|
renderPlayer()
|
|
await userEvent.click(screen.getByRole('button', { name: 'load' }))
|
|
await waitFor(() => expect(screen.getByText('phase:playing')).toBeTruthy())
|
|
await userEvent.click(screen.getByRole('button', { name: 'close' }))
|
|
expect(screen.getByText('phase:empty')).toBeTruthy()
|
|
})
|
|
|
|
it('audio error marks the track failed and skips to the next', async () => {
|
|
vi.mocked(api.getAlbumTracks).mockResolvedValue({ id: 'a1', title: 'Motion', artist: 'TCO', tracks } as never)
|
|
vi.mocked(api.markPlayed).mockResolvedValue({ ok: true } as never)
|
|
renderPlayer()
|
|
await userEvent.click(screen.getByRole('button', { name: 'load' }))
|
|
await waitFor(() => expect(screen.getByText('track:Overture')).toBeTruthy())
|
|
act(() => {
|
|
document.querySelector('audio')!.dispatchEvent(new Event('error'))
|
|
})
|
|
await waitFor(() => expect(screen.getByText('track:Theme de Yoyo')).toBeTruthy())
|
|
})
|
|
})
|