feat(web): /playlists index page (M7 #352 slice 1)
Replaces the placeholder route. Two sections: "Your playlists" (owned)
and "From other users" (public). Inline create form in the header
with Enter-to-submit / Esc-to-cancel. Empty-state copy when the
operator has nothing yet. Routes to /playlists/{id} on card click via
PlaylistCard.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
||||
import { mockQuery } from '../../test-utils/query';
|
||||
import type { Playlist } from '$lib/api/types';
|
||||
|
||||
vi.mock('$lib/api/playlists', () => ({
|
||||
createPlaylistsQuery: vi.fn(),
|
||||
createPlaylist: vi.fn().mockResolvedValue({ id: 'p-new', name: 'X' })
|
||||
}));
|
||||
|
||||
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('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
|
||||
import PlaylistsPage from './+page.svelte';
|
||||
import { createPlaylistsQuery, createPlaylist } from '$lib/api/playlists';
|
||||
|
||||
const mockedCreatePlaylistsQuery = createPlaylistsQuery as ReturnType<typeof vi.fn>;
|
||||
const mockedCreatePlaylist = createPlaylist as ReturnType<typeof vi.fn>;
|
||||
|
||||
function p(over: Partial<Playlist>): Playlist {
|
||||
return {
|
||||
id: 'p1', user_id: 'u-self', owner_username: 'me', name: 'A',
|
||||
description: '', is_public: false, cover_url: '', track_count: 0, duration_sec: 0,
|
||||
created_at: '', updated_at: '', ...over
|
||||
};
|
||||
}
|
||||
|
||||
describe('Playlists index page', () => {
|
||||
test('renders own + public sections', () => {
|
||||
mockedCreatePlaylistsQuery.mockReturnValue(mockQuery({
|
||||
data: {
|
||||
owned: [p({ id: 'p1', name: 'Mine' })],
|
||||
public: [p({ id: 'p2', name: 'Theirs', user_id: 'u-other', owner_username: 'bob' })]
|
||||
}
|
||||
}));
|
||||
render(PlaylistsPage);
|
||||
expect(screen.getByText('Mine')).toBeInTheDocument();
|
||||
expect(screen.getByText('Theirs')).toBeInTheDocument();
|
||||
expect(screen.getByText(/your playlists/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/from other users/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('empty-state copy when operator has no playlists', () => {
|
||||
mockedCreatePlaylistsQuery.mockReturnValue(mockQuery({
|
||||
data: { owned: [], public: [] }
|
||||
}));
|
||||
render(PlaylistsPage);
|
||||
expect(screen.getByText(/no playlists yet/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hides "From other users" section when empty', () => {
|
||||
mockedCreatePlaylistsQuery.mockReturnValue(mockQuery({
|
||||
data: { owned: [p({ id: 'p1', name: 'Mine' })], public: [] }
|
||||
}));
|
||||
render(PlaylistsPage);
|
||||
expect(screen.queryByText(/from other users/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Create button reveals inline form; submit creates and navigates', async () => {
|
||||
mockedCreatePlaylistsQuery.mockReturnValue(mockQuery({ data: { owned: [], public: [] } }));
|
||||
render(PlaylistsPage);
|
||||
await fireEvent.click(screen.getByRole('button', { name: /new playlist/i }));
|
||||
const input = screen.getByPlaceholderText(/saturday morning/i);
|
||||
await fireEvent.input(input, { target: { value: 'Test' } });
|
||||
await fireEvent.click(screen.getByRole('button', { name: /^create$/i }));
|
||||
await waitFor(() => expect(mockedCreatePlaylist).toHaveBeenCalledWith({ name: 'Test' }));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user