import { afterEach, describe, expect, test, vi } from 'vitest'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/svelte'; import { mockQuery } from '../../../test-utils/query'; import type { AdminPlaybackError, AdminQuarantineRow } from '#lib/api/types.js'; vi.mock('#lib/api/admin.js', () => ({ createAdminQuarantineQuery: vi.fn(), createAdminPlaybackErrorsQuery: vi.fn(), createQuarantineActionsQuery: vi.fn(), resolveQuarantine: vi.fn().mockResolvedValue({}), resolvePlaybackError: vi.fn().mockResolvedValue({}), deleteQuarantineFile: vi.fn().mockResolvedValue({}), deleteQuarantineViaLidarr: vi.fn().mockResolvedValue({}) })); vi.mock('#lib/player/store.svelte.js', () => ({ playRadio: vi.fn() })); import Page from './+page.svelte'; import { createAdminPlaybackErrorsQuery, createAdminQuarantineQuery, createQuarantineActionsQuery, deleteQuarantineFile, resolvePlaybackError, resolveQuarantine } from '#lib/api/admin.js'; const hoursAgo = (h: number) => new Date(Date.now() - h * 3_600_000).toISOString(); const flagged: AdminQuarantineRow = { track_id: 't1', track_title: 'Roygbiv', artist_name: 'Boards of Canada', album_title: 'Geogaddi', album_id: 'al1', lidarr_album_mbid: 'rg1', report_count: 1, latest_at: hoursAgo(5), reason_counts: { bad_rip: 1 }, reports: [{ user_id: 'u1', username: 'alice', reason: 'bad_rip', notes: 'crackles', created_at: hoursAgo(5) }] }; const failure = (id: string, track: string, title: string, h: number): AdminPlaybackError => ({ id, track_id: track, user_id: 'u2', username: 'bob', client_id: 'android', kind: 'load_failed', occurred_at: hoursAgo(h), track_title: title, track_file_path: `/music/${title}.flac`, artist_name: 'Annadale', album_title: 'Dial Tone', album_id: 'al2' }); afterEach(() => vi.clearAllMocks()); function setup(q: AdminQuarantineRow[], e: AdminPlaybackError[], opts: { error?: boolean } = {}) { vi.mocked(createAdminQuarantineQuery).mockReturnValue( (opts.error ? mockQuery({ isError: true, error: new Error('x') }) : mockQuery({ data: q })) as never ); vi.mocked(createAdminPlaybackErrorsQuery).mockReturnValue(mockQuery({ data: e }) as never); vi.mocked(createQuarantineActionsQuery).mockReturnValue(mockQuery({ data: [] }) as never); return render(Page); } describe('/admin/reports', () => { test('a flagged track and a track that failed to play are one list, one row per track, newest first', () => { setup([flagged], [failure('e1', 't1', 'Roygbiv', 1), failure('e2', 't2', 'Intro', 9)]); const rows = screen.getAllByTestId('report-row'); expect(rows).toHaveLength(2); expect(screen.getByTestId('reports-verdict').textContent).toMatch(/2 tracks need a look/); const first = within(rows[0]).getByTestId('report-kinds').textContent?.replace(/\s+/g, ' '); expect(first).toMatch(/1× Bad rip/); expect(first).toMatch(/1× Wouldn't load/); expect(rows[1].textContent).toMatch(/Intro/); }); test('Resolve clears the listeners\' reports and marks the playback errors fixed', async () => { setup([flagged], [failure('e1', 't1', 'Roygbiv', 1)]); await fireEvent.click(screen.getByRole('button', { name: /resolve roygbiv/i })); await waitFor(() => expect(resolveQuarantine).toHaveBeenCalledWith('t1')); await waitFor(() => expect(resolvePlaybackError).toHaveBeenCalledWith('e1', 'fixed')); }); test('a track only failing to play is resolved without touching listener reports', async () => { setup([], [failure('e2', 't2', 'Intro', 2)]); await fireEvent.click(screen.getByRole('button', { name: /resolve intro/i })); await waitFor(() => expect(resolvePlaybackError).toHaveBeenCalledWith('e2', 'fixed')); expect(resolveQuarantine).not.toHaveBeenCalled(); }); test('Delete file is one path, whatever the report came from', async () => { setup([], [failure('e2', 't2', 'Intro', 2)]); await fireEvent.click(screen.getByRole('button', { name: /delete file for intro/i })); await fireEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: /confirm delete file/i })); await waitFor(() => expect(deleteQuarantineFile).toHaveBeenCalledWith('t2')); expect(resolvePlaybackError).not.toHaveBeenCalled(); }); test('Delete via Lidarr is offered only for an album Lidarr holds, and on a phone it is in the row menu', async () => { setup([flagged], [failure('e2', 't2', 'Intro', 2)]); expect(screen.getByRole('button', { name: /delete via lidarr for roygbiv/i })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /delete via lidarr for intro/i })).toBeNull(); await fireEvent.click(screen.getAllByRole('button', { name: /more actions/i })[0]); expect(within(screen.getByRole('menu')).getByText('Delete via Lidarr')).toBeInTheDocument(); }); test('details show who flagged it, who it failed for, and the file', async () => { setup([flagged], [failure('e1', 't1', 'Roygbiv', 1)]); await fireEvent.click(screen.getByRole('button', { name: /^details$/i })); const details = screen.getByTestId('report-details').textContent?.replace(/\s+/g, ' '); expect(details).toMatch(/alice flagged it · Bad rip · crackles/); expect(details).toMatch(/Failed to play for bob · Wouldn't load/); expect(details).toMatch(/\/music\/Roygbiv\.flac/); }); test('a load failure says so with Try again, and an empty queue says nothing waits', () => { setup([], [], { error: true }); expect(screen.getByTestId('reports-error').textContent).toMatch(/couldn't load the reports/i); }); test('with nothing reported it says so', () => { setup([], []); expect(screen.getByTestId('reports-empty').textContent).toMatch(/no reports/i); }); });