import { afterEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent, waitFor, within } from '@testing-library/svelte'; import { mockQuery } from '../../../test-utils/query'; import type { CoverProvider, TagProvider } from '#lib/api/admin.js'; const invalidateQueries = vi.fn(); vi.mock('@tanstack/svelte-query', async (orig) => { const actual = (await orig()) as Record; return { ...actual, useQueryClient: () => ({ invalidateQueries }) }; }); vi.mock('#lib/api/admin.js', () => ({ createCoverProvidersQuery: vi.fn(), updateCoverProvider: vi.fn(), testCoverProvider: vi.fn(), createTagProvidersQuery: vi.fn(), updateTagProvider: vi.fn(), testTagProvider: vi.fn(), // AcoustIDCard loads on mount and has its own suite; left pending. getAcoustID: vi.fn(() => new Promise(() => {})), updateAcoustIDSettings: vi.fn(), runAcoustIDLookup: vi.fn(), listUnsettledAcoustID: vi.fn() })); import MetadataPage from './+page.svelte'; import { createCoverProvidersQuery, updateCoverProvider, testCoverProvider, createTagProvidersQuery, updateTagProvider, testTagProvider } from '#lib/api/admin.js'; const caa: CoverProvider = { id: 'mbcaa', display_name: 'MusicBrainz CAA', requires_api_key: false, supports: ['album_cover'], enabled: true, api_key_set: false, display_order: 0, testable: true }; const audiodb: CoverProvider = { id: 'theaudiodb', display_name: 'TheAudioDB', requires_api_key: true, supports: ['album_cover', 'artist_thumb', 'artist_fanart'], enabled: true, api_key_set: false, display_order: 1, testable: true }; const mb: TagProvider = { id: 'musicbrainz', display_name: 'MusicBrainz', requires_api_key: false, supports: ['track_tags'], enabled: true, api_key_set: false, display_order: 0, testable: true }; const lastfm: TagProvider = { id: 'lastfm', display_name: 'Last.fm', requires_api_key: true, supports: ['track_tags'], enabled: false, api_key_set: false, display_order: 1, testable: true }; afterEach(() => { vi.clearAllMocks(); invalidateQueries.mockReset(); }); function setup(opts: { cover?: ReturnType; covers?: CoverProvider[] } = {}) { vi.mocked(createCoverProvidersQuery).mockReturnValue( (opts.cover ?? mockQuery({ data: { providers: opts.covers ?? [caa, audiodb], sources_version: 1 } })) as never ); vi.mocked(createTagProvidersQuery).mockReturnValue( mockQuery({ data: { providers: [mb, lastfm], sources_version: 1 } }) as never ); return render(MetadataPage); } const source = (id: string) => screen.getAllByTestId('provider').find((el) => el.dataset.provider === id)!; const group = (title: string) => screen.getByRole('heading', { name: title, level: 2 }).closest('section') as HTMLElement; describe('Admin › Metadata sources', () => { test('each source is a row saying what it supplies in words, not its codes', () => { setup(); const row = source('theaudiodb'); expect(row.textContent).toMatch(/Album covers, artist photos, artist backdrops/); expect(row.textContent).not.toMatch(/artist_thumb|album_cover/); expect(within(group('Tags')).getAllByTestId('provider')).toHaveLength(2); }); test('a saved key says so, and an unsaved one invites a key', () => { setup({ covers: [{ ...audiodb, api_key_set: true }] }); expect(within(source('theaudiodb')).getByPlaceholderText(/key saved/i)).toBeInTheDocument(); }); test('Save is off until something changes, then sends only the change', async () => { setup(); vi.mocked(updateCoverProvider).mockResolvedValueOnce({ ...audiodb, version_bumped: false }); const row = source('theaudiodb'); const save = within(row).getByRole('button', { name: /^save$/i }) as HTMLButtonElement; expect(save.disabled).toBe(true); await fireEvent.input(within(row).getByLabelText(/theaudiodb api key/i), { target: { value: 'newkey123' } }); expect(save.disabled).toBe(false); await fireEvent.click(save); expect(updateCoverProvider).toHaveBeenCalledWith('theaudiodb', { api_key: 'newkey123' }); }); test('switching a cover source off reopens covers no source had', async () => { setup(); vi.mocked(updateCoverProvider).mockResolvedValueOnce({ ...caa, enabled: false, version_bumped: true }); const row = source('mbcaa'); await fireEvent.click(within(row).getByRole('switch')); await fireEvent.click(within(row).getByRole('button', { name: /^save$/i })); await waitFor(() => expect(invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['coverage'] }))); }); test('a save that fails says so', async () => { setup(); vi.mocked(updateCoverProvider).mockRejectedValueOnce({ code: 'server_error' }); const row = source('theaudiodb'); await fireEvent.input(within(row).getByLabelText(/api key/i), { target: { value: 'k' } }); await fireEvent.click(within(row).getByRole('button', { name: /^save$/i })); await waitFor(() => expect(screen.getByTestId('toast').textContent).toMatch(/couldn't save theaudiodb/i)); }); test('Test says whether the source answered', async () => { setup(); vi.mocked(testCoverProvider).mockResolvedValueOnce({ ok: true, duration_ms: 120 }); await fireEvent.click(within(source('mbcaa')).getByRole('button', { name: /^test$/i })); await waitFor(() => expect(within(source('mbcaa')).getByTestId('provider-test').textContent).toMatch(/works · answered in 120 ms/i)); }); test('sources that fail to load say so, with Try again', () => { setup({ cover: mockQuery({ isError: true, error: { code: 'server_error' } }) }); expect(within(group('Cover art')).getByTestId('providers-error').textContent).toMatch(/couldn't load the cover art sources/i); }); test('a tag source is switched on with its key in one save', async () => { setup(); vi.mocked(updateTagProvider).mockResolvedValueOnce({ ...lastfm, enabled: true, api_key_set: true, version_bumped: true }); const row = source('lastfm'); await fireEvent.click(within(row).getByRole('switch')); await fireEvent.input(within(row).getByLabelText(/api key/i), { target: { value: 'lfmkey' } }); await fireEvent.click(within(row).getByRole('button', { name: /^save$/i })); expect(updateTagProvider).toHaveBeenCalledWith('lastfm', { enabled: true, api_key: 'lfmkey' }); await waitFor(() => expect(invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['tagProviders'] }))); }); test('Test on a tag source asks that source', async () => { setup(); vi.mocked(testTagProvider).mockResolvedValueOnce({ ok: true, duration_ms: 42 }); await fireEvent.click(within(source('musicbrainz')).getByRole('button', { name: /^test$/i })); await waitFor(() => expect(testTagProvider).toHaveBeenCalledWith('musicbrainz')); }); test('where to get a key is linked from the source that needs one', () => { setup(); expect(within(source('lastfm')).getByRole('link', { name: /get a free key/i })).toBeInTheDocument(); expect(within(source('theaudiodb')).getByRole('link', { name: /a free key of your own/i })).toBeInTheDocument(); }); });