fix(api): a gone row and a refused playlist action are said in the reader's language, and a failure is reported once
Deep-review round 3 of bug hunt 2026-09.
- get_or_404 / owned_or_404 answer a structured {code: "not_found", reason: <what>}
(errors.notFound.* in HU+EN, generic fallback). The structured-404 fix of round 2 had gone into the
ad-hoc `_own_playlist` (CLAUDE.md's "Not" pattern) while the shared helpers stayed string-only, so
views, tags, pins, Plex playlists, invites and users still 404'd in English. `_own_playlist` now
goes through owned_or_404; admin's hand-rolled user lookups use get_or_404.
- A move into a list that is gone names the TARGET (reason "move_target") — "this playlist doesn't
exist any more" was read as the list on screen, which still exists.
- Every refusal in the playlists router is a {code: "playlist", reason} (playlists.errors.* HU+EN)
instead of a bare English string shown verbatim in the Hungarian UI.
- `req` / `uploadReq` mark the errors they already put on screen (the error dialog, the connection
notice, the 401 sign-out), and notifyActionError skips those. The watched toggles, the saved views
and a playlist delete toasted a fault the dialog was already showing; Playlists' two hand-written
status filters now go through the same check.
This commit is contained in:
@@ -6,9 +6,9 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
// Spy on the toast sink so we can assert exactly what each error maps to.
|
||||
vi.mock("./notifications", () => ({ notify: vi.fn(() => 0) }));
|
||||
|
||||
import { HttpError } from "./api";
|
||||
import { HttpError, api } from "./api";
|
||||
import { notify } from "./notifications";
|
||||
import { notifyCollectionEditError } from "./actionErrors";
|
||||
import { notifyActionError, notifyCollectionEditError } from "./actionErrors";
|
||||
|
||||
const notifyMock = vi.mocked(notify);
|
||||
beforeEach(() => notifyMock.mockClear());
|
||||
@@ -29,3 +29,25 @@ describe("notifyCollectionEditError", () => {
|
||||
expect(notifyMock).toHaveBeenCalledWith({ level: "error", message: "EDIT_FAILED" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifyActionError", () => {
|
||||
it("does not toast a fault the request layer already put on screen", async () => {
|
||||
// A 500 opens the blocking error dialog inside `req`; the watched toggles, the saved views and a
|
||||
// playlist delete then toasted the same failure as well.
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ detail: "boom" }), {
|
||||
status: 500,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
const err = await api.plexSetState("1", "watched").catch((e: unknown) => e);
|
||||
notifyMock.mockClear();
|
||||
notifyActionError(err, "FAILED");
|
||||
expect(notifyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does toast one the caller was left to handle", () => {
|
||||
notifyActionError(new HttpError(404, "gone"), "FAILED");
|
||||
expect(notifyMock).toHaveBeenCalledWith({ level: "error", message: "gone" });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user