diff --git a/web/src/lib/components/QueueTrackRow.svelte b/web/src/lib/components/QueueTrackRow.svelte new file mode 100644 index 00000000..5fa711a4 --- /dev/null +++ b/web/src/lib/components/QueueTrackRow.svelte @@ -0,0 +1,71 @@ + + +
+ + + + + +
diff --git a/web/src/lib/components/QueueTrackRow.test.ts b/web/src/lib/components/QueueTrackRow.test.ts new file mode 100644 index 00000000..8fc0f127 --- /dev/null +++ b/web/src/lib/components/QueueTrackRow.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/svelte'; +import QueueTrackRow from './QueueTrackRow.svelte'; +import type { TrackRef } from '$lib/api/types'; + +const playFromQueueIndex = vi.fn(); +const removeFromQueue = vi.fn(); +const moveQueueItem = vi.fn(); + +vi.mock('$lib/player/store.svelte', () => ({ + playFromQueueIndex: (...args: unknown[]) => playFromQueueIndex(...args), + removeFromQueue: (...args: unknown[]) => removeFromQueue(...args), + moveQueueItem: (...args: unknown[]) => moveQueueItem(...args) +})); + +const sampleTrack: TrackRef = { + id: 't1', + title: 'Song Title', + artist_name: 'Artist Name', + album_name: 'Album', + duration_ms: 200000, + stream_url: '/stream/t1' +} as TrackRef; + +describe('QueueTrackRow', () => { + beforeEach(() => { + playFromQueueIndex.mockClear(); + removeFromQueue.mockClear(); + moveQueueItem.mockClear(); + }); + + it('renders track title and artist', () => { + render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } }); + expect(screen.getByText('Song Title')).toBeInTheDocument(); + expect(screen.getByText(/Artist Name/)).toBeInTheDocument(); + }); + + it('clicking the body calls playFromQueueIndex with the row index', async () => { + render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } }); + const body = screen.getByRole('button', { name: /play song title/i }); + await fireEvent.click(body); + expect(playFromQueueIndex).toHaveBeenCalledWith(3); + }); + + it('clicking the remove button calls removeFromQueue with the row index', async () => { + render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } }); + const remove = screen.getByLabelText(/remove from queue/i); + await fireEvent.click(remove); + expect(removeFromQueue).toHaveBeenCalledWith(3); + }); + + it('current row shows the "Now playing" chip', () => { + render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: true } }); + expect(screen.getByText(/now playing/i)).toBeInTheDocument(); + }); + + it('current row body click does NOT call playFromQueueIndex', async () => { + render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: true } }); + // The body button has a different aria-label when isCurrent + const body = screen.getByLabelText(/now playing/i); + await fireEvent.click(body); + expect(playFromQueueIndex).not.toHaveBeenCalled(); + }); +});