/** * Library suggestions on the Overview: one button adds the Library item * through the normal write API, Dismiss hides it. The suggestion list is * seeded the way the Suwayomi job leaves it in the sync cache. */ import type { AddressInfo } from "node:net"; import type { Server } from "node:http"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { parseTokens } from "../../content/src/config.ts"; import { createHttpServer } from "../../content/src/http.ts"; import { SUGGESTIONS_KEY } from "../../content/src/sync/jobs/reader.ts"; import { rig, type Rig } from "../../content/test/helpers.ts"; import { ContentApi } from "../src/client.ts"; import { createAdminServer } from "../src/server.ts"; const ADMIN = "s".repeat(24); let r: Rig; let content: Server; let admin: Server; let base: string; const listen = async (s: Server) => { await new Promise((ok) => s.listen(0, "127.0.0.1", ok)); return `http://127.0.0.1:${(s.address() as AddressInfo).port}`; }; const page = (path: string) => fetch(`${base}${path}`).then((res) => res.text()); const flash = (res: Response) => new URL(res.headers.get("location") ?? "/", "http://admin").searchParams; const post = (path: string, form: Record) => fetch(`${base}${path}`, { method: "POST", redirect: "manual", headers: { "content-type": "application/x-www-form-urlencoded", "sec-fetch-site": "same-origin" }, body: new URLSearchParams(form).toString(), }); beforeAll(async () => { r = rig(); r.service.scan(); r.store.syncCache.set( "suwayomi", SUGGESTIONS_KEY, { items: [ { anilistId: 100004, title: "New Sample Series", medium: "manhwa" }, { anilistId: 100006, title: "Another ", medium: "manga" }, ], }, "2026-10-11T12:00:00Z", ); content = createHttpServer(r.service, { tokens: parseTokens(`admin:${ADMIN}`) }); admin = createAdminServer(new ContentApi(await listen(content), ADMIN)); base = await listen(admin); }); afterAll(() => { admin.close(); content.close(); r.done(); }); describe("Library suggestions", () => { it("show on the Overview, escaped", async () => { const html = await page("/"); expect(html).toContain("Library suggestions"); expect(html).toContain("New Sample Series"); expect(html).toContain("Another <Sample>"); expect(html).toContain('value="anilist-100004"'); }); it("add creates the Library item through the write API, and the suggestion leaves", async () => { const res = await post("/suggestions/add", { key: "anilist-100004" }); expect(res.status).toBe(303); expect(flash(res).get("msg")).toBe("Added New Sample Series to the Library as Now."); const note = r.store.list({ type: "library-item" }).find((n) => n.data.anilistId === 100004)!; expect(note.data).toMatchObject({ title: "New Sample Series", medium: "manhwa", state: "now", publish: true, updatedBy: "admin" }); expect(r.vault.exists(note.path)).toBe(true); expect(await page("/")).not.toContain("New Sample Series"); }); it("dismiss hides it for good; the panel goes when nothing is left", async () => { expect((await post("/suggestions/dismiss", { key: "anilist-100006" })).status).toBe(303); const html = await page("/"); expect(html).not.toContain("Library suggestions"); // A gone suggestion cannot be added. const again = await post("/suggestions/add", { key: "anilist-100006" }); expect(flash(again).get("err")).toContain("That suggestion is gone"); }); });