100 lines
5.2 KiB
TypeScript
100 lines
5.2 KiB
TypeScript
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<string, unknown> = {}) =>
|
|
collect(plugin.importFile!(createTestContext({ config }), { name, bytes }));
|
|
const entries = (r: SourceRecord[]) => r.filter((x): x is Extract<SourceRecord, { kind: "entry" }> => 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<SourceRecord, { kind: "engagement" }>).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<SourceRecord, { kind: "engagement" }>[];
|
|
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();
|
|
});
|
|
|
|
const enc = (s: string) => new TextEncoder().encode(s);
|
|
test("duplicate rows get distinct deterministic keys", async () => {
|
|
const csv = "Title,Date Read\nA,2020-01-01\nA,2020-01-01\nB,\nB,\nB,\n";
|
|
const keys = async () => (await run("d.csv", enc(csv))).map((r) => r.key);
|
|
const k = await keys();
|
|
expect(new Set(k).size).toBe(k.length);
|
|
expect(k[0]).toMatch(/^row:[0-9a-f]{40}$/);
|
|
expect(k.some((x) => x.endsWith("#2"))).toBe(true);
|
|
expect(k.some((x) => x.endsWith("#3"))).toBe(true);
|
|
expect(await keys()).toEqual(k);
|
|
});
|
|
|
|
test("default mediaType is per file, not per row", async () => {
|
|
const csv = "Title,Author,ISBN13\nA,Someone,=\"9780441172719\"\nB,,\n";
|
|
const e = entries(await run("g.csv", enc(csv)));
|
|
expect(e.map((r) => r.work.mediaType)).toEqual(["book", "book"]);
|
|
const plain = entries(await run("p.csv", enc("Title\nA\n")));
|
|
expect(plain[0]!.work.mediaType).toBe("other");
|
|
const j = entries(await run("j.json", enc('[{"title":"A","author":"x"},{"title":"B"},{"title":"C","mediaType":"game"}]')));
|
|
expect(j.map((r) => r.work.mediaType)).toEqual(["book", "book", "game"]);
|
|
});
|
|
|
|
test("rating 0 is unrated only on scale 5", async () => {
|
|
const m = JSON.stringify({ T: "title", R: "rating" });
|
|
const rate = async (csv: string) => entries(await run("r.csv", enc(csv), { mapping: m })).map((r) => r.entry.rating ?? null);
|
|
expect(await rate("T,R\nA,0\nB,4\n")).toEqual([null, 80]);
|
|
expect(await rate("T,R\nA,0\nB,8\n")).toEqual([0, 80]);
|
|
expect(await rate("T,R\nA,0\nB,80\n")).toEqual([0, 80]);
|
|
});
|
|
});
|