feat(web): extend ArtistRef/AlbumRef + add HomePayload + new query keys
Adds sort_name/cover_url to ArtistRef, sort_title to AlbumRef (matching backend Task 5 wire shape), HomePayload type, and qk.home/albumsAlpha/ artistTracks query keys. Updates existing test fixtures to satisfy the new required fields. Verified clean via 'npm run check' (0 errors, only pre-existing warnings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,3 +30,11 @@ describe('qk search keys', () => {
|
||||
expect(qk.searchTracks('foo')).toEqual(['searchTracks', { q: 'foo' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('qk M6a keys', () => {
|
||||
test('M6a query keys are stable', () => {
|
||||
expect(qk.home()).toEqual(['home']);
|
||||
expect(qk.albumsAlpha()).toEqual(['albumsAlpha']);
|
||||
expect(qk.artistTracks('a1')).toEqual(['artistTracks', 'a1']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,9 @@ export const qk = {
|
||||
['adminQuarantineActions', { limit: limit ?? 50 }] as const,
|
||||
suggestions: (limit?: number) =>
|
||||
['suggestions', { limit: limit ?? 12 }] as const,
|
||||
home: () => ['home'] as const,
|
||||
albumsAlpha: () => ['albumsAlpha'] as const,
|
||||
artistTracks: (artistId: string) => ['artistTracks', artistId] as const,
|
||||
};
|
||||
|
||||
export function createArtistsQuery(sort: ArtistSort) {
|
||||
|
||||
@@ -12,12 +12,15 @@ export type Page<T> = {
|
||||
export type ArtistRef = {
|
||||
id: string;
|
||||
name: string;
|
||||
sort_name: string;
|
||||
album_count: number;
|
||||
cover_url: string; // "" when no representative album cover
|
||||
};
|
||||
|
||||
export type AlbumRef = {
|
||||
id: string;
|
||||
title: string;
|
||||
sort_title: string;
|
||||
artist_id: string;
|
||||
artist_name: string;
|
||||
year?: number;
|
||||
@@ -236,3 +239,13 @@ export type ArtistSuggestion = {
|
||||
score: number;
|
||||
attribution: SeedContribution[]; // up to 3 entries, ordered by contribution DESC
|
||||
};
|
||||
|
||||
// Mirrors internal/api/types.go HomePayload. All slices are non-null
|
||||
// per the server contract — empty sections render as [].
|
||||
export type HomePayload = {
|
||||
recently_added_albums: AlbumRef[];
|
||||
rediscover_albums: AlbumRef[];
|
||||
rediscover_artists: ArtistRef[];
|
||||
most_played_tracks: TrackRef[];
|
||||
last_played_artists: ArtistRef[];
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ import { enqueueTracks } from '$lib/player/store.svelte';
|
||||
const album: AlbumRef = {
|
||||
id: 'xyz',
|
||||
title: 'Kind of Blue',
|
||||
sort_title: 'Kind of Blue',
|
||||
artist_id: 'm-davis',
|
||||
artist_name: 'Miles Davis',
|
||||
year: 1959,
|
||||
|
||||
@@ -19,7 +19,7 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
|
||||
return { ...actual, useQueryClient: () => ({}) };
|
||||
});
|
||||
|
||||
const artist: ArtistRef = { id: 'abc', name: 'alice', album_count: 12 };
|
||||
const artist: ArtistRef = { id: 'abc', name: 'alice', sort_name: 'alice', album_count: 12, cover_url: '' };
|
||||
|
||||
describe('ArtistRow', () => {
|
||||
test('renders initial, name, album count inside a link to /artists/:id', () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ afterEach(() => {
|
||||
describe('album detail page', () => {
|
||||
test('renders hero metadata + one TrackRow per track', () => {
|
||||
const detail: AlbumDetail = {
|
||||
id: 'xyz', title: 'Kind of Blue',
|
||||
id: 'xyz', title: 'Kind of Blue', sort_title: 'Kind of Blue',
|
||||
artist_id: 'md', artist_name: 'Miles Davis',
|
||||
year: 1959, track_count: 2, duration_sec: 544 + 565,
|
||||
cover_url: '/api/albums/xyz/cover',
|
||||
@@ -83,7 +83,7 @@ describe('album detail page', () => {
|
||||
|
||||
test('back link points to /artists/:artistId with the artist name', () => {
|
||||
const detail: AlbumDetail = {
|
||||
id: 'xyz', title: 'T',
|
||||
id: 'xyz', title: 'T', sort_title: 'T',
|
||||
artist_id: 'md', artist_name: 'Miles Davis',
|
||||
track_count: 0, duration_sec: 0,
|
||||
cover_url: '/api/albums/xyz/cover',
|
||||
|
||||
@@ -50,8 +50,8 @@ afterEach(() => {
|
||||
|
||||
describe('artists list page', () => {
|
||||
test('renders a row per artist', () => {
|
||||
const alice: ArtistRef = { id: 'a', name: 'Alice', album_count: 3 };
|
||||
const bob: ArtistRef = { id: 'b', name: 'Bob', album_count: 1 };
|
||||
const alice: ArtistRef = { id: 'a', name: 'Alice', sort_name: 'Alice', album_count: 3, cover_url: '' };
|
||||
const bob: ArtistRef = { id: 'b', name: 'Bob', sort_name: 'Bob', album_count: 1, cover_url: '' };
|
||||
(createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
|
||||
mockInfiniteQuery({ pages: [page([alice, bob], 2)] })
|
||||
);
|
||||
@@ -84,7 +84,7 @@ describe('artists list page', () => {
|
||||
|
||||
test('"End of library" when hasNextPage is false and at least one page loaded', () => {
|
||||
(createArtistsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
|
||||
mockInfiniteQuery({ pages: [page<ArtistRef>([{ id: 'a', name: 'A', album_count: 1 }], 1)] })
|
||||
mockInfiniteQuery({ pages: [page<ArtistRef>([{ id: 'a', name: 'A', sort_name: 'A', album_count: 1, cover_url: '' }], 1)] })
|
||||
);
|
||||
render(ArtistsPage);
|
||||
expect(screen.getByText(/end of library/i)).toBeInTheDocument();
|
||||
|
||||
@@ -40,6 +40,7 @@ import { createArtistQuery } from '$lib/api/queries';
|
||||
function album(id: string, title: string, year?: number): AlbumRef {
|
||||
return {
|
||||
id, title,
|
||||
sort_title: title,
|
||||
artist_id: 'abc', artist_name: 'Alice',
|
||||
year, track_count: 10, duration_sec: 2400,
|
||||
cover_url: `/api/albums/${id}/cover`
|
||||
@@ -54,7 +55,7 @@ afterEach(() => {
|
||||
describe('artist detail page', () => {
|
||||
test('renders artist name, subtitle, and one AlbumCard per album', () => {
|
||||
const detail: ArtistDetail = {
|
||||
id: 'abc', name: 'Alice', album_count: 2,
|
||||
id: 'abc', name: 'Alice', sort_name: 'Alice', album_count: 2, cover_url: '',
|
||||
albums: [album('a1', 'First', 2020), album('a2', 'Second')]
|
||||
};
|
||||
(createArtistQuery as ReturnType<typeof vi.fn>).mockReturnValue(mockQuery({ data: detail }));
|
||||
@@ -67,7 +68,7 @@ describe('artist detail page', () => {
|
||||
|
||||
test('back link points to Library', () => {
|
||||
(createArtistQuery as ReturnType<typeof vi.fn>).mockReturnValue(mockQuery({
|
||||
data: { id: 'abc', name: 'Alice', album_count: 0, albums: [] }
|
||||
data: { id: 'abc', name: 'Alice', sort_name: 'Alice', album_count: 0, cover_url: '', albums: [] }
|
||||
}));
|
||||
render(ArtistPage);
|
||||
const back = screen.getByRole('link', { name: /library/i });
|
||||
|
||||
@@ -42,9 +42,9 @@ afterEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('liked library page', () => {
|
||||
test('renders three sections when each has items', () => {
|
||||
const ar: ArtistRef = { id: 'a1', name: 'X', album_count: 1 };
|
||||
const ar: ArtistRef = { id: 'a1', name: 'X', sort_name: 'X', album_count: 1, cover_url: '' };
|
||||
const al: AlbumRef = {
|
||||
id: 'al1', title: 'Y', artist_id: 'a1', artist_name: 'X',
|
||||
id: 'al1', title: 'Y', sort_title: 'Y', artist_id: 'a1', artist_name: 'X',
|
||||
year: 2020, track_count: 1, duration_sec: 100, cover_url: '/x'
|
||||
};
|
||||
const tr: TrackRef = {
|
||||
|
||||
@@ -44,7 +44,7 @@ function page<T>(items: T[], total: number, offset = 0, limit = 50): Page<T> {
|
||||
}
|
||||
|
||||
const album: AlbumRef = {
|
||||
id: 'al1', title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis',
|
||||
id: 'al1', title: 'Kind of Blue', sort_title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis',
|
||||
year: 1959, track_count: 5, duration_sec: 2630, cover_url: '/api/albums/al1/cover'
|
||||
};
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ afterEach(() => {
|
||||
|
||||
describe('search artists overflow', () => {
|
||||
test('renders a row per artist', () => {
|
||||
const a1: ArtistRef = { id: 'a1', name: 'Miles Davis', album_count: 12 };
|
||||
const a2: ArtistRef = { id: 'a2', name: 'Miles Mosley', album_count: 1 };
|
||||
const a1: ArtistRef = { id: 'a1', name: 'Miles Davis', sort_name: 'Miles Davis', album_count: 12, cover_url: '' };
|
||||
const a2: ArtistRef = { id: 'a2', name: 'Miles Mosley', sort_name: 'Miles Mosley', album_count: 1, cover_url: '' };
|
||||
(createSearchArtistsInfiniteQuery as ReturnType<typeof vi.fn>).mockReturnValue(
|
||||
mockInfiniteQuery({ pages: [page([a1, a2], 2)] })
|
||||
);
|
||||
@@ -69,7 +69,7 @@ describe('search artists overflow', () => {
|
||||
|
||||
test('Load more is hidden when hasNextPage is false', () => {
|
||||
(createSearchArtistsInfiniteQuery as ReturnType<typeof vi.fn>).mockReturnValue(
|
||||
mockInfiniteQuery({ pages: [page<ArtistRef>([{ id: 'a', name: 'A', album_count: 1 }], 1)] })
|
||||
mockInfiniteQuery({ pages: [page<ArtistRef>([{ id: 'a', name: 'A', sort_name: 'A', album_count: 1, cover_url: '' }], 1)] })
|
||||
);
|
||||
render(ArtistsOverflow);
|
||||
expect(screen.queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
|
||||
|
||||
@@ -53,9 +53,9 @@ function emptyResp(): SearchResponse {
|
||||
};
|
||||
}
|
||||
|
||||
const artist: ArtistRef = { id: 'a1', name: 'Miles Davis', album_count: 12 };
|
||||
const artist: ArtistRef = { id: 'a1', name: 'Miles Davis', sort_name: 'Miles Davis', album_count: 12, cover_url: '' };
|
||||
const album: AlbumRef = {
|
||||
id: 'al1', title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis',
|
||||
id: 'al1', title: 'Kind of Blue', sort_title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis',
|
||||
year: 1959, track_count: 5, duration_sec: 2630, cover_url: '/api/albums/al1/cover'
|
||||
};
|
||||
const track: TrackRef = {
|
||||
|
||||
Reference in New Issue
Block a user