import { afterEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; import type { LoudnessCoverage, LoudnessSettings } from '$lib/api/admin'; vi.mock('$lib/api/admin', () => ({ getLoudnessSettings: vi.fn(), updateLoudnessSettings: vi.fn(), getLoudnessCoverage: vi.fn() })); vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() })); import LoudnessSettingsCard from './LoudnessSettingsCard.svelte'; import { getLoudnessCoverage, getLoudnessSettings, updateLoudnessSettings } from '$lib/api/admin'; import { pushToast } from '$lib/stores/toast.svelte'; const base: LoudnessSettings = { enabled: true, backfill_concurrency: 2 }; const coverage: LoudnessCoverage = { total: 1200, measured: 900, silent: 2, unreadable: 3, pending: 295, enabled: true }; afterEach(() => vi.clearAllMocks()); async function renderCard( over: Partial = {}, cov: Partial = {} ) { vi.mocked(getLoudnessSettings).mockResolvedValue({ ...base, ...over }); vi.mocked(getLoudnessCoverage).mockResolvedValue({ ...coverage, ...cov }); const r = render(LoudnessSettingsCard); await screen.findByRole('spinbutton', { name: /files analyzed at once/i }); return r; } const saveButton = () => screen.getByRole('button', { name: /save/i }); const concurrency = () => screen.getByRole('spinbutton', { name: /files analyzed at once/i }); describe('LoudnessSettingsCard', () => { test('shows how far the analysis has got', async () => { await renderCard(); const gauge = await screen.findByTestId('loudness-coverage'); expect(gauge.textContent).toMatch(/900 of 1,200 tracks measured/); expect(gauge.textContent).toMatch(/295 pending/); expect(gauge.textContent).toMatch(/2 silent/); expect(gauge.textContent).toMatch(/3 unreadable/); expect(gauge.textContent).not.toMatch(/paused/); }); // With analysis off, pending never shrinks; the gauge must not read as progress. test('says the work is paused when analysis is off', async () => { await renderCard({ enabled: false }, { enabled: false }); const gauge = await screen.findByTestId('loudness-coverage'); expect(gauge.textContent).toMatch(/paused while analysis is off/); }); test('save is disabled until something changes, then saves the form', async () => { vi.mocked(updateLoudnessSettings).mockResolvedValue({ ...base, backfill_concurrency: 4 }); await renderCard(); expect(saveButton()).toHaveProperty('disabled', true); await fireEvent.input(concurrency(), { target: { value: '4' } }); await waitFor(() => expect(saveButton()).toHaveProperty('disabled', false)); await fireEvent.click(saveButton()); await waitFor(() => expect(updateLoudnessSettings).toHaveBeenCalledWith({ enabled: true, backfill_concurrency: 4 }) ); await waitFor(() => expect(pushToast).toHaveBeenCalledWith('Loudness analysis settings saved.')); // The gauge is read again after a save: switching analysis on or off changes it. expect(getLoudnessCoverage).toHaveBeenCalledTimes(2); }); test('an out-of-range value is named and cannot be saved', async () => { await renderCard(); await fireEvent.input(concurrency(), { target: { value: '9' } }); await waitFor(() => expect(screen.getByTestId('settings-problems').textContent).toMatch(/1 to 8/) ); expect(saveButton()).toHaveProperty('disabled', true); }); test('a failed load offers a retry', async () => { vi.mocked(getLoudnessSettings).mockRejectedValue(new Error('boom')); vi.mocked(getLoudnessCoverage).mockResolvedValue(coverage); render(LoudnessSettingsCard); await screen.findByText(/couldn't load loudness analysis settings/i); expect(screen.getByRole('button', { name: /try again/i })).toBeTruthy(); }); });