Files
minstrel/web/src/lib/components/AcoustIDCard.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

182 lines
5.6 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 } from "@testing-library/svelte";
import type { AcoustIDOverview } from "#lib/api/admin.js";
vi.mock("#lib/api/admin.js", () => ({
getAcoustID: vi.fn(),
updateAcoustIDSettings: vi.fn(),
runAcoustIDLookup: vi.fn(),
listUnsettledAcoustID: vi.fn(),
}));
vi.mock("#lib/stores/toast.svelte.js", () => ({ pushToast: vi.fn() }));
import AcoustIDCard from "./AcoustIDCard.svelte";
import {
getAcoustID,
listUnsettledAcoustID,
runAcoustIDLookup,
updateAcoustIDSettings,
} from "#lib/api/admin.js";
const base: AcoustIDOverview = {
settings: { enabled: true, min_score: 0.85, api_key_set: true },
ready: true,
status: { running: false, last_pass_at: null, problem: "" },
coverage: {
total: 1000,
from_tag: 700,
from_acoustid: 150,
ambiguous: 12,
no_match: 80,
failed: 3,
pending: 55,
},
};
function overview(over: Partial<AcoustIDOverview> = {}): AcoustIDOverview {
return { ...base, ...over, settings: { ...base.settings, ...over.settings } };
}
afterEach(() => vi.clearAllMocks());
async function renderCard(o: AcoustIDOverview = base) {
vi.mocked(getAcoustID).mockResolvedValue(o);
render(AcoustIDCard);
await screen.findByRole("switch", { name: /look up untagged tracks/i });
}
const saveButton = () => screen.getByRole("button", { name: /^save$/i });
const lookUpNow = () => screen.getByRole("button", { name: /look up now/i });
describe("AcoustIDCard", () => {
test("shows where the library’s recording ids came from", async () => {
await renderCard();
const cov = screen.getByTestId("acoustid-coverage").textContent?.replace(/\s+/g, " ");
// 1000 - 700 - 150 have no id.
expect(cov).toMatch(/Recording ids 700 from tags · 150 looked up · 150 none/);
expect(cov).toMatch(/Without one 55 waiting · 80 no match · 12 ambiguous · 3 failed/);
});
test("never shows the stored key, and sends one only when typed", async () => {
vi.mocked(updateAcoustIDSettings).mockResolvedValue(base.settings);
await renderCard();
const key = screen.getByLabelText(/api key/i) as HTMLInputElement;
expect(key.value).toBe("");
expect(key.placeholder).toBe("Stored");
expect(saveButton()).toHaveProperty("disabled", true);
await fireEvent.input(
screen.getByRole("spinbutton", { name: /minimum score/i }),
{
target: { value: "0.9" },
},
);
await fireEvent.click(saveButton());
await waitFor(() =>
expect(updateAcoustIDSettings).toHaveBeenCalledWith({
enabled: true,
min_score: 0.9,
}),
);
await fireEvent.input(key, { target: { value: " new-key " } });
await fireEvent.click(saveButton());
await waitFor(() =>
expect(updateAcoustIDSettings).toHaveBeenLastCalledWith({
enabled: true,
min_score: 0.85,
api_key: "new-key",
}),
);
});
test("a score outside 0.5 to 1 is named and cannot be saved", async () => {
await renderCard();
await fireEvent.input(
screen.getByRole("spinbutton", { name: /minimum score/i }),
{
target: { value: "0.3" },
},
);
await waitFor(() =>
expect(screen.getByTestId("acoustid-problems").textContent).toMatch(
/0\.5 to 1/,
),
);
expect(saveButton()).toHaveProperty("disabled", true);
});
// Rule 164: off, keyless or unreachable is a visible state, not a silent one.
test("says why it is not looking anything up", async () => {
await renderCard(
overview({
ready: false,
settings: { enabled: true, min_score: 0.85, api_key_set: false },
}),
);
expect(screen.getByTestId("acoustid-state").textContent).toMatch(
/needs a key/i,
);
expect(lookUpNow()).toHaveProperty("disabled", true);
});
test("a pass that stopped short says why", async () => {
await renderCard(
overview({
status: {
running: false,
last_pass_at: "2026-10-07T03:00:00Z",
problem: "AcoustID refused the API key. Check it in Settings.",
},
}),
);
expect(screen.getByTestId("acoustid-problem").textContent).toMatch(
/refused the API key/,
);
expect(screen.getByTestId("acoustid-state").textContent).toMatch(
/needs attention/i,
);
});
test("look up now starts a pass", async () => {
vi.mocked(runAcoustIDLookup).mockResolvedValue();
await renderCard();
await fireEvent.click(lookUpNow());
await waitFor(() => expect(runAcoustIDLookup).toHaveBeenCalledTimes(1));
});
test("the tracks a lookup could not settle can be found", async () => {
vi.mocked(listUnsettledAcoustID).mockResolvedValue([
{
track_id: "t1",
title: "WWW",
artist: "Moe Shop",
album: "WWW",
file_path: "/music/a.mp3",
state: "ambiguous",
best_score: 0.97,
candidates: 2,
detail: null,
looked_up_at: "2026-10-07T03:00:00Z",
},
]);
await renderCard();
const details = screen
.getByText(/couldn't settle \(15\)/i)
.closest("details")!;
details.open = true;
await fireEvent(details, new Event("toggle"));
const list = await screen.findByTestId("acoustid-unsettled");
expect(list.textContent).toMatch(/WWW/);
expect(list.textContent).toMatch(/2 recordings/);
});
test("a failed load offers a retry", async () => {
vi.mocked(getAcoustID).mockRejectedValue(new Error("boom"));
render(AcoustIDCard);
await screen.findByText(/couldn't load acoustid settings/i);
expect(screen.getByRole("button", { name: /try again/i })).toBeTruthy();
});
});