import { describe, expect, test } from "bun:test"; import { collect, createTestContext } from "@sttracker/plugin-sdk/testing"; import type { SourceRecord } from "@sttracker/plugin-sdk/types"; import plugin from "../index.ts"; const fixture = async (n: string) => new Uint8Array(await Bun.file(new URL(`./fixtures/${n}`, import.meta.url)).arrayBuffer()); const run = async (name: string, bytes: Uint8Array, config: Record = {}) => collect(plugin.importFile!(createTestContext({ config }), { name, bytes })); const entries = (r: SourceRecord[]) => r.filter((x): x is Extract => x.kind === "entry"); describe("csv plugin", () => { test("manifest", () => { expect(plugin.manifest.id).toBe("csv"); expect(plugin.manifest.capabilities).toEqual(["importFile"]); expect(plugin.manifest.config.map((c) => c.key)).toEqual(["mapping"]); }); test("messy.csv: BOM, ';', quoted newline and quotes, empty rating, year date, emoji", async () => { const recs = await run("messy.csv", await fixture("messy.csv")); const e = entries(recs); expect(e.map((r) => r.work.title)).toEqual(['Line "one"', "🐉 Dragons"]); expect(e[0]!.entry.review).toBe("multi\nline"); expect(e[0]!.entry.rating).toBe(80); expect(e[1]!.entry.rating ?? null).toBeNull(); const eng = recs.filter((r) => r.kind === "engagement"); expect(eng).toHaveLength(2); expect((eng[0] as Extract).engagement.finishedAt).toEqual({ value: "2019", precision: "year" }); expect(e[0]!.key).toMatch(/^row:[0-9a-f]{40}$/); }); test("goodreads headers auto-mapped", async () => { const e = entries(await run("goodreads.csv", await fixture("goodreads.csv"))); expect(e.map((r) => r.entry.status)).toEqual(["completed", "planned", "in_progress"]); expect(e[0]!.work.title).toBe("Dune"); expect(e[0]!.work.mediaType).toBe("book"); expect(e[0]!.work.creators).toEqual([{ name: "Frank Herbert" }]); expect(e[0]!.work.externalIds).toEqual({ isbn13: "9780441172719" }); expect(e[0]!.entry.rating).toBe(80); expect(e[0]!.entry.tags).toEqual(["sci-fi", "classics"]); expect(e[1]!.entry.rating ?? null).toBeNull(); expect(e[1]!.work.externalIds).toEqual({}); const eng = (await run("goodreads.csv", await fixture("goodreads.csv"))).filter((r) => r.kind === "engagement") as Extract[]; expect(eng).toHaveLength(1); expect(eng[0]!.engagement.finishedAt).toEqual({ value: "2021-03-04", precision: "day" }); }); test("explicit mapping and rating scale default", async () => { const csv = "Name,Score,Kind\nA,7,movie\nB,10,game\n"; const mapping = JSON.stringify({ Name: "title", Score: "rating", Kind: "mediaType" }); const e = entries(await run("x.csv", new TextEncoder().encode(csv), { mapping })); expect(e.map((r) => r.entry.rating)).toEqual([70, 100]); expect(e.map((r) => r.work.mediaType)).toEqual(["movie", "game"]); }); test("json ignores mapping", async () => { const json = JSON.stringify([{ title: "T", mediaType: "movie", rating: 3, status: "completed", finishedAt: "2020-01-02", tags: ["a"], imdb: "tt1" }]); const recs = await run("x.json", new TextEncoder().encode(json), { mapping: '{"title":"notes"}' }); const e = entries(recs); expect(e[0]!.work.title).toBe("T"); expect(e[0]!.entry.rating).toBe(60); expect(e[0]!.entry.tags).toEqual(["a"]); expect(e[0]!.work.externalIds).toEqual({ imdb: "tt1" }); expect(recs).toHaveLength(2); }); test("unsupported extension throws", async () => { await expect(run("x.txt", new Uint8Array())).rejects.toThrow(); }); });