import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import HistoryPage from './+page.svelte'; import type { HistoryEvent, HistoryResponse } from '$lib/api/history'; import type { TrackRef } from '$lib/api/types'; const mkTrack = (id: string): TrackRef => ({ id: `track-${id}`, title: `Song ${id}`, album_id: `album-${id}`, album_title: `Album ${id}`, artist_id: `artist-${id}`, artist_name: `Artist ${id}`, duration_sec: 200, stream_url: `/stream/${id}` }); const mkEvent = (id: string, playedAt: Date): HistoryEvent => ({ id: `event-${id}`, played_at: playedAt.toISOString(), track: mkTrack(id) }); let mockData: { pages: HistoryResponse[] } = { pages: [] }; let mockHasNextPage = false; let mockIsPending = false; let mockIsError = false; let mockIsFetchingNextPage = false; const mockFetchNextPage = vi.fn(); const mockRefetch = vi.fn(); vi.mock('$lib/api/history', async () => { const actual = await vi.importActual( '$lib/api/history' ); return { ...actual, createHistoryQuery: () => ({ subscribe: (run: (v: unknown) => void) => { run({ data: mockData, isPending: mockIsPending, isError: mockIsError, error: mockIsError ? new Error('test') : null, hasNextPage: mockHasNextPage, isFetchingNextPage: mockIsFetchingNextPage, fetchNextPage: mockFetchNextPage, refetch: mockRefetch }); return () => {}; } }) }; }); vi.mock('$lib/player/store.svelte', () => ({ playQueue: vi.fn() })); vi.mock('$lib/branding', () => ({ pageTitle: (s: string) => `Minstrel ยท ${s}` })); describe('library/history page', () => { beforeEach(() => { mockData = { pages: [] }; mockHasNextPage = false; mockIsPending = false; mockIsError = false; mockIsFetchingNextPage = false; mockFetchNextPage.mockClear(); mockRefetch.mockClear(); }); it('renders Today and Yesterday section headers across page boundary', () => { const today = new Date(); const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); mockData = { pages: [ { events: [mkEvent('a', today), mkEvent('b', today)], has_more: true }, { events: [mkEvent('c', yesterday)], has_more: false } ] }; render(HistoryPage); expect(screen.getByText('Today')).toBeInTheDocument(); expect(screen.getByText('Yesterday')).toBeInTheDocument(); expect(screen.getByText('Song a')).toBeInTheDocument(); expect(screen.getByText('Song b')).toBeInTheDocument(); expect(screen.getByText('Song c')).toBeInTheDocument(); }); it('renders empty state when query data is empty', () => { mockData = { pages: [{ events: [], has_more: false }] }; render(HistoryPage); expect(screen.getByText(/no listening history yet/i)).toBeInTheDocument(); }); it('renders error banner on isError', () => { mockIsError = true; render(HistoryPage); // ApiErrorBanner renders generic error copy. The default message // it falls back to is "Something went wrong." expect(screen.getByText(/something went wrong/i)).toBeInTheDocument(); }); it('does not render the InfiniteScrollSentinel when hasNextPage is false', () => { mockData = { pages: [{ events: [mkEvent('a', new Date())], has_more: false }] }; mockHasNextPage = false; const { container } = render(HistoryPage); // InfiniteScrollSentinel renders