From 57a353a1381fabfb602e4526073fdc99651e26b2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 3 May 2026 21:41:59 -0400 Subject: [PATCH] feat(web/m7-364): QueueTrackRow with neodrag reorder + remove --- web/src/lib/components/QueueTrackRow.svelte | 71 ++++++++++++++++++++ web/src/lib/components/QueueTrackRow.test.ts | 64 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 web/src/lib/components/QueueTrackRow.svelte create mode 100644 web/src/lib/components/QueueTrackRow.test.ts 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(); + }); +});