Files
minstrel/web/src/routes/admin/metadata/metadata.test.ts
T
bvandeusenandClaude Opus 5.5 7f913e3e70
release / govulncheck (push) Successful in 16s
release / web (push) Failing after 1m27s
release / go (push) Successful in 1m44s
release / android (push) Successful in 5m13s
release / integration (push) Successful in 5m18s
release / Build signed APK (releases and dev) (push) Successful in 5m26s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
feat: the admin settings cards read as menus: each a titled group of rows with one short line, status first, failures in words (M503 #5607)
Indexers and clients, searching, missing files, Lidarr's neighbours, AcoustID, fingerprinting, loudness, outgoing email's schedule, public address and proxies move from paragraph cards to SettingsGroup and SettingRow. Switches are role="switch". Also fixes the two type errors run 9023 found: the Lidarr test result is narrowed before its lists are read, and the tag-source save mock carries version_bumped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:25:46 -04:00

175 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown>;
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<typeof mockQuery>; 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();
});
});