diff --git a/web/src/lib/components/QueueDrawer.svelte b/web/src/lib/components/QueueDrawer.svelte new file mode 100644 index 00000000..f0e1bf58 --- /dev/null +++ b/web/src/lib/components/QueueDrawer.svelte @@ -0,0 +1,60 @@ + + +{#if player.queueDrawerOpen} + +{/if} + + diff --git a/web/src/lib/components/QueueDrawer.test.ts b/web/src/lib/components/QueueDrawer.test.ts new file mode 100644 index 00000000..17107e96 --- /dev/null +++ b/web/src/lib/components/QueueDrawer.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/svelte'; +import QueueDrawer from './QueueDrawer.svelte'; +import type { TrackRef } from '$lib/api/types'; + +const closeQueueDrawer = vi.fn(); +let queueValue: TrackRef[] = []; +let indexValue = 0; +let openValue = true; + +vi.mock('$lib/player/store.svelte', () => ({ + closeQueueDrawer: (...args: unknown[]) => closeQueueDrawer(...args), + player: { + get queue() { return queueValue; }, + get index() { return indexValue; }, + get queueDrawerOpen() { return openValue; } + }, + // QueueTrackRow imports these from the store; provide stubs so its + // module-load doesn't break when QueueDrawer renders rows. + playFromQueueIndex: vi.fn(), + removeFromQueue: vi.fn(), + moveQueueItem: vi.fn() +})); + +const t = (id: string): TrackRef => ({ + id, + title: `Song ${id}`, + artist_name: `Artist ${id}`, + album_name: 'A', + duration_ms: 200000, + stream_url: `/stream/${id}` +} as TrackRef); + +describe('QueueDrawer', () => { + beforeEach(() => { + closeQueueDrawer.mockClear(); + queueValue = []; + indexValue = 0; + openValue = true; + }); + + it('renders Queue title and track-count summary', () => { + queueValue = [t('a'), t('b'), t('c')]; + render(QueueDrawer); + expect(screen.getByText(/^Queue$/i)).toBeInTheDocument(); + expect(screen.getByText(/3 tracks/i)).toBeInTheDocument(); + }); + + it('renders one row per queue entry', () => { + queueValue = [t('a'), t('b')]; + render(QueueDrawer); + expect(screen.getByText('Song a')).toBeInTheDocument(); + expect(screen.getByText('Song b')).toBeInTheDocument(); + }); + + it('shows empty-state copy when queue is empty', () => { + queueValue = []; + render(QueueDrawer); + expect(screen.getByText(/no tracks queued/i)).toBeInTheDocument(); + }); + + it('clicking the close button calls closeQueueDrawer', async () => { + render(QueueDrawer); + const close = screen.getByLabelText(/close queue/i); + await fireEvent.click(close); + expect(closeQueueDrawer).toHaveBeenCalled(); + }); + + it('clicking the backdrop calls closeQueueDrawer', async () => { + render(QueueDrawer); + const backdrop = screen.getByLabelText(/close queue backdrop/i); + await fireEvent.click(backdrop); + expect(closeQueueDrawer).toHaveBeenCalled(); + }); + + it('drawer is hidden (aria-hidden=true) when queueDrawerOpen is false', () => { + openValue = false; + queueValue = [t('a')]; + render(QueueDrawer); + const aside = screen.getByRole('complementary', { hidden: true }); + expect(aside).toHaveAttribute('aria-hidden', 'true'); + }); +});