diff --git a/web/src/lib/components/PlayerBar.svelte b/web/src/lib/components/PlayerBar.svelte index e2e593b3..f6f78e56 100644 --- a/web/src/lib/components/PlayerBar.svelte +++ b/web/src/lib/components/PlayerBar.svelte @@ -7,6 +7,7 @@ import { formatDuration } from '$lib/media/duration'; import { FALLBACK_COVER } from '$lib/media/covers'; import LikeButton from './LikeButton.svelte'; + import TrackMenu from './TrackMenu.svelte'; const current = $derived(player.current); @@ -59,6 +60,7 @@ + diff --git a/web/src/lib/components/PlayerBar.test.ts b/web/src/lib/components/PlayerBar.test.ts index 67c31e02..f36fbf5a 100644 --- a/web/src/lib/components/PlayerBar.test.ts +++ b/web/src/lib/components/PlayerBar.test.ts @@ -51,6 +51,13 @@ vi.mock('$lib/api/likes', () => ({ unlikeEntity: vi.fn() })); +vi.mock('$lib/api/quarantine', () => ({ + flagTrack: vi.fn(), + unflagTrack: vi.fn(), + listMyQuarantine: vi.fn().mockResolvedValue([]), + createMyQuarantineQuery: vi.fn() +})); + vi.mock('@tanstack/svelte-query', async (orig) => { const actual = (await orig()) as Record; return { ...actual, useQueryClient: () => ({}) }; @@ -174,4 +181,11 @@ describe('PlayerBar', () => { expect(screen.getByText('1:05')).toBeInTheDocument(); expect(screen.getByText('4:05')).toBeInTheDocument(); }); + + test('renders the TrackMenu kebab button when a track is current', () => { + render(PlayerBar); + expect( + screen.getByRole('button', { name: /track actions for/i }) + ).toBeInTheDocument(); + }); }); diff --git a/web/src/lib/components/TrackRow.svelte b/web/src/lib/components/TrackRow.svelte index eeafee1e..8d558708 100644 --- a/web/src/lib/components/TrackRow.svelte +++ b/web/src/lib/components/TrackRow.svelte @@ -3,6 +3,7 @@ import { formatDuration } from '$lib/media/duration'; import { playQueue, enqueueTrack, playRadio } from '$lib/player/store.svelte'; import LikeButton from './LikeButton.svelte'; + import TrackMenu from './TrackMenu.svelte'; type PlayHandler = (tracks: TrackRef[], index: number) => void; @@ -47,7 +48,7 @@ aria-label={track.title} onclick={onRowClick} onkeydown={onRowKey} - class="grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent" + class="grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent" > {track.track_number ?? '—'} @@ -67,5 +68,6 @@ onclick={onAddClick} class="rounded p-1 text-text-secondary hover:text-text-primary" >+ + {formatDuration(track.duration_sec)} diff --git a/web/src/lib/components/TrackRow.test.ts b/web/src/lib/components/TrackRow.test.ts index b1693021..0b54c548 100644 --- a/web/src/lib/components/TrackRow.test.ts +++ b/web/src/lib/components/TrackRow.test.ts @@ -19,6 +19,13 @@ vi.mock('$lib/api/likes', () => ({ unlikeEntity: vi.fn() })); +vi.mock('$lib/api/quarantine', () => ({ + flagTrack: vi.fn(), + unflagTrack: vi.fn(), + listMyQuarantine: vi.fn().mockResolvedValue([]), + createMyQuarantineQuery: vi.fn() +})); + vi.mock('@tanstack/svelte-query', async (orig) => { const actual = (await orig()) as Record; return { ...actual, useQueryClient: () => ({}) }; @@ -62,26 +69,26 @@ describe('TrackRow', () => { test('clicking the row calls playQueue(tracks, index)', async () => { render(TrackRow, { props: { tracks, index: 1 } }); - await fireEvent.click(screen.getByRole('button', { name: /Freddie Freeloader/ })); + await fireEvent.click(screen.getByRole('button', { name: 'Freddie Freeloader' })); expect(playQueue).toHaveBeenCalledWith(tracks, 1); }); test('Enter on the row activates play', async () => { render(TrackRow, { props: { tracks, index: 0 } }); - await fireEvent.keyDown(screen.getByRole('button', { name: /So What/ }), { key: 'Enter' }); + await fireEvent.keyDown(screen.getByRole('button', { name: 'So What' }), { key: 'Enter' }); expect(playQueue).toHaveBeenCalledWith(tracks, 0); }); test('Space on the row activates play', async () => { render(TrackRow, { props: { tracks, index: 0 } }); - await fireEvent.keyDown(screen.getByRole('button', { name: /So What/ }), { key: ' ' }); + await fireEvent.keyDown(screen.getByRole('button', { name: 'So What' }), { key: ' ' }); expect(playQueue).toHaveBeenCalledWith(tracks, 0); }); test('onPlay prop overrides default playQueue', async () => { const onPlay = vi.fn(); render(TrackRow, { props: { tracks, index: 1, onPlay } }); - await fireEvent.click(screen.getByRole('button', { name: /Freddie Freeloader/ })); + await fireEvent.click(screen.getByRole('button', { name: 'Freddie Freeloader' })); expect(onPlay).toHaveBeenCalledWith(tracks, 1); expect(playQueue).not.toHaveBeenCalled(); }); @@ -99,4 +106,11 @@ describe('TrackRow', () => { expect(playRadio).toHaveBeenCalledWith('t1'); expect(playQueue).not.toHaveBeenCalled(); }); + + test('renders the TrackMenu kebab button', () => { + render(TrackRow, { props: { tracks, index: 0 } }); + expect( + screen.getByRole('button', { name: /track actions for/i }) + ).toBeInTheDocument(); + }); });