fix: every library album is matched to MusicBrainz before it is listed, by its tags, its artist's discography or a search, and checked against its tracks (M502 #5569)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m19s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m28s
release / android (push) Successful in 5m44s
release / Build signed APK (releases and dev) (push) Successful in 5m58s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m19s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m28s
release / android (push) Successful in 5m44s
release / Build signed APK (releases and dev) (push) Successful in 5m58s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
The one-time listing took only albums whose tags already named their release group and artist, and left 340 off the list. The rest are now matched: the release their tags name, else the artist's release groups called what the album is called, else a MusicBrainz search by artist and title. A group found by name is taken only when its tracks agree with the album's (by disc and number, then title; length and title, or recording), so a partial copy still matches and a same-titled other album does not. library_album_matches records each album the listing decided, listed or unmatched with why, so an album taken off the list is never re-listed. The albums no match was found for are named on the Held tab. Album rows with no tracks are no longer counted. The length and title checks move from the importer to release, shared by both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -815,7 +815,9 @@ export type ReleaseBlocksPage = { items: ReleaseBlock[]; limit: number; offset:
|
||||
// The one-time listing of the library's albums as held (#5567): when it
|
||||
// finished (null while it runs), and how many albums nothing names to
|
||||
// MusicBrainz, so they could not be listed.
|
||||
export type LibraryListing = { listed_at: string | null; unlisted: number };
|
||||
export type UnmatchedAlbum = { album_id: string; title: string; artist: string; detail: string };
|
||||
|
||||
export type LibraryListing = { listed_at: string | null; unlisted: number; unmatched: UnmatchedAlbum[] };
|
||||
|
||||
export type WantsPage = Page<Want> & { counts: WantCounts; library: LibraryListing };
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
// the missing albums wanted back, or the ones held. Each open want says
|
||||
// what its last search found and how its download is getting on; Releases
|
||||
// lists every candidate with its verdict, and Search runs one now. The
|
||||
// library's albums were put on the list as held once (#5567); taking one
|
||||
// off leaves its files and stops it being fetched back if they go.
|
||||
// library's albums were put on the list as held once (#5567), each matched to
|
||||
// MusicBrainz (#5569); taking one off leaves its files and stops it being
|
||||
// fetched back if they go. The ones no match was found for are named here.
|
||||
|
||||
let { list }: { list: WantState } = $props();
|
||||
|
||||
@@ -26,6 +27,7 @@
|
||||
const library = $derived(wants.data?.pages[0]?.library);
|
||||
|
||||
let busy = $state<string | null>(null);
|
||||
let showUnmatched = $state(false);
|
||||
let searching = $state<string | null>(null);
|
||||
let open = $state<string | null>(null);
|
||||
|
||||
@@ -162,16 +164,46 @@
|
||||
{#if list === 'held' && library}
|
||||
{#if !library.listed_at}
|
||||
<p class="mb-3 text-sm text-text-secondary" data-testid="library-listing">
|
||||
Your library's albums are being added to this list as held. Albums whose tags name only a
|
||||
release are looked up on MusicBrainz first, about one a second.
|
||||
Your library's albums are being added to this list as held. Albums whose tags don't name them
|
||||
are matched on MusicBrainz first, by their artist, title and tracks, about one a second.
|
||||
</p>
|
||||
{:else if library.unlisted > 0}
|
||||
<p class="mb-3 text-sm text-text-secondary" data-testid="library-listing">
|
||||
{library.unlisted === 1 ? '1 album' : `${library.unlisted} albums`} in your library
|
||||
{library.unlisted === 1 ? "isn't" : "aren't"} on this list: nothing in {library.unlisted === 1 ? 'its' : 'their'}
|
||||
tags names {library.unlisted === 1 ? 'it' : 'them'} to MusicBrainz. Those are never fetched back if
|
||||
their files go missing.
|
||||
</p>
|
||||
<div class="mb-3 rounded-lg border border-border bg-surface p-3 text-sm" data-testid="library-listing">
|
||||
<p class="text-text-primary">
|
||||
{library.unlisted === 1 ? '1 album' : `${library.unlisted} albums`} in your library
|
||||
{library.unlisted === 1 ? "isn't" : "aren't"} on this list: no MusicBrainz album matched
|
||||
{library.unlisted === 1 ? 'its' : 'their'} artist, title and tracks. Until
|
||||
{library.unlisted === 1 ? "it's" : "they're"} matched, {library.unlisted === 1 ? "it isn't" : "they aren't"}
|
||||
fetched back if {library.unlisted === 1 ? 'its' : 'their'} files go missing.
|
||||
</p>
|
||||
{#if library.unmatched.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
class="mt-2 inline-flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary"
|
||||
aria-expanded={showUnmatched}
|
||||
onclick={() => (showUnmatched = !showUnmatched)}
|
||||
>
|
||||
<ChevronDown size={12} strokeWidth={2} class={showUnmatched ? 'rotate-180' : ''} />
|
||||
{showUnmatched ? 'Hide them' : 'Show them'}
|
||||
</button>
|
||||
{#if showUnmatched}
|
||||
<ul class="mt-2 space-y-2" data-testid="unmatched-albums">
|
||||
{#each library.unmatched as u (u.album_id)}
|
||||
<li>
|
||||
<a href="/albums/{u.album_id}" class="text-text-primary hover:underline">{u.title}</a>
|
||||
<span class="text-text-secondary">· {u.artist}</span>
|
||||
<div class="text-xs text-text-muted">{u.detail}</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if library.unmatched.length < library.unlisted}
|
||||
<p class="mt-2 text-xs text-text-muted">
|
||||
The first {library.unmatched.length} of {library.unlisted}.
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/svelte";
|
||||
import { mockInfiniteQuery, mockQuery } from "../../../test-utils/query";
|
||||
import type {
|
||||
LibraryListing,
|
||||
Want,
|
||||
WantGrab,
|
||||
WantsPage,
|
||||
} from "#lib/api/types.js";
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
||||
import { mockInfiniteQuery, mockQuery } from '../../../test-utils/query';
|
||||
import type { LibraryListing, Want, WantGrab, WantsPage } from '#lib/api/types.js';
|
||||
|
||||
vi.mock("#lib/api/wanted.js", () => ({
|
||||
vi.mock('#lib/api/wanted.js', () => ({
|
||||
createWantsQuery: vi.fn(),
|
||||
removeWant: vi.fn().mockResolvedValue(undefined),
|
||||
searchWant: vi.fn().mockResolvedValue({
|
||||
outcome: "nothing",
|
||||
considered: 0,
|
||||
best_release: "",
|
||||
best_verdict: "",
|
||||
narrowed: [],
|
||||
outcome: 'nothing', considered: 0, best_release: '', best_verdict: '', narrowed: []
|
||||
}),
|
||||
createCandidatesQuery: vi.fn(),
|
||||
grabCandidate: vi.fn(),
|
||||
blockCandidate: vi.fn(),
|
||||
unblockRelease: vi.fn(),
|
||||
unblockRelease: vi.fn()
|
||||
}));
|
||||
vi.mock("#lib/stores/toast.svelte.js", () => ({ pushToast: vi.fn() }));
|
||||
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
|
||||
|
||||
import WantList from "./WantList.svelte";
|
||||
import { pushToast } from "#lib/stores/toast.svelte.js";
|
||||
import {
|
||||
createCandidatesQuery,
|
||||
createWantsQuery,
|
||||
removeWant,
|
||||
searchWant,
|
||||
} from "#lib/api/wanted.js";
|
||||
import type { WantState } from "#lib/api/types.js";
|
||||
import WantList from './WantList.svelte';
|
||||
import { pushToast } from '#lib/stores/toast.svelte.js';
|
||||
import { createCandidatesQuery, createWantsQuery, removeWant, searchWant } from '#lib/api/wanted.js';
|
||||
import type { WantState } from '#lib/api/types.js';
|
||||
|
||||
const DAY = 24 * 3_600_000;
|
||||
const ago = (ms: number) => new Date(Date.now() - ms).toISOString();
|
||||
@@ -42,305 +28,203 @@ function want(id: string, extra: Partial<Want> = {}): Want {
|
||||
return {
|
||||
id,
|
||||
release_group_mbid: `rg-${id}`,
|
||||
title: "The Ballad of Darren",
|
||||
artist_mbid: "blur",
|
||||
artist_name: "Blur",
|
||||
artist_id: "art-1",
|
||||
primary_type: "Album",
|
||||
title: 'The Ballad of Darren',
|
||||
artist_mbid: 'blur',
|
||||
artist_name: 'Blur',
|
||||
artist_id: 'art-1',
|
||||
primary_type: 'Album',
|
||||
secondary_types: [],
|
||||
year: "2023",
|
||||
reason: "follow",
|
||||
year: '2023',
|
||||
reason: 'follow',
|
||||
wanted_at: ago(2 * DAY),
|
||||
held_at: null,
|
||||
image_url: "",
|
||||
...extra,
|
||||
image_url: '',
|
||||
...extra
|
||||
};
|
||||
}
|
||||
|
||||
function page(
|
||||
items: Want[],
|
||||
library: LibraryListing = { listed_at: ago(DAY), unlisted: 0 },
|
||||
library: LibraryListing = { listed_at: ago(DAY), unlisted: 0, unmatched: [] }
|
||||
): WantsPage {
|
||||
return {
|
||||
items,
|
||||
total: items.length,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
counts: {
|
||||
wanted: 2,
|
||||
missing: 1,
|
||||
held: 4,
|
||||
following: 1,
|
||||
activity: 1,
|
||||
attention: 0,
|
||||
blocked: 0,
|
||||
},
|
||||
library,
|
||||
counts: { wanted: 2, missing: 1, held: 4, following: 1, activity: 1, attention: 0, blocked: 0 },
|
||||
library
|
||||
};
|
||||
}
|
||||
|
||||
function grab(extra: Partial<WantGrab> = {}): WantGrab {
|
||||
return {
|
||||
id: "g1",
|
||||
name: "Blur - Blur (1997) [FLAC]",
|
||||
id: 'g1',
|
||||
name: 'Blur - Blur (1997) [FLAC]',
|
||||
grabbed_at: ago(60_000),
|
||||
origin: "schedule",
|
||||
quality: "lossless",
|
||||
origin: 'schedule',
|
||||
quality: 'lossless',
|
||||
live: true,
|
||||
end_reason: "",
|
||||
end_reason: '',
|
||||
progress: 0,
|
||||
import_state: "",
|
||||
import_reason: "",
|
||||
end_detail: "",
|
||||
import_state: '',
|
||||
import_reason: '',
|
||||
end_detail: '',
|
||||
imported_at: null,
|
||||
imported_files: 0,
|
||||
...extra,
|
||||
...extra
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
function setup(
|
||||
items: Want[] = [
|
||||
want("w1"),
|
||||
want("w2", { reason: "request", requested_by: "sam", title: "Parklife" }),
|
||||
],
|
||||
state: WantState = "wanted",
|
||||
library?: LibraryListing,
|
||||
items: Want[] = [want('w1'), want('w2', { reason: 'request', requested_by: 'sam', title: 'Parklife' })],
|
||||
state: WantState = 'wanted',
|
||||
library?: LibraryListing
|
||||
) {
|
||||
vi.mocked(createWantsQuery).mockReturnValue(
|
||||
mockInfiniteQuery({
|
||||
pages: [page(items, library)],
|
||||
}) as unknown as ReturnType<typeof createWantsQuery>,
|
||||
mockInfiniteQuery({ pages: [page(items, library)] }) as unknown as ReturnType<typeof createWantsQuery>
|
||||
);
|
||||
vi.mocked(createCandidatesQuery).mockReturnValue(
|
||||
mockQuery({ data: [] }) as unknown as ReturnType<
|
||||
typeof createCandidatesQuery
|
||||
>,
|
||||
mockQuery({ data: [] }) as unknown as ReturnType<typeof createCandidatesQuery>
|
||||
);
|
||||
return render(WantList, { list: state });
|
||||
}
|
||||
|
||||
describe("WantList", () => {
|
||||
test("asks for its own state", () => {
|
||||
setup([], "missing");
|
||||
expect(createWantsQuery).toHaveBeenCalledWith("missing");
|
||||
describe('WantList', () => {
|
||||
test('asks for its own state', () => {
|
||||
setup([], 'missing');
|
||||
expect(createWantsQuery).toHaveBeenCalledWith('missing');
|
||||
expect(screen.getByText(/no missing albums are wanted back/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("a want says why it is wanted", () => {
|
||||
test('a want says why it is wanted', () => {
|
||||
setup();
|
||||
const why = screen
|
||||
.getAllByTestId("want-why")
|
||||
.map((e) => e.textContent ?? "");
|
||||
const why = screen.getAllByTestId('want-why').map((e) => e.textContent ?? '');
|
||||
expect(why[0]).toMatch(/new from a followed artist/i);
|
||||
expect(why[1]).toMatch(/requested by sam/i);
|
||||
});
|
||||
|
||||
test("remove takes a want off the list", async () => {
|
||||
test('remove takes a want off the list', async () => {
|
||||
setup();
|
||||
await fireEvent.click(
|
||||
screen.getByRole("button", { name: /stop wanting parklife/i }),
|
||||
);
|
||||
await waitFor(() => expect(removeWant).toHaveBeenCalledWith("w2"));
|
||||
await fireEvent.click(screen.getByRole('button', { name: /stop wanting parklife/i }));
|
||||
await waitFor(() => expect(removeWant).toHaveBeenCalledWith('w2'));
|
||||
});
|
||||
|
||||
test("a held album can be taken off the list, and is not searched for", async () => {
|
||||
setup(
|
||||
[want("w1", { held_at: ago(DAY), album_id: "al-1", reason: "library" })],
|
||||
"held",
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /stop wanting|search for/i }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
screen
|
||||
.getByRole("link", { name: /the ballad of darren/i })
|
||||
.getAttribute("href"),
|
||||
).toBe("/albums/al-1");
|
||||
expect(screen.getByTestId("want-why").textContent).toMatch(
|
||||
/in your library when the list began/i,
|
||||
);
|
||||
await fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: /take the ballad of darren off the list/i,
|
||||
}),
|
||||
);
|
||||
await waitFor(() => expect(removeWant).toHaveBeenCalledWith("w1"));
|
||||
test('a held album can be taken off the list, and is not searched for', async () => {
|
||||
setup([want('w1', { held_at: ago(DAY), album_id: 'al-1', reason: 'library' })], 'held');
|
||||
expect(screen.queryByRole('button', { name: /stop wanting|search for/i })).toBeNull();
|
||||
expect(screen.getByRole('link', { name: /the ballad of darren/i }).getAttribute('href')).toBe('/albums/al-1');
|
||||
expect(screen.getByTestId('want-why').textContent).toMatch(/in your library when the list began/i);
|
||||
await fireEvent.click(screen.getByRole('button', { name: /take the ballad of darren off the list/i }));
|
||||
await waitFor(() => expect(removeWant).toHaveBeenCalledWith('w1'));
|
||||
});
|
||||
|
||||
test("the held tab says while the library is being listed, and what could not be", () => {
|
||||
setup([], "held", { listed_at: null, unlisted: 0 });
|
||||
expect(screen.getByTestId("library-listing").textContent).toMatch(
|
||||
/being added to this list/i,
|
||||
);
|
||||
test('the held tab says while the library is being listed', () => {
|
||||
setup([], 'held', { listed_at: null, unlisted: 0, unmatched: [] });
|
||||
expect(screen.getByTestId('library-listing').textContent).toMatch(/being added to this list/i);
|
||||
});
|
||||
|
||||
test("the held tab names how many albums nothing names to MusicBrainz", () => {
|
||||
setup([want("w1", { held_at: ago(DAY) })], "held", {
|
||||
test('the held tab names the albums no MusicBrainz album matched, on request', async () => {
|
||||
setup([want('w1', { held_at: ago(DAY) })], 'held', {
|
||||
listed_at: ago(DAY),
|
||||
unlisted: 12,
|
||||
unmatched: [
|
||||
{ album_id: 'al-9', title: 'Demo Tape', artist: 'Blur', detail: 'MusicBrainz has no album called "Demo Tape" by Blur.' }
|
||||
]
|
||||
});
|
||||
expect(screen.getByTestId("library-listing").textContent).toMatch(
|
||||
/12 albums in your library\s+aren't on this list/i,
|
||||
);
|
||||
expect(screen.getByTestId('library-listing').textContent).toMatch(/12 albums in your library\s+aren't on this list/i);
|
||||
expect(screen.queryByTestId('unmatched-albums')).toBeNull();
|
||||
await fireEvent.click(screen.getByRole('button', { name: /show them/i }));
|
||||
const list = screen.getByTestId('unmatched-albums');
|
||||
expect(list.textContent).toMatch(/Demo Tape/);
|
||||
expect(list.textContent).toMatch(/has no album called/);
|
||||
expect(screen.getByRole('link', { name: 'Demo Tape' }).getAttribute('href')).toBe('/albums/al-9');
|
||||
expect(screen.getByText(/the first 1 of 12/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("an empty list explains what lands there", () => {
|
||||
test('an empty list explains what lands there', () => {
|
||||
setup([]);
|
||||
expect(screen.getByText(/nothing is wanted/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("each open want says what its last search found", () => {
|
||||
test('each open want says what its last search found', () => {
|
||||
setup([
|
||||
want("w1"),
|
||||
want("w2", {
|
||||
title: "Parklife",
|
||||
search: {
|
||||
searched_at: ago(3_600_000),
|
||||
outcome: "nothing",
|
||||
considered: 4,
|
||||
best_release: "x",
|
||||
best_verdict: "rejected",
|
||||
error: "",
|
||||
},
|
||||
}),
|
||||
want("w3", { title: "Blur", grab: grab() }),
|
||||
want("w4", {
|
||||
title: "13",
|
||||
grab: grab({ import_state: "downloading", progress: 0.42 }),
|
||||
want('w1'),
|
||||
want('w2', {
|
||||
title: 'Parklife',
|
||||
search: { searched_at: ago(3_600_000), outcome: 'nothing', considered: 4, best_release: 'x', best_verdict: 'rejected', error: '' }
|
||||
}),
|
||||
want('w3', { title: 'Blur', grab: grab() }),
|
||||
want('w4', { title: '13', grab: grab({ import_state: 'downloading', progress: 0.42 }) })
|
||||
]);
|
||||
const lines = screen
|
||||
.getAllByTestId("want-search")
|
||||
.map((e) => e.textContent ?? "");
|
||||
const lines = screen.getAllByTestId('want-search').map((e) => e.textContent ?? '');
|
||||
expect(lines[0]).toMatch(/not searched yet/i);
|
||||
expect(lines[1]).toMatch(/4 found, none of them this album/i);
|
||||
expect(lines[2]).toMatch(
|
||||
/downloading blur - blur \(1997\) \[flac\], taken by the schedule/i,
|
||||
);
|
||||
expect(lines[2]).toMatch(/downloading blur - blur \(1997\) \[flac\], taken by the schedule/i);
|
||||
expect(lines[3]).toMatch(/downloading 42%/i);
|
||||
});
|
||||
|
||||
test("a download waiting on a person says why and points to Activity", () => {
|
||||
test('a download waiting on a person says why and points to Activity', () => {
|
||||
setup([
|
||||
want("w1", {
|
||||
grab: grab({
|
||||
import_state: "review",
|
||||
import_reason: "Only the lengths agree. A person decides.",
|
||||
}),
|
||||
}),
|
||||
want('w1', { grab: grab({ import_state: 'review', import_reason: 'Only the lengths agree. A person decides.' }) })
|
||||
]);
|
||||
const line = screen.getByTestId("want-search");
|
||||
const line = screen.getByTestId('want-search');
|
||||
expect(line.textContent).toMatch(/needs you: only the lengths agree/i);
|
||||
expect(
|
||||
screen
|
||||
.getByRole("link", { name: /decide on activity/i })
|
||||
.getAttribute("href"),
|
||||
).toBe("/wanted/activity");
|
||||
expect(screen.getByRole('link', { name: /decide on activity/i }).getAttribute('href')).toBe('/wanted/activity');
|
||||
// A live download can't be searched for again.
|
||||
expect(
|
||||
(
|
||||
screen.getByRole("button", {
|
||||
name: /search for the ballad of darren now/i,
|
||||
}) as HTMLButtonElement
|
||||
).disabled,
|
||||
).toBe(true);
|
||||
expect((screen.getByRole('button', { name: /search for the ballad of darren now/i }) as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
test("a refused download is named beside the search that follows it", () => {
|
||||
test('a refused download is named beside the search that follows it', () => {
|
||||
setup([
|
||||
want("w1", {
|
||||
search: {
|
||||
searched_at: ago(3_600_000),
|
||||
outcome: "nothing",
|
||||
considered: 0,
|
||||
best_release: "",
|
||||
best_verdict: "",
|
||||
error: "",
|
||||
},
|
||||
grab: grab({
|
||||
live: false,
|
||||
end_reason: "refused",
|
||||
import_state: "refused",
|
||||
import_reason: "Another album.",
|
||||
}),
|
||||
}),
|
||||
want('w1', {
|
||||
search: { searched_at: ago(3_600_000), outcome: 'nothing', considered: 0, best_release: '', best_verdict: '', error: '' },
|
||||
grab: grab({ live: false, end_reason: 'refused', import_state: 'refused', import_reason: 'Another album.' })
|
||||
})
|
||||
]);
|
||||
expect(screen.getByTestId("want-search").textContent).toMatch(
|
||||
/last download refused: another album/i,
|
||||
);
|
||||
expect(screen.getByTestId('want-search').textContent).toMatch(/last download refused: another album/i);
|
||||
});
|
||||
|
||||
test("a download given up on is named beside the search that follows it", () => {
|
||||
test('a download given up on is named beside the search that follows it', () => {
|
||||
setup([
|
||||
want("w1", {
|
||||
search: {
|
||||
searched_at: ago(3_600_000),
|
||||
outcome: "waiting",
|
||||
considered: 4,
|
||||
best_release: "",
|
||||
best_verdict: "",
|
||||
error: "",
|
||||
},
|
||||
grab: grab({
|
||||
live: false,
|
||||
end_reason: "stalled",
|
||||
import_state: "downloading",
|
||||
end_detail: "Nobody was sharing it.",
|
||||
}),
|
||||
}),
|
||||
want('w1', {
|
||||
search: { searched_at: ago(3_600_000), outcome: 'waiting', considered: 4, best_release: '', best_verdict: '', error: '' },
|
||||
grab: grab({ live: false, end_reason: 'stalled', import_state: 'downloading', end_detail: 'Nobody was sharing it.' })
|
||||
})
|
||||
]);
|
||||
expect(screen.getByTestId("want-search").textContent).toMatch(
|
||||
/last download given up: nobody was sharing it/i,
|
||||
);
|
||||
expect(screen.getByTestId('want-search').textContent).toMatch(/last download given up: nobody was sharing it/i);
|
||||
});
|
||||
|
||||
test("a held album says how it arrived", () => {
|
||||
test('a held album says how it arrived', () => {
|
||||
setup(
|
||||
[
|
||||
want("w1", {
|
||||
want('w1', {
|
||||
held_at: ago(DAY),
|
||||
album_id: "al-1",
|
||||
grab: grab({
|
||||
live: false,
|
||||
end_reason: "imported",
|
||||
import_state: "imported",
|
||||
imported_at: ago(DAY),
|
||||
imported_files: 11,
|
||||
}),
|
||||
}),
|
||||
album_id: 'al-1',
|
||||
grab: grab({ live: false, end_reason: 'imported', import_state: 'imported', imported_at: ago(DAY), imported_files: 11 })
|
||||
})
|
||||
],
|
||||
"held",
|
||||
);
|
||||
expect(screen.getByTestId("want-arrived").textContent).toMatch(
|
||||
/imported 11 tracks .* from blur - blur/i,
|
||||
'held'
|
||||
);
|
||||
expect(screen.getByTestId('want-arrived').textContent).toMatch(/imported 11 tracks .* from blur - blur/i);
|
||||
});
|
||||
|
||||
test("Search runs one now and says what came of it", async () => {
|
||||
test('Search runs one now and says what came of it', async () => {
|
||||
setup();
|
||||
await fireEvent.click(
|
||||
screen.getByRole("button", { name: /search for parklife now/i }),
|
||||
);
|
||||
await waitFor(() => expect(searchWant).toHaveBeenCalledWith("w2"));
|
||||
await waitFor(() =>
|
||||
expect(pushToast).toHaveBeenCalledWith(
|
||||
"Nothing found for Parklife.",
|
||||
"info",
|
||||
),
|
||||
);
|
||||
await fireEvent.click(screen.getByRole('button', { name: /search for parklife now/i }));
|
||||
await waitFor(() => expect(searchWant).toHaveBeenCalledWith('w2'));
|
||||
await waitFor(() => expect(pushToast).toHaveBeenCalledWith('Nothing found for Parklife.', 'info'));
|
||||
});
|
||||
|
||||
test("Releases opens what the last search found", async () => {
|
||||
test('Releases opens what the last search found', async () => {
|
||||
setup();
|
||||
expect(screen.queryByTestId("want-candidates")).toBeNull();
|
||||
const toggle = screen.getByRole("button", {
|
||||
name: /releases found for parklife/i,
|
||||
});
|
||||
expect(screen.queryByTestId('want-candidates')).toBeNull();
|
||||
const toggle = screen.getByRole('button', { name: /releases found for parklife/i });
|
||||
await fireEvent.click(toggle);
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(screen.getByTestId("want-candidates")).toBeTruthy();
|
||||
expect(createCandidatesQuery).toHaveBeenCalledWith("w2");
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('true');
|
||||
expect(screen.getByTestId('want-candidates')).toBeTruthy();
|
||||
expect(createCandidatesQuery).toHaveBeenCalledWith('w2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/svelte";
|
||||
import { createRawSnippet } from "svelte";
|
||||
import { mockInfiniteQuery } from "../../test-utils/query";
|
||||
import { pageUrlModule } from "#test-utils/mocks/appState.js";
|
||||
import type { WantCounts, WantsPage } from "#lib/api/types.js";
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { createRawSnippet } from 'svelte';
|
||||
import { mockInfiniteQuery } from '../../test-utils/query';
|
||||
import { pageUrlModule } from '#test-utils/mocks/appState.js';
|
||||
import type { WantCounts, WantsPage } from '#lib/api/types.js';
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
pageUrl: new URL("http://localhost/wanted"),
|
||||
}));
|
||||
vi.mock("$app/state", () => pageUrlModule(state));
|
||||
vi.mock("#lib/api/wanted.js", () => ({ createWantsQuery: vi.fn() }));
|
||||
const state = vi.hoisted(() => ({ pageUrl: new URL('http://localhost/wanted') }));
|
||||
vi.mock('$app/state', () => pageUrlModule(state));
|
||||
vi.mock('#lib/api/wanted.js', () => ({ createWantsQuery: vi.fn() }));
|
||||
|
||||
import WantedLayout from "./+layout.svelte";
|
||||
import { createWantsQuery } from "#lib/api/wanted.js";
|
||||
import WantedLayout from './+layout.svelte';
|
||||
import { createWantsQuery } from '#lib/api/wanted.js';
|
||||
|
||||
const counts: WantCounts = {
|
||||
wanted: 5,
|
||||
missing: 1,
|
||||
held: 9,
|
||||
following: 2,
|
||||
activity: 3,
|
||||
attention: 0,
|
||||
blocked: 4,
|
||||
};
|
||||
const children = createRawSnippet(() => ({
|
||||
render: () => `<p data-testid="tab-body">body</p>`,
|
||||
}));
|
||||
const counts: WantCounts = { wanted: 5, missing: 1, held: 9, following: 2, activity: 3, attention: 0, blocked: 4 };
|
||||
const children = createRawSnippet(() => ({ render: () => `<p data-testid="tab-body">body</p>` }));
|
||||
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
function setup(c: WantCounts = counts, path = "/wanted") {
|
||||
function setup(c: WantCounts = counts, path = '/wanted') {
|
||||
state.pageUrl = new URL(`http://localhost${path}`);
|
||||
const page: WantsPage = {
|
||||
items: [],
|
||||
@@ -37,58 +25,44 @@ function setup(c: WantCounts = counts, path = "/wanted") {
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
counts: c,
|
||||
library: { listed_at: null, unlisted: 0 },
|
||||
library: { listed_at: null, unlisted: 0, unmatched: [] }
|
||||
};
|
||||
vi.mocked(createWantsQuery).mockReturnValue(
|
||||
mockInfiniteQuery({ pages: [page] }) as unknown as ReturnType<
|
||||
typeof createWantsQuery
|
||||
>,
|
||||
mockInfiniteQuery({ pages: [page] }) as unknown as ReturnType<typeof createWantsQuery>
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return render(WantedLayout, { children } as any);
|
||||
}
|
||||
|
||||
describe("/wanted layout", () => {
|
||||
test("tabs are links carrying their counts, the current one marked", () => {
|
||||
setup(counts, "/wanted/activity");
|
||||
const nav = screen.getByRole("navigation", { name: /wanted sections/i });
|
||||
const links = Array.from(nav.querySelectorAll("a")).map((a) => [
|
||||
a.getAttribute("href"),
|
||||
a.textContent?.replace(/\s+/g, " ").trim(),
|
||||
]);
|
||||
describe('/wanted layout', () => {
|
||||
test('tabs are links carrying their counts, the current one marked', () => {
|
||||
setup(counts, '/wanted/activity');
|
||||
const nav = screen.getByRole('navigation', { name: /wanted sections/i });
|
||||
const links = Array.from(nav.querySelectorAll('a')).map((a) => [a.getAttribute('href'), a.textContent?.replace(/\s+/g, ' ').trim()]);
|
||||
expect(links).toEqual([
|
||||
["/wanted", "Wanted 5"],
|
||||
["/wanted/activity", "Activity 3"],
|
||||
["/wanted/missing", "Missing files 1"],
|
||||
["/wanted/held", "Held 9"],
|
||||
["/wanted/following", "Following 2"],
|
||||
["/wanted/blocked", "Blocked 4"],
|
||||
['/wanted', 'Wanted 5'],
|
||||
['/wanted/activity', 'Activity 3'],
|
||||
['/wanted/missing', 'Missing files 1'],
|
||||
['/wanted/held', 'Held 9'],
|
||||
['/wanted/following', 'Following 2'],
|
||||
['/wanted/blocked', 'Blocked 4']
|
||||
]);
|
||||
expect(screen.getByRole("link", { name: /activity/i })).toHaveAttribute(
|
||||
"aria-current",
|
||||
"page",
|
||||
);
|
||||
expect(screen.getByRole("link", { name: /^wanted/i })).not.toHaveAttribute(
|
||||
"aria-current",
|
||||
);
|
||||
expect(screen.getByTestId("tab-body")).toBeTruthy();
|
||||
expect(screen.getByRole('link', { name: /activity/i })).toHaveAttribute('aria-current', 'page');
|
||||
expect(screen.getByRole('link', { name: /^wanted/i })).not.toHaveAttribute('aria-current');
|
||||
expect(screen.getByTestId('tab-body')).toBeTruthy();
|
||||
});
|
||||
|
||||
test("when nothing needs a person the verdict says so", () => {
|
||||
test('when nothing needs a person the verdict says so', () => {
|
||||
setup();
|
||||
expect(screen.getByTestId("wanted-verdict").textContent).toMatch(
|
||||
/5 albums wanted, 3 downloading\. Nothing needs you\./,
|
||||
);
|
||||
expect(screen.queryByTestId("tab-attention")).toBeNull();
|
||||
expect(screen.getByTestId('wanted-verdict').textContent).toMatch(/5 albums wanted, 3 downloading\. Nothing needs you\./);
|
||||
expect(screen.queryByTestId('tab-attention')).toBeNull();
|
||||
});
|
||||
|
||||
test("downloads waiting on a person lead the page and mark Activity", () => {
|
||||
test('downloads waiting on a person lead the page and mark Activity', () => {
|
||||
setup({ ...counts, attention: 2 });
|
||||
const verdict = screen.getByTestId("wanted-verdict");
|
||||
const verdict = screen.getByTestId('wanted-verdict');
|
||||
expect(verdict.textContent).toMatch(/2 downloads need you/);
|
||||
expect(
|
||||
screen.getByRole("link", { name: /see activity/i }).getAttribute("href"),
|
||||
).toBe("/wanted/activity");
|
||||
expect(screen.getByTestId("tab-attention").textContent).toBe("2");
|
||||
expect(screen.getByRole('link', { name: /see activity/i }).getAttribute('href')).toBe('/wanted/activity');
|
||||
expect(screen.getByTestId('tab-attention').textContent).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user