Files
minstrel/web/src/routes/page.test.ts
T
bvandeusen d67c0de596 refactor(playlists): #411 R2 — generic registry-driven system endpoints
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>
2026-05-15 13:21:09 -04:00

113 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});