d67c0de596
Replaces the per-kind refresh/shuffle handlers with one generic
pair driven off the kind registry, in lockstep across both clients.
Server:
- systemPlaylistKind gains Singleton; RefreshableSystemKind(key)
exported. for_you/discover singleton; songs_like_artist not.
- New generic POST /api/playlists/system/{kind}/refresh and
GET /api/playlists/system/{kind}/shuffle ({kind} = raw
system_variant). Non-singleton/unknown kind → 404. Deleted
playlists_{foryou,discover}_refresh.go and the per-kind shuffle
wrappers; serveSystemPlaylistShuffle core kept.
- playlistRowView.refreshable: server-derived flag so clients show
the refresh affordance generically without hardcoding kinds.
Web (not drift-cached → uses the server flag):
- refreshSystem(variant) replaces refreshForYou/refreshDiscover;
systemShuffle drops the for_you→for-you mapping (raw variant).
- PlaylistCard + detail page gate the kebab/Refresh button on
playlist.refreshable; label is "Refresh {name}". Tests reworked;
obsolete refresh-foryou/discover api tests deleted.
Flutter (list tiles are drift-cache-sourced → derive, no migration):
- Playlist.refreshable getter = isSystem && variant !=
songs_like_artist (holds for all current + planned kinds).
- refreshSystem/systemShuffle use the raw variant; PlaylistCard +
detail screen gate kebab/Regenerate/source-tagging on refreshable
so songs_like_artist plays via get() (no by-kind endpoint).
Pure-plumbing refactor; CI verifies parity. Next (R3): the five
discovery mixes — each a candidate query + one registry entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||
import { render, screen } from '@testing-library/svelte';
|
||
import { readable } from 'svelte/store';
|
||
import { emptyLikesMock } from '../test-utils/mocks/likes';
|
||
import type { Playlist } from '$lib/api/types';
|
||
|
||
// Mock all the queries the home page constructs.
|
||
vi.mock('$lib/api/home', () => ({
|
||
createHomeQuery: () =>
|
||
readable({
|
||
data: {
|
||
recently_added_albums: [],
|
||
most_played_tracks: [],
|
||
rediscover_albums: [],
|
||
rediscover_artists: [],
|
||
last_played_artists: []
|
||
},
|
||
isPending: false,
|
||
isError: false,
|
||
refetch: vi.fn()
|
||
})
|
||
}));
|
||
|
||
vi.mock('$lib/api/playlists', () => ({
|
||
createPlaylistsQuery: vi.fn()
|
||
}));
|
||
|
||
vi.mock('$lib/api/me', () => ({
|
||
createSystemPlaylistsStatusQuery: vi.fn()
|
||
}));
|
||
|
||
// LikeButton imports — needed because the home renders cards that may use it.
|
||
vi.mock('$lib/api/likes', () => emptyLikesMock());
|
||
|
||
import Page from './+page.svelte';
|
||
import { createPlaylistsQuery } from '$lib/api/playlists';
|
||
import { createSystemPlaylistsStatusQuery } from '$lib/api/me';
|
||
|
||
const emptyPlaylistsResponse = { owned: [], public: [] };
|
||
|
||
beforeEach(() => {
|
||
(createPlaylistsQuery as unknown as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||
readable({ data: emptyPlaylistsResponse, isPending: false, isError: false })
|
||
);
|
||
(createSystemPlaylistsStatusQuery as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
|
||
readable({
|
||
data: { in_flight: false, last_run_at: null, last_error: null },
|
||
isPending: false,
|
||
isError: false
|
||
})
|
||
);
|
||
});
|
||
|
||
afterEach(() => vi.clearAllMocks());
|
||
|
||
describe('home Playlists section', () => {
|
||
test('renders 5 placeholder cards when no playlists exist', () => {
|
||
// Slot layout: For-You, Discover, 3× Songs-like.
|
||
render(Page);
|
||
const placeholders = screen.queryAllByTestId('playlist-placeholder-card');
|
||
expect(placeholders).toHaveLength(5);
|
||
});
|
||
|
||
test('building status sets variant=building on placeholders', () => {
|
||
(createSystemPlaylistsStatusQuery as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
|
||
readable({
|
||
data: { in_flight: true, last_run_at: null, last_error: null },
|
||
isPending: false,
|
||
isError: false
|
||
})
|
||
);
|
||
const { container } = render(Page);
|
||
const buildingPlaceholders = container.querySelectorAll('[data-variant="building"]');
|
||
expect(buildingPlaceholders.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('renders For-You playlist + 3 placeholders when only for_you exists', () => {
|
||
const forYou: Playlist = {
|
||
id: 'fy',
|
||
user_id: 'u1',
|
||
owner_username: 'u1',
|
||
name: 'For You',
|
||
description: '',
|
||
is_public: false,
|
||
kind: 'system',
|
||
system_variant: 'for_you',
|
||
refreshable: false,
|
||
seed_artist_id: null,
|
||
cover_url: '',
|
||
track_count: 25,
|
||
duration_sec: 1500,
|
||
created_at: '2026-05-04T00:00:00Z',
|
||
updated_at: '2026-05-04T00:00:00Z'
|
||
};
|
||
(createPlaylistsQuery as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||
(kind?: string) =>
|
||
kind === 'system'
|
||
? readable({
|
||
data: { owned: [forYou], public: [] },
|
||
isPending: false,
|
||
isError: false
|
||
})
|
||
: readable({ data: emptyPlaylistsResponse, isPending: false, isError: false })
|
||
);
|
||
render(Page);
|
||
expect(screen.getByText('For You')).toBeInTheDocument();
|
||
const placeholders = screen.queryAllByTestId('playlist-placeholder-card');
|
||
// 1 Discover + 3 Songs-like slots = 4 placeholders alongside the
|
||
// real For-You tile.
|
||
expect(placeholders).toHaveLength(4);
|
||
});
|
||
});
|