feat(web/playlists): Discover refresh button (detail + home kebab)
Operator can refresh their Discover playlist without waiting for the next daily cron tick. Two surfaces, both gated on system_variant === 'discover': - Detail page header: "Refresh" button next to the cover art / meta. - Home Playlists row tile kebab: "Refresh Discover" menu item. Both call POST /api/playlists/system/discover/refresh, show a confirmation toast, and invalidate the playlist + playlists queries so the new content lands without a manual page reload. Extends Playlist.system_variant union with 'discover'. Adds refreshDiscover() typed client. Tests cover client behaviour (success + empty-library) plus conditional rendering on both surfaces.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/svelte';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
||||
import PlaylistCard from './PlaylistCard.svelte';
|
||||
import type { Playlist, PlaylistDetail } from '$lib/api/types';
|
||||
|
||||
@@ -7,6 +7,14 @@ vi.mock('$lib/auth/store.svelte', () => ({
|
||||
user: { value: { id: 'u-self', username: 'me', is_admin: false } }
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/svelte-query', async (orig) => {
|
||||
const actual = (await orig()) as Record<string, unknown>;
|
||||
return {
|
||||
...actual,
|
||||
useQueryClient: () => ({ invalidateQueries: vi.fn().mockResolvedValue(undefined) })
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('$lib/api/playlists', () => ({
|
||||
getPlaylist: vi.fn().mockResolvedValue({
|
||||
id: 'p-1',
|
||||
@@ -37,13 +45,21 @@ vi.mock('$lib/api/playlists', () => ({
|
||||
added_at: '2026-01-01T00:00:00Z'
|
||||
}
|
||||
]
|
||||
} as PlaylistDetail)
|
||||
} as PlaylistDetail),
|
||||
refreshDiscover: vi.fn().mockResolvedValue({ playlist_id: 'p-1', track_count: 5 })
|
||||
}));
|
||||
|
||||
vi.mock('$lib/player/store.svelte', () => ({
|
||||
playQueue: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('$lib/api/queries', () => ({
|
||||
qk: {
|
||||
playlist: (id: string) => ['playlist', id],
|
||||
playlists: (kind?: string) => ['playlists', { kind: kind ?? 'user' }]
|
||||
}
|
||||
}));
|
||||
|
||||
const base: Playlist = {
|
||||
id: 'p-1',
|
||||
user_id: 'u-self',
|
||||
@@ -133,4 +149,71 @@ describe('PlaylistCard', () => {
|
||||
expect(getPlaylist).toHaveBeenCalledWith('p-1');
|
||||
expect(playQueue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('shows kebab button only on Discover playlists', () => {
|
||||
const discoverPlaylist: Playlist = {
|
||||
...base,
|
||||
kind: 'system',
|
||||
system_variant: 'discover',
|
||||
name: 'Discover'
|
||||
};
|
||||
render(PlaylistCard, { props: { playlist: discoverPlaylist } });
|
||||
expect(screen.getByLabelText(/playlist actions/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not show kebab button on for_you playlists', () => {
|
||||
const forYouPlaylist: Playlist = {
|
||||
...base,
|
||||
kind: 'system',
|
||||
system_variant: 'for_you',
|
||||
name: 'For You'
|
||||
};
|
||||
render(PlaylistCard, { props: { playlist: forYouPlaylist } });
|
||||
expect(screen.queryByLabelText(/playlist actions/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not show kebab button on user playlists', () => {
|
||||
render(PlaylistCard, { props: { playlist: base } });
|
||||
expect(screen.queryByLabelText(/playlist actions/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Refresh Discover item appears in kebab menu when opened', async () => {
|
||||
const discoverPlaylist: Playlist = {
|
||||
...base,
|
||||
kind: 'system',
|
||||
system_variant: 'discover',
|
||||
name: 'Discover'
|
||||
};
|
||||
render(PlaylistCard, { props: { playlist: discoverPlaylist } });
|
||||
const kebabBtn = screen.getByLabelText(/playlist actions/i);
|
||||
await fireEvent.click(kebabBtn);
|
||||
expect(screen.getByRole('menuitem', { name: /refresh discover/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Refresh Discover item is absent from for_you card kebab', () => {
|
||||
const forYouPlaylist: Playlist = {
|
||||
...base,
|
||||
kind: 'system',
|
||||
system_variant: 'for_you',
|
||||
name: 'For You'
|
||||
};
|
||||
render(PlaylistCard, { props: { playlist: forYouPlaylist } });
|
||||
expect(screen.queryByRole('menuitem', { name: /refresh discover/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('clicking Refresh Discover calls refreshDiscover', async () => {
|
||||
const { refreshDiscover } = await import('$lib/api/playlists');
|
||||
const discoverPlaylist: Playlist = {
|
||||
...base,
|
||||
kind: 'system',
|
||||
system_variant: 'discover',
|
||||
name: 'Discover'
|
||||
};
|
||||
render(PlaylistCard, { props: { playlist: discoverPlaylist } });
|
||||
const kebabBtn = screen.getByLabelText(/playlist actions/i);
|
||||
await fireEvent.click(kebabBtn);
|
||||
const refreshItem = screen.getByRole('menuitem', { name: /refresh discover/i });
|
||||
await fireEvent.click(refreshItem);
|
||||
await waitFor(() => expect(refreshDiscover).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user