feat: a release catalogue from MusicBrainz; request search asks it directly (M502 #5519)
release / govulncheck (push) Successful in 22s
release / web (push) Failing after 24s
release / go (push) Successful in 1m57s
release / integration (push) Successful in 4m1s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 4m38s
release / Build signed APK (releases and dev) (push) Canceled after 4m37s
release / govulncheck (push) Successful in 22s
release / web (push) Failing after 24s
release / go (push) Successful in 1m57s
release / integration (push) Successful in 4m1s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 4m38s
release / Build signed APK (releases and dev) (push) Canceled after 4m37s
Minstrel keeps its own record of what artists have released, read from MusicBrainz, so the wanted list, search and import no longer need Lidarr. - internal/musicbrainz: searches (artists, release groups, recordings) and browses (an artist's release groups; a group's releases with track lists). One process-wide pacer at 1.1s, shared with tag enrichment, because MusicBrainz's limit is per address. A 503 throttle is retried. - Migration 0080: mb_artists, mb_release_groups, mb_artist_release_groups (a collaboration belongs to each credited artist), mb_releases, mb_release_tracks. - internal/catalogue: Discography and Releases serve the held copy and re-read it when older than 7 or 30 days. An unreachable MusicBrainz serves the stale copy, marked. An hourly pass re-reads due artists. - Art for what isn't held comes through /api/catalogue/art from the enabled cover-art sources (Cover Art Archive by release group, then the name-based sources) and is kept on disk, so the browser never fetches a third-party host. A miss is a 404 and the card shows its icon. - Request search moves from /api/lidarr/search to /api/catalogue/search, with the same response shape. Albums are release groups, and "in library" matches albums.release_group_mbid. A 10-minute cache absorbs search-as-you-type. Discover suggestions point at the art endpoint instead of looking up Lidarr. - Android calls the new path and loads Discover art through ServerImage. Installed builds older than this one lose Discover search until they update (no compatibility path, rule 22). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,39 +9,39 @@ vi.mock('./client', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
import { searchLidarr } from './lidarr';
|
||||
import { searchCatalogue } from './catalogue';
|
||||
import { api } from './client';
|
||||
import { qk } from './queries';
|
||||
import type { LidarrSearchResult } from './types';
|
||||
import type { CatalogueSearchResult } from './types';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('searchLidarr', () => {
|
||||
describe('searchCatalogue', () => {
|
||||
test('builds URL with q and kind query params', async () => {
|
||||
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||
await searchLidarr('aphex twin', 'artist');
|
||||
expect(api.get).toHaveBeenCalledWith('/api/lidarr/search?q=aphex+twin&kind=artist');
|
||||
await searchCatalogue('aphex twin', 'artist');
|
||||
expect(api.get).toHaveBeenCalledWith('/api/catalogue/search?q=aphex+twin&kind=artist');
|
||||
});
|
||||
|
||||
test('encodes special characters in q (quotes, ampersands, plus)', async () => {
|
||||
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||
await searchLidarr('boards of canada & friends', 'album');
|
||||
await searchCatalogue('boards of canada & friends', 'album');
|
||||
const calledWith = (api.get as ReturnType<typeof vi.fn>).mock.calls[0][0] as string;
|
||||
// URLSearchParams encodes space as '+' and '&' as '%26'.
|
||||
expect(calledWith).toBe(
|
||||
'/api/lidarr/search?q=boards+of+canada+%26+friends&kind=album'
|
||||
'/api/catalogue/search?q=boards+of+canada+%26+friends&kind=album'
|
||||
);
|
||||
});
|
||||
|
||||
test('passes through results unchanged', async () => {
|
||||
const fixture: LidarrSearchResult[] = [
|
||||
const fixture: CatalogueSearchResult[] = [
|
||||
{
|
||||
mbid: 'mbid-1',
|
||||
name: 'Aphex Twin',
|
||||
secondary_text: 'Electronic',
|
||||
image_url: 'https://example.test/img.jpg',
|
||||
image_url: '/api/catalogue/art/artist/mbid-1?name=Aphex+Twin',
|
||||
artist_mbid: '',
|
||||
album_mbid: '',
|
||||
in_library: false,
|
||||
@@ -49,24 +49,24 @@ describe('searchLidarr', () => {
|
||||
}
|
||||
];
|
||||
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce(fixture);
|
||||
const out = await searchLidarr('aphex', 'artist');
|
||||
const out = await searchCatalogue('aphex', 'artist');
|
||||
expect(out).toBe(fixture);
|
||||
});
|
||||
|
||||
test('propagates errors from api.get', async () => {
|
||||
const err = { code: 'lidarr_unavailable', message: 'down', status: 503 };
|
||||
const err = { code: 'musicbrainz_unreachable', message: 'down', status: 503 };
|
||||
(api.get as ReturnType<typeof vi.fn>).mockRejectedValueOnce(err);
|
||||
await expect(searchLidarr('x', 'track')).rejects.toMatchObject({
|
||||
code: 'lidarr_unavailable',
|
||||
await expect(searchCatalogue('x', 'track')).rejects.toMatchObject({
|
||||
code: 'musicbrainz_unreachable',
|
||||
status: 503
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('qk.lidarrSearch', () => {
|
||||
describe('qk.catalogueSearch', () => {
|
||||
test('keys include q + kind', () => {
|
||||
expect(qk.lidarrSearch('foo', 'artist')).toEqual([
|
||||
'lidarrSearch',
|
||||
expect(qk.catalogueSearch('foo', 'artist')).toEqual([
|
||||
'catalogueSearch',
|
||||
{ q: 'foo', kind: 'artist' }
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { api } from './client';
|
||||
import { qk } from './queries';
|
||||
import type { CatalogueSearchResult, LidarrRequestKind } from './types';
|
||||
|
||||
// searchCatalogue asks MusicBrainz through Minstrel's release catalogue
|
||||
// (M502 #5519). URLSearchParams handles encoding (spaces, quotes,
|
||||
// ampersands) so callers can pass q raw.
|
||||
export async function searchCatalogue(
|
||||
q: string,
|
||||
kind: LidarrRequestKind
|
||||
): Promise<CatalogueSearchResult[]> {
|
||||
const params = new URLSearchParams({ q, kind });
|
||||
return api.get<CatalogueSearchResult[]>(`/api/catalogue/search?${params.toString()}`);
|
||||
}
|
||||
|
||||
// MusicBrainz allows a request a second, so a repeated search within a minute
|
||||
// is served from the client's cache rather than asked again.
|
||||
export function createCatalogueSearchQuery(q: string, kind: LidarrRequestKind) {
|
||||
return createQuery({
|
||||
queryKey: qk.catalogueSearch(q, kind),
|
||||
queryFn: () => searchCatalogue(q, kind),
|
||||
enabled: q.length > 0,
|
||||
staleTime: 60_000
|
||||
});
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { api } from './client';
|
||||
import { qk } from './queries';
|
||||
import type { LidarrSearchResult, LidarrRequestKind } from './types';
|
||||
|
||||
// Search-only. Admin-side Lidarr config + connection probing lives in admin.ts.
|
||||
|
||||
// searchLidarr proxies the user-facing Lidarr search endpoint. URLSearchParams
|
||||
// handles encoding (spaces, quotes, ampersands) so callers can pass q raw.
|
||||
export async function searchLidarr(
|
||||
q: string,
|
||||
kind: LidarrRequestKind
|
||||
): Promise<LidarrSearchResult[]> {
|
||||
const params = new URLSearchParams({ q, kind });
|
||||
return api.get<LidarrSearchResult[]>(`/api/lidarr/search?${params.toString()}`);
|
||||
}
|
||||
|
||||
// staleTime mirrors the spec §11 60s server-side LRU cache: re-issuing the
|
||||
// same query within a minute hits cache on the server, so there's no point
|
||||
// in marking it stale client-side any sooner.
|
||||
export function createLidarrSearchQuery(q: string, kind: LidarrRequestKind) {
|
||||
return createQuery({
|
||||
queryKey: qk.lidarrSearch(q, kind),
|
||||
queryFn: () => searchLidarr(q, kind),
|
||||
enabled: q.length > 0,
|
||||
staleTime: 60_000
|
||||
});
|
||||
}
|
||||
@@ -30,8 +30,8 @@ export const qk = {
|
||||
likedArtists: () => ['likedArtists'] as const,
|
||||
history: () => ['history'] as const,
|
||||
// Lidarr / requests / admin.
|
||||
lidarrSearch: (q: string, kind: string) =>
|
||||
['lidarrSearch', { q, kind }] as const,
|
||||
catalogueSearch: (q: string, kind: string) =>
|
||||
['catalogueSearch', { q, kind }] as const,
|
||||
myRequests: () => ['myRequests'] as const,
|
||||
lidarrConfig: () => ['lidarrConfig'] as const,
|
||||
lidarrQualityProfiles: () => ['lidarrQualityProfiles'] as const,
|
||||
|
||||
@@ -146,7 +146,10 @@ export type LidarrRequestStatus =
|
||||
|
||||
export type LidarrRequestKind = 'artist' | 'album' | 'track';
|
||||
|
||||
export type LidarrSearchResult = {
|
||||
// One row of the request search (GET /api/catalogue/search, M502 #5519).
|
||||
// An album is a MusicBrainz release group; image_url is Minstrel's own art
|
||||
// endpoint, which answers 404 when no source has an image.
|
||||
export type CatalogueSearchResult = {
|
||||
mbid: string;
|
||||
name: string;
|
||||
secondary_text: string;
|
||||
|
||||
@@ -28,12 +28,21 @@
|
||||
state: DiscoverCardState;
|
||||
attribution?: string;
|
||||
onRequest?: () => void;
|
||||
// Omit both to get a card with no snooze affordance — the Lidarr search
|
||||
// Omit both to get a card with no snooze affordance — the catalogue search
|
||||
// results reuse this component and have nothing to park.
|
||||
onSnooze?: () => void;
|
||||
onUnsnooze?: () => void;
|
||||
} = $props();
|
||||
|
||||
// Art comes from Minstrel's catalogue endpoint, which answers 404 when no
|
||||
// source has an image (M502 #5519): show the icon then, not a broken image.
|
||||
// Reset when the card is reused for another result.
|
||||
let imageFailed = $state(false);
|
||||
$effect(() => {
|
||||
void imageUrl;
|
||||
imageFailed = false;
|
||||
});
|
||||
|
||||
const FallbackIcon = $derived(
|
||||
kind === 'artist' ? Disc3 : kind === 'album' ? Album : Music2
|
||||
);
|
||||
@@ -50,12 +59,13 @@
|
||||
<div
|
||||
class="art flex aspect-square w-full items-center justify-center overflow-hidden rounded-md bg-surface-hover"
|
||||
>
|
||||
{#if imageUrl}
|
||||
{#if imageUrl && !imageFailed}
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
class="h-full w-full object-cover"
|
||||
onerror={() => (imageFailed = true)}
|
||||
/>
|
||||
{:else}
|
||||
<FallbackIcon size={32} strokeWidth={1} class="text-text-muted" />
|
||||
|
||||
@@ -168,4 +168,20 @@ describe('DiscoverResultCard', () => {
|
||||
expect(onUnsnooze).toHaveBeenCalledOnce();
|
||||
expect(onRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('an image that fails to load falls back to the icon', async () => {
|
||||
const { container } = render(DiscoverResultCard, {
|
||||
props: {
|
||||
kind: 'artist',
|
||||
title: 'Boards of Canada',
|
||||
state: 'requestable',
|
||||
imageUrl: '/api/catalogue/art/artist/x',
|
||||
},
|
||||
});
|
||||
const img = container.querySelector('img');
|
||||
expect(img).not.toBeNull();
|
||||
await fireEvent.error(img!);
|
||||
expect(container.querySelector('img')).toBeNull();
|
||||
expect(container.querySelector('.art svg')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"mbid_required": "An MBID is required for this lookup.",
|
||||
"system_playlist_readonly": "System playlists can't be edited directly.",
|
||||
"connection_refused": "Couldn't reach the server. Check the URL and try again.",
|
||||
"musicbrainz_unreachable": "MusicBrainz can't be reached right now, so search is unavailable. Try again in a minute.",
|
||||
"lidarr_unreachable": "Lidarr is unreachable right now. Try again, or check Admin → Integrations.",
|
||||
"lidarr_disabled": "Lidarr integration is not enabled.",
|
||||
"lidarr_auth_failed": "Lidarr authentication failed.",
|
||||
|
||||
@@ -428,8 +428,8 @@
|
||||
<div>
|
||||
<h3 class="font-display text-lg font-medium text-text-primary">Lidarr</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">
|
||||
Search Lidarr from <span class="font-mono text-accent-fg">/discover</span> and
|
||||
route approved requests to it.
|
||||
Route approved requests from <span class="font-mono text-accent-fg">/discover</span>
|
||||
to Lidarr. Search itself now asks MusicBrainz.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { pageTitle } from '#lib/branding.js';
|
||||
import { createLidarrSearchQuery } from '#lib/api/lidarr.js';
|
||||
import { createCatalogueSearchQuery } from '#lib/api/catalogue.js';
|
||||
import { createRequest } from '#lib/api/requests.js';
|
||||
import { qk } from '#lib/api/queries.js';
|
||||
import { useQueryClient } from '@tanstack/svelte-query';
|
||||
@@ -10,8 +10,8 @@
|
||||
import SuggestionFeed from '#lib/components/SuggestionFeed.svelte';
|
||||
import Modal from '#lib/components/Modal.svelte';
|
||||
import type {
|
||||
LidarrRequestKind,
|
||||
LidarrSearchResult
|
||||
CatalogueSearchResult,
|
||||
LidarrRequestKind
|
||||
} from '#lib/api/types.js';
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
@@ -35,19 +35,19 @@
|
||||
};
|
||||
});
|
||||
|
||||
const queryStore = $derived(createLidarrSearchQuery(debouncedQ, activeKind));
|
||||
const queryStore = $derived(createCatalogueSearchQuery(debouncedQ, activeKind));
|
||||
const query = $derived($queryStore);
|
||||
const results = $derived((query.data ?? []) as LidarrSearchResult[]);
|
||||
const results = $derived((query.data ?? []) as CatalogueSearchResult[]);
|
||||
|
||||
// Track-kind confirm modal: nulled out = closed.
|
||||
let modalResult: LidarrSearchResult | null = $state(null);
|
||||
let modalResult: CatalogueSearchResult | null = $state(null);
|
||||
|
||||
// In-session optimistic flip — once a Request goes through, we flip the
|
||||
// card to 'requested' immediately. Cleared only on full page reload, which
|
||||
// is fine for a short-lived discover session.
|
||||
let optimisticRequested: Set<string> = $state(new Set());
|
||||
|
||||
function cardState(r: LidarrSearchResult) {
|
||||
function cardState(r: CatalogueSearchResult) {
|
||||
if (r.in_library) return 'kept' as const;
|
||||
if (r.requested || optimisticRequested.has(r.mbid)) return 'requested' as const;
|
||||
return 'requestable' as const;
|
||||
@@ -56,7 +56,7 @@
|
||||
// Backend validates kind→required-MBID-fields, not the human strings, so
|
||||
// imperfect derivations of artist_name from secondary_text won't block the
|
||||
// request. We pass the data we have and let the server take care of the rest.
|
||||
function buildRequestParams(r: LidarrSearchResult, kind: LidarrRequestKind) {
|
||||
function buildRequestParams(r: CatalogueSearchResult, kind: LidarrRequestKind) {
|
||||
if (kind === 'artist') {
|
||||
return {
|
||||
kind,
|
||||
@@ -85,7 +85,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function submitRequest(r: LidarrSearchResult) {
|
||||
async function submitRequest(r: CatalogueSearchResult) {
|
||||
try {
|
||||
await createRequest(buildRequestParams(r, activeKind));
|
||||
const next = new Set(optimisticRequested);
|
||||
@@ -99,7 +99,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleRequestClick(r: LidarrSearchResult) {
|
||||
function handleRequestClick(r: CatalogueSearchResult) {
|
||||
if (activeKind === 'track') {
|
||||
modalResult = r;
|
||||
} else {
|
||||
@@ -137,7 +137,7 @@
|
||||
|
||||
<input
|
||||
type="search"
|
||||
aria-label="Search Lidarr"
|
||||
aria-label="Search MusicBrainz"
|
||||
placeholder="Search artists, albums, or tracks"
|
||||
bind:value={inputValue}
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-2 text-text-primary placeholder:text-text-muted focus:outline-hidden focus:ring-2 focus:ring-accent"
|
||||
@@ -153,7 +153,7 @@
|
||||
Add music to the library
|
||||
</h2>
|
||||
<p class="text-text-secondary">
|
||||
Search Lidarr to add new artists, albums, or tracks.
|
||||
Search MusicBrainz to add new artists, albums, or tracks.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
||||
import { mockQuery } from '../../test-utils/query';
|
||||
import { apiClientMock } from '../../test-utils/mocks/client';
|
||||
import type { LidarrSearchResult } from '#lib/api/types.js';
|
||||
import type { CatalogueSearchResult } from '#lib/api/types.js';
|
||||
import { pageUrlModule } from '../../test-utils/mocks/appState';
|
||||
|
||||
// $app/state / $app/navigation must be mocked or rendering the page
|
||||
@@ -14,11 +14,11 @@ const pageState = vi.hoisted(() => ({
|
||||
vi.mock('$app/state', () => pageUrlModule(pageState));
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
|
||||
// Lidarr search query factory and createRequest are mocked at the module
|
||||
// The catalogue search query factory and createRequest are mocked at the module
|
||||
// level so each test can shape what the page sees without standing up a
|
||||
// real QueryClient + network.
|
||||
vi.mock('#lib/api/lidarr.js', () => ({
|
||||
createLidarrSearchQuery: vi.fn()
|
||||
vi.mock('#lib/api/catalogue.js', () => ({
|
||||
createCatalogueSearchQuery: vi.fn()
|
||||
}));
|
||||
|
||||
// SuggestionFeed reaches for the snooze surface too (#2375). These are
|
||||
@@ -49,16 +49,16 @@ vi.mock('@tanstack/svelte-query', async (importOriginal) => {
|
||||
});
|
||||
|
||||
import DiscoverPage from './+page.svelte';
|
||||
import { createLidarrSearchQuery } from '#lib/api/lidarr.js';
|
||||
import { createCatalogueSearchQuery } from '#lib/api/catalogue.js';
|
||||
import { createSuggestionsQuery, createSnoozesQuery } from '#lib/api/suggestions.js';
|
||||
import { createRequest } from '#lib/api/requests.js';
|
||||
|
||||
const mockedCreateQuery = createLidarrSearchQuery as ReturnType<typeof vi.fn>;
|
||||
const mockedCreateQuery = createCatalogueSearchQuery as ReturnType<typeof vi.fn>;
|
||||
const mockedCreateSuggestionsQuery = createSuggestionsQuery as ReturnType<typeof vi.fn>;
|
||||
const mockedCreateSnoozesQuery = createSnoozesQuery as ReturnType<typeof vi.fn>;
|
||||
const mockedCreateRequest = createRequest as ReturnType<typeof vi.fn>;
|
||||
|
||||
function result(over: Partial<LidarrSearchResult> = {}): LidarrSearchResult {
|
||||
function result(over: Partial<CatalogueSearchResult> = {}): CatalogueSearchResult {
|
||||
return {
|
||||
mbid: 'mbid-1',
|
||||
name: 'Boards of Canada',
|
||||
@@ -74,7 +74,7 @@ function result(over: Partial<LidarrSearchResult> = {}): LidarrSearchResult {
|
||||
|
||||
beforeEach(() => {
|
||||
// Default: empty results, non-pending. Tests override per-case.
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [] }));
|
||||
// Default: empty suggestion feed so its empty-state copy renders without
|
||||
// interfering with search-mode tests.
|
||||
mockedCreateSuggestionsQuery.mockReturnValue(mockQuery({ data: [] }));
|
||||
@@ -105,7 +105,7 @@ describe('Discover page', () => {
|
||||
test('typing replaces feed with search', async () => {
|
||||
vi.useFakeTimers();
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'miles' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(screen.queryByText(/suggested for you/i)).not.toBeInTheDocument();
|
||||
@@ -118,7 +118,7 @@ describe('Discover page', () => {
|
||||
test('debounced input fires query factory with typed value after 250ms', async () => {
|
||||
vi.useFakeTimers();
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'boards' } });
|
||||
// Before the timer elapses, the factory was called only with the
|
||||
// initial empty string — never with 'boards'.
|
||||
@@ -131,7 +131,7 @@ describe('Discover page', () => {
|
||||
test('switching tabs refetches with the new kind', async () => {
|
||||
vi.useFakeTimers();
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'boards' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
// Switch to Albums.
|
||||
@@ -145,9 +145,9 @@ describe('Discover page', () => {
|
||||
|
||||
test('empty results state renders the voice-rule copy', async () => {
|
||||
vi.useFakeTimers();
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [] }));
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'zzz' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
await waitFor(() =>
|
||||
@@ -165,9 +165,9 @@ describe('Discover page', () => {
|
||||
artist_mbid: 'art-mbid',
|
||||
name: 'Boards of Canada'
|
||||
});
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [r] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [r] }));
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'boards' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
vi.useRealTimers();
|
||||
@@ -191,9 +191,9 @@ describe('Discover page', () => {
|
||||
artist_mbid: 'art-mbid',
|
||||
name: 'Boards of Canada'
|
||||
});
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [r] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [r] }));
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'boards' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
vi.useRealTimers();
|
||||
@@ -211,9 +211,9 @@ describe('Discover page', () => {
|
||||
test('submitting a request invalidates myRequests so /requests updates (#369)', async () => {
|
||||
vi.useFakeTimers();
|
||||
const r = result({ mbid: 'art-mbid', artist_mbid: 'art-mbid', name: 'Boards of Canada' });
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [r] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [r] }));
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'boards' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
vi.useRealTimers();
|
||||
@@ -238,11 +238,11 @@ describe('Discover page', () => {
|
||||
name: 'Roygbiv',
|
||||
secondary_text: 'Music Has The Right To Children · Boards of Canada'
|
||||
});
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [r] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [r] }));
|
||||
render(DiscoverPage);
|
||||
// Type first so the kind tabs become visible (empty-input mode shows the
|
||||
// suggestion feed and hides tabs).
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'roy' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
// Switch to track kind.
|
||||
@@ -279,9 +279,9 @@ describe('Discover page', () => {
|
||||
name: 'Roygbiv',
|
||||
secondary_text: 'Music Has The Right To Children · Boards of Canada'
|
||||
});
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [r] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [r] }));
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'roy' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Tracks' }));
|
||||
@@ -305,9 +305,9 @@ describe('Discover page', () => {
|
||||
in_library: true,
|
||||
requested: true
|
||||
});
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [r] }));
|
||||
mockedCreateQuery.mockReturnValue(mockQuery<CatalogueSearchResult[]>({ data: [r] }));
|
||||
render(DiscoverPage);
|
||||
const input = screen.getByLabelText(/search lidarr/i);
|
||||
const input = screen.getByLabelText(/search musicbrainz/i);
|
||||
await fireEvent.input(input, { target: { value: 'kind' } });
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
vi.useRealTimers();
|
||||
|
||||
Reference in New Issue
Block a user