diff --git a/web/src/lib/components/AlbumCard.svelte b/web/src/lib/components/AlbumCard.svelte index 828daa79..4eeb0aed 100644 --- a/web/src/lib/components/AlbumCard.svelte +++ b/web/src/lib/components/AlbumCard.svelte @@ -3,6 +3,7 @@ import { FALLBACK_COVER } from '$lib/media/covers'; import { api } from '$lib/api/client'; import { enqueueTracks } from '$lib/player/store.svelte'; + import LikeButton from './LikeButton.svelte'; let { album }: { album: AlbumRef } = $props(); @@ -35,10 +36,13 @@
{album.year}
{/if} - +
+ + +
diff --git a/web/src/lib/components/AlbumCard.test.ts b/web/src/lib/components/AlbumCard.test.ts index 1f512b6a..6a95cbd0 100644 --- a/web/src/lib/components/AlbumCard.test.ts +++ b/web/src/lib/components/AlbumCard.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; import type { AlbumRef, AlbumDetail, TrackRef } from '$lib/api/types'; import { FALLBACK_COVER } from '$lib/media/covers'; +import { readable } from 'svelte/store'; vi.mock('$lib/api/client', () => ({ api: { get: vi.fn() } @@ -10,6 +11,21 @@ vi.mock('$lib/player/store.svelte', () => ({ enqueueTracks: vi.fn() })); +vi.mock('$lib/api/likes', () => ({ + createLikedIdsQuery: () => readable({ + data: { track_ids: [], album_ids: [], artist_ids: [] }, + isPending: false, + isError: false + }), + likeEntity: vi.fn(), + unlikeEntity: vi.fn() +})); + +vi.mock('@tanstack/svelte-query', async (orig) => { + const actual = (await orig()) as Record; + return { ...actual, useQueryClient: () => ({}) }; +}); + import AlbumCard from './AlbumCard.svelte'; import { api } from '$lib/api/client'; import { enqueueTracks } from '$lib/player/store.svelte'; diff --git a/web/src/lib/components/ArtistRow.svelte b/web/src/lib/components/ArtistRow.svelte index 92aee7ad..33fd5698 100644 --- a/web/src/lib/components/ArtistRow.svelte +++ b/web/src/lib/components/ArtistRow.svelte @@ -1,5 +1,6 @@ - +
+ - {artist.name} - {countLabel} - - + {artist.name} + {countLabel} + + + + +
diff --git a/web/src/lib/components/ArtistRow.test.ts b/web/src/lib/components/ArtistRow.test.ts index 5dec7fe4..333b78c2 100644 --- a/web/src/lib/components/ArtistRow.test.ts +++ b/web/src/lib/components/ArtistRow.test.ts @@ -1,8 +1,24 @@ -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; +import { readable } from 'svelte/store'; import ArtistRow from './ArtistRow.svelte'; import type { ArtistRef } from '$lib/api/types'; +vi.mock('$lib/api/likes', () => ({ + createLikedIdsQuery: () => readable({ + data: { track_ids: [], album_ids: [], artist_ids: [] }, + isPending: false, + isError: false + }), + likeEntity: vi.fn(), + unlikeEntity: vi.fn() +})); + +vi.mock('@tanstack/svelte-query', async (orig) => { + const actual = (await orig()) as Record; + return { ...actual, useQueryClient: () => ({}) }; +}); + const artist: ArtistRef = { id: 'abc', name: 'alice', album_count: 12 }; describe('ArtistRow', () => { @@ -11,14 +27,14 @@ describe('ArtistRow', () => { const link = screen.getByRole('link'); expect(link).toHaveAttribute('href', '/artists/abc'); - expect(link).toHaveTextContent('alice'); - expect(link).toHaveTextContent('12 albums'); - expect(link).toHaveTextContent('A'); // initial letter + expect(screen.getByText('alice')).toBeInTheDocument(); + expect(screen.getByText('12 albums')).toBeInTheDocument(); + expect(screen.getByText('A')).toBeInTheDocument(); }); test('singular "1 album" when album_count is 1', () => { render(ArtistRow, { props: { artist: { ...artist, album_count: 1 } } }); - expect(screen.getByRole('link')).toHaveTextContent('1 album'); + expect(screen.getByText('1 album')).toBeInTheDocument(); }); test('empty name still renders a safe initial', () => { diff --git a/web/src/lib/components/PlayerBar.svelte b/web/src/lib/components/PlayerBar.svelte index a35c8932..e2e593b3 100644 --- a/web/src/lib/components/PlayerBar.svelte +++ b/web/src/lib/components/PlayerBar.svelte @@ -6,6 +6,7 @@ } from '$lib/player/store.svelte'; import { formatDuration } from '$lib/media/duration'; import { FALLBACK_COVER } from '$lib/media/covers'; + import LikeButton from './LikeButton.svelte'; const current = $derived(player.current); @@ -48,7 +49,7 @@ onerror={onCoverError} /> -
+
{current.title}
+
diff --git a/web/src/lib/components/PlayerBar.test.ts b/web/src/lib/components/PlayerBar.test.ts index e817aa64..67c31e02 100644 --- a/web/src/lib/components/PlayerBar.test.ts +++ b/web/src/lib/components/PlayerBar.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; import type { TrackRef } from '$lib/api/types'; +import { readable } from 'svelte/store'; // Mutable state the mocked store reads from. const state = vi.hoisted(() => ({ @@ -40,6 +41,21 @@ vi.mock('$lib/player/store.svelte', () => ({ playQueue: vi.fn() })); +vi.mock('$lib/api/likes', () => ({ + createLikedIdsQuery: () => readable({ + data: { track_ids: [], album_ids: [], artist_ids: [] }, + isPending: false, + isError: false + }), + likeEntity: vi.fn(), + unlikeEntity: vi.fn() +})); + +vi.mock('@tanstack/svelte-query', async (orig) => { + const actual = (await orig()) as Record; + return { ...actual, useQueryClient: () => ({}) }; +}); + import PlayerBar from './PlayerBar.svelte'; import { togglePlay, skipNext, skipPrev, seekTo, setVolume, diff --git a/web/src/lib/components/Shell.svelte b/web/src/lib/components/Shell.svelte index e19df1f6..8a81e8a8 100644 --- a/web/src/lib/components/Shell.svelte +++ b/web/src/lib/components/Shell.svelte @@ -22,9 +22,10 @@ } const navItems = [ - { href: '/', label: 'Library' }, - { href: '/search', label: 'Search' }, - { href: '/playlists', label: 'Playlists' } + { href: '/', label: 'Library' }, + { href: '/library/liked', label: 'Liked' }, + { href: '/search', label: 'Search' }, + { href: '/playlists', label: 'Playlists' } ]; diff --git a/web/src/lib/components/TrackRow.svelte b/web/src/lib/components/TrackRow.svelte index 5cad7134..68a76432 100644 --- a/web/src/lib/components/TrackRow.svelte +++ b/web/src/lib/components/TrackRow.svelte @@ -2,6 +2,7 @@ import type { TrackRef } from '$lib/api/types'; import { formatDuration } from '$lib/media/duration'; import { playQueue, enqueueTrack } from '$lib/player/store.svelte'; + import LikeButton from './LikeButton.svelte'; type PlayHandler = (tracks: TrackRef[], index: number) => void; @@ -41,12 +42,13 @@ aria-label={track.title} onclick={onRowClick} onkeydown={onRowKey} - class="grid w-full cursor-pointer grid-cols-[32px_1fr_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] 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 ?? '—'} {track.title} +