Régression worker PDF : le worker pdf.js est désormais instancié une seule fois via un port dédié (?worker&url) et reconfiguré à chaque montage, au lieu d'un workerSrc recalculé qui cassait le rendu. - ReaderShell : chrome commun aux lecteurs (toolbar, zones de tap, statut) et contrat ReaderControls pour EPUB/PDF/CBZ - préférences de lecture par livre (mode horizontal/vertical, fit) : table reader_preferences + migrations idempotentes, module API, DTO partagés, client web avec fallback localStorage hors-ligne Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { api, ApiFallbackError, getApiFallback } from "./client";
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("api fallback helpers", () => {
|
|
it("extracts typed fallback payloads", () => {
|
|
expect(getApiFallback<string[]>(new ApiFallbackError("offline", ["demo"]))).toEqual(["demo"]);
|
|
});
|
|
|
|
it("ignores non fallback errors", () => {
|
|
expect(getApiFallback<string[]>(new Error("boom"))).toBeUndefined();
|
|
});
|
|
|
|
it("does not send JSON content-type for bodyless delete requests", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" }
|
|
})
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await api.deleteLibrary(42);
|
|
|
|
const init = fetchMock.mock.calls[0][1] as RequestInit;
|
|
const headers = new Headers(init.headers);
|
|
expect(init.method).toBe("DELETE");
|
|
expect(headers.has("Content-Type")).toBe(false);
|
|
});
|
|
|
|
it("surfaces create library API errors without fallback", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ message: "Library path does not exist" }), {
|
|
status: 400,
|
|
statusText: "Bad Request",
|
|
headers: { "Content-Type": "application/json" }
|
|
})
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await expect(api.createLibrary({ name: "Books", path: "/missing", enabled: true })).rejects.toThrow("Library path does not exist");
|
|
});
|
|
|
|
it("does not fallback when scan enqueue fails", async () => {
|
|
const fetchMock = vi.fn().mockRejectedValue(new Error("offline"));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await expect(api.scanLibrary(42)).rejects.toThrow("offline");
|
|
});
|
|
|
|
it("keeps reader preferences locally when the backend contract is absent", async () => {
|
|
const storage = new Map<string, string>();
|
|
vi.stubGlobal("localStorage", {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => storage.set(key, value),
|
|
removeItem: (key: string) => storage.delete(key),
|
|
clear: () => storage.clear()
|
|
});
|
|
const fetchMock = vi.fn().mockResolvedValue(new Response("", { status: 404, statusText: "Not Found" }));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await expect(api.readerPreferences(8)).resolves.toEqual({ mode: "horizontal", fit: "page" });
|
|
await expect(api.saveReaderPreferences(8, { mode: "vertical", fit: "width" })).resolves.toEqual({ mode: "vertical", fit: "width" });
|
|
expect(storage.get("readabook:reader-preferences:8")).toBe(JSON.stringify({ mode: "vertical", fit: "width" }));
|
|
});
|
|
|
|
it("sends metadata source updates to the admin endpoint", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ isbnPriorityEnabled: false, sources: [] }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" }
|
|
})
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await api.updateMetadataSources({
|
|
isbnPriorityEnabled: false,
|
|
sources: [{ provider: "openlibrary", enabled: true, priority: 1 }]
|
|
});
|
|
|
|
expect(fetchMock.mock.calls[0][0]).toBe("/admin/metadata-sources");
|
|
const init = fetchMock.mock.calls[0][1] as RequestInit;
|
|
expect(init.method).toBe("PUT");
|
|
expect(JSON.parse(init.body as string)).toEqual({
|
|
isbnPriorityEnabled: false,
|
|
sources: [{ provider: "openlibrary", enabled: true, priority: 1 }]
|
|
});
|
|
});
|
|
|
|
it("sends automation settings to the admin endpoint", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
watchLibraries: true,
|
|
autoEnrichNewBooks: true,
|
|
scanSchedule: { frequency: "daily", time: "03:00", dayOfWeek: 1 },
|
|
enrichSchedule: { frequency: "disabled", time: "04:00", dayOfWeek: 1 }
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" }
|
|
}
|
|
)
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await api.updateAutomationSettings({
|
|
watchLibraries: true,
|
|
scanSchedule: { frequency: "daily", time: "03:00", dayOfWeek: 1 }
|
|
});
|
|
|
|
expect(fetchMock.mock.calls[0][0]).toBe("/admin/automation");
|
|
const init = fetchMock.mock.calls[0][1] as RequestInit;
|
|
expect(init.method).toBe("PUT");
|
|
expect(JSON.parse(init.body as string)).toEqual({
|
|
watchLibraries: true,
|
|
scanSchedule: { frequency: "daily", time: "03:00", dayOfWeek: 1 }
|
|
});
|
|
});
|
|
});
|