import { afterEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; import type { AlbumRef, AlbumDetail, TrackRef } from '$lib/api/types'; import { FALLBACK_COVER } from '$lib/media/covers'; import { emptyLikesMock } from '../../test-utils/mocks/likes'; import { apiClientMock } from '../../test-utils/mocks/client'; import { makeTrack } from '$test-utils/fixtures/track'; vi.mock('$lib/api/client', () => apiClientMock()); vi.mock('$lib/player/store.svelte', () => ({ enqueueTracks: vi.fn(), playQueue: vi.fn() })); vi.mock('$lib/api/likes', () => emptyLikesMock()); vi.mock('$app/navigation', () => ({ goto: vi.fn() })); vi.mock('$lib/api/albums', async (orig) => { const actual = (await orig()) as Record; return { ...actual, listAlbumTracks: vi.fn().mockResolvedValue([]) }; }); vi.mock('$lib/api/playlists', async (orig) => { const { readable } = await import('svelte/store'); const actual = (await orig()) as Record; return { ...actual, createPlaylistsQuery: () => readable({ data: { owned: [], public: [] }, isPending: false, isError: false }), appendTracks: vi.fn().mockResolvedValue({ id: 'p1', tracks: [] }), createPlaylist: vi.fn().mockResolvedValue({ id: 'p1', name: 'New' }) }; }); import AlbumCard from './AlbumCard.svelte'; import { api } from '$lib/api/client'; import { enqueueTracks, playQueue } from '$lib/player/store.svelte'; const album: AlbumRef = { id: 'xyz', title: 'Kind of Blue', sort_title: 'Kind of Blue', artist_id: 'm-davis', artist_name: 'Miles Davis', year: 1959, track_count: 5, duration_sec: 2630, cover_url: '/api/albums/xyz/cover', cover_art_source: null, }; afterEach(() => vi.clearAllMocks()); describe('AlbumCard', () => { test('renders cover, title, year inside a link to /albums/:id', () => { const { container } = render(AlbumCard, { props: { album } }); const link = screen.getByRole('link'); expect(link).toHaveAttribute('href', '/albums/xyz'); expect(link).toHaveTextContent('Kind of Blue'); expect(link).toHaveTextContent('1959'); const img = container.querySelector('img') as HTMLImageElement; expect(img.src).toContain('/api/albums/xyz/cover'); }); test('year is omitted when not present', () => { render(AlbumCard, { props: { album: { ...album, year: undefined } } }); expect(screen.queryByText(/1959/)).not.toBeInTheDocument(); }); test('broken cover falls back to FALLBACK_COVER asset', async () => { const { container } = render(AlbumCard, { props: { album } }); const img = container.querySelector('img') as HTMLImageElement; await fireEvent.error(img); // jsdom resolves relative URLs against the test origin; assert containment // rather than strict equality to keep the test agnostic of the host. expect(img.src).toContain(FALLBACK_COVER); }); test('+ queue button fetches album detail and calls enqueueTracks', async () => { const tracks: TrackRef[] = [ makeTrack({ title: 'So What', album_id: 'xyz', album_title: 'Kind of Blue', artist_id: 'm-davis', artist_name: 'Miles Davis', duration_sec: 545 }) ]; const detail: AlbumDetail = { ...album, tracks }; (api.get as ReturnType).mockResolvedValueOnce(detail); render(AlbumCard, { props: { album } }); await fireEvent.click(screen.getByRole('button', { name: /add to queue/i })); expect(api.get).toHaveBeenCalledWith('/api/albums/xyz'); await Promise.resolve(); await Promise.resolve(); expect(enqueueTracks).toHaveBeenCalledWith(tracks); }); test('play overlay click fetches album detail and calls playQueue at index 0', async () => { const tracks: TrackRef[] = [ makeTrack({ title: 'So What', album_id: 'xyz', album_title: 'Kind of Blue', artist_id: 'm-davis', artist_name: 'Miles Davis', duration_sec: 545 }) ]; const detail: AlbumDetail = { ...album, tracks }; (api.get as ReturnType).mockResolvedValueOnce(detail); render(AlbumCard, { props: { album } }); await fireEvent.click(screen.getByRole('button', { name: /play kind of blue/i })); expect(api.get).toHaveBeenCalledWith('/api/albums/xyz'); await Promise.resolve(); await Promise.resolve(); expect(playQueue).toHaveBeenCalledWith(tracks, 0); }); test('renders the bottom-right kebab menu trigger', () => { render(AlbumCard, { props: { album } }); expect(screen.getByRole('button', { name: /Album actions for Kind of Blue/i })).toBeInTheDocument(); }); });