120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
import { render, screen, fireEvent } from '@testing-library/svelte';
|
|
import { readable } from 'svelte/store';
|
|
import type { PlaylistTrack } from '$lib/api/types';
|
|
|
|
// LikeButton + TrackMenu transitively pull in TanStack Query, the auth
|
|
// store, likes/quarantine APIs, the player store, and SvelteKit
|
|
// navigation. Mock all of them at module level so the component can be
|
|
// rendered in isolation.
|
|
|
|
vi.mock('$lib/auth/store.svelte', () => ({
|
|
user: { value: { id: 'u1', username: 'me', is_admin: false } }
|
|
}));
|
|
|
|
vi.mock('$app/navigation', () => ({
|
|
goto: vi.fn()
|
|
}));
|
|
|
|
vi.mock('$lib/api/likes', () => ({
|
|
createLikedIdsQuery: () =>
|
|
readable({
|
|
data: { track_ids: [], album_ids: [], artist_ids: [] },
|
|
isPending: false,
|
|
isError: false
|
|
}),
|
|
likeEntity: vi.fn().mockResolvedValue(undefined),
|
|
unlikeEntity: vi.fn().mockResolvedValue(undefined)
|
|
}));
|
|
|
|
vi.mock('$lib/api/quarantine', () => ({
|
|
flagTrack: vi.fn().mockResolvedValue({}),
|
|
unflagTrack: vi.fn().mockResolvedValue(undefined),
|
|
listMyQuarantine: vi.fn().mockResolvedValue([]),
|
|
createMyQuarantineQuery: () =>
|
|
readable({ data: [], isPending: false, isError: false })
|
|
}));
|
|
|
|
vi.mock('$lib/api/admin/tracks', () => ({
|
|
removeTrack: vi.fn().mockResolvedValue({ deleted_track_id: 't-1' })
|
|
}));
|
|
|
|
vi.mock('$lib/player/store.svelte', () => ({
|
|
playNext: vi.fn(),
|
|
enqueueTrack: vi.fn(),
|
|
playQueue: vi.fn(),
|
|
playRadio: vi.fn()
|
|
}));
|
|
|
|
import PlaylistTrackRow from './PlaylistTrackRow.svelte';
|
|
|
|
const live: PlaylistTrack = {
|
|
position: 2,
|
|
track_id: 't-1',
|
|
album_id: 'a-1',
|
|
artist_id: 'ar-1',
|
|
title: 'Roygbiv',
|
|
artist_name: 'Boards of Canada',
|
|
album_title: 'MHTRTC',
|
|
duration_sec: 137,
|
|
stream_url: '/api/tracks/t-1/stream',
|
|
added_at: ''
|
|
};
|
|
|
|
const removed: PlaylistTrack = {
|
|
...live,
|
|
track_id: null,
|
|
album_id: null,
|
|
artist_id: null,
|
|
stream_url: null
|
|
};
|
|
|
|
afterEach(() => vi.clearAllMocks());
|
|
|
|
describe('PlaylistTrackRow', () => {
|
|
test('renders title, artist, mm:ss duration', () => {
|
|
render(PlaylistTrackRow, {
|
|
props: { row: live, isOwner: true, onRemove: vi.fn(), onPlay: vi.fn() }
|
|
});
|
|
expect(screen.getByText('Roygbiv')).toBeInTheDocument();
|
|
expect(screen.getByText('2:17')).toBeInTheDocument();
|
|
});
|
|
|
|
test('shows drag handle and remove button for owner', () => {
|
|
render(PlaylistTrackRow, {
|
|
props: { row: live, isOwner: true, onRemove: vi.fn(), onPlay: vi.fn() }
|
|
});
|
|
expect(screen.getByLabelText('Drag handle')).toBeInTheDocument();
|
|
expect(
|
|
screen.getByLabelText(/Remove Roygbiv from playlist/i)
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
test('hides drag handle and remove button for non-owner', () => {
|
|
render(PlaylistTrackRow, {
|
|
props: { row: live, isOwner: false, onRemove: vi.fn(), onPlay: vi.fn() }
|
|
});
|
|
expect(screen.queryByLabelText('Drag handle')).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByLabelText(/Remove Roygbiv from playlist/i)
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('greyed-out + strikethrough when track_id is null', () => {
|
|
render(PlaylistTrackRow, {
|
|
props: { row: removed, isOwner: true, onRemove: vi.fn(), onPlay: vi.fn() }
|
|
});
|
|
const titleEl = screen.getByText('Roygbiv');
|
|
expect(titleEl.className).toContain('line-through');
|
|
});
|
|
|
|
test('clicking the title fires onPlay with the position', async () => {
|
|
const onPlay = vi.fn();
|
|
render(PlaylistTrackRow, {
|
|
props: { row: live, isOwner: true, onRemove: vi.fn(), onPlay }
|
|
});
|
|
await fireEvent.click(screen.getByText('Roygbiv'));
|
|
expect(onPlay).toHaveBeenCalledWith(2);
|
|
});
|
|
});
|