Files
minstrel/web/src/routes/library/history/page.test.ts
T
bvandeusenandClaude Opus 5.5 aaa6fa15d6 test(web): import history components after their mocks (M494)
HistoryRow now renders LikeButton, and both history test files imported
the component before the likes mock helper. LikeButton then loaded while
emptyLikesMock was still uninitialised, and the suites failed at import.
Every other suite imports components after its mocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:18:44 -04:00

132 lines
4.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import type { HistoryEvent, HistoryResponse } from '#lib/api/history.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
import { emptyLikesMock } from '../../../test-utils/mocks/likes';
import { emptyQuarantineMock } from '../../../test-utils/mocks/quarantine';
const mkTrack = (id: string) => makeTrack({ id: `track-${id}`, title: `Song ${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.js', async () => {
const actual = await vi.importActual<typeof import('#lib/api/history.js')>(
'#lib/api/history.js'
);
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 () => {};
}
})
};
});
// Rows are TrackListRows, so LikeButton and TrackMenu render too.
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn(),
playNext: vi.fn(),
enqueueTrack: vi.fn(),
playRadio: vi.fn(),
player: { current: undefined }
}));
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
vi.mock('#lib/branding.js', () => ({
pageTitle: (s: string) => `Minstrel · ${s}`
}));
// Imported after the mocks: the component pulls in LikeButton, whose
// mocked module must already be initialised.
import HistoryPage from './+page.svelte';
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 a role="alert" container. Assert by role
// rather than by message text, since the message comes from the
// error object's `.message` property and varies.
expect(screen.getByRole('alert')).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 <div aria-hidden="true" class="h-px">.
// When hasNextPage is false, no such div should be in the DOM.
expect(container.querySelector('div[aria-hidden="true"].h-px')).toBeNull();
// Confirm the page rendered the row so we know we hit the success branch.
expect(screen.getByText('Song a')).toBeInTheDocument();
});
it('renders the InfiniteScrollSentinel when hasNextPage is true', () => {
mockData = { pages: [{ events: [mkEvent('a', new Date())], has_more: true }] };
mockHasNextPage = true;
mockIsFetchingNextPage = false;
const { container } = render(HistoryPage);
expect(container.querySelector('div[aria-hidden="true"].h-px')).not.toBeNull();
});
});