// Unit tests for the pure candidate extraction (run with `node --test`). Zero-dep. // CJS so it can require() extract.js, which is a UMD script (also a content script). const { test } = require("node:test"); const assert = require("node:assert/strict"); const { matchesIn, cueMatchesIn, albumMatchesIn, allMatchesIn } = require("./extract.js"); test("matchesIn finds Title-Case / ALL-CAPS runs, skips stopwords", () => { // Note: "and" is a connector, so "X and Y" greedily joins into one run (by design, // for names like "Iron and Wine") — keep the two names in separate clauses here. const names = matchesIn("Cattle Decapitation is great; DEVOURMENT rules. I agree").map((m) => m.name); assert.ok(names.includes("Cattle Decapitation")); assert.ok(names.includes("DEVOURMENT")); assert.ok(!names.includes("I")); }); test("matchesIn reports positions that map back to the text", () => { const text = "Check out Devourment now"; const m = matchesIn(text).find((x) => x.name === "Devourment"); assert.equal(text.substr(m.index, m.length), "Devourment"); }); test("cueMatchesIn catches lowercase names after a cue phrase", () => { const names = cueMatchesIn("you should check out devourment honestly").map((m) => m.name); assert.ok(names.includes("devourment")); }); test("cueMatchesIn positions map back to the text", () => { const text = "i'm a big fan of cattle decapitation"; // "fan of" cue for (const m of cueMatchesIn(text)) assert.equal(text.substr(m.index, m.length), m.name); }); test("cueMatchesIn skips lowercase stopwords right after the cue", () => { const names = cueMatchesIn("check out the new album").map((m) => m.name); assert.ok(!names.includes("the")); }); test("albumMatchesIn pulls quoted strings (double + single), excluding the quotes", () => { const text = `their album "Reek of Putrefaction" and the 'Scum' demo`; const ms = albumMatchesIn(text); const names = ms.map((m) => m.name); assert.ok(names.includes("Reek of Putrefaction")); assert.ok(names.includes("Scum")); // index/length cover the inner text only (no quote chars) so the wrapper links the title for (const m of ms) assert.equal(text.substr(m.index, m.length), m.name); }); test("albumMatchesIn catches album cue phrases ('the album X', 'X LP/EP')", () => { const a = albumMatchesIn("the album Scum is a classic").map((m) => m.name); assert.ok(a.includes("Scum")); const b = albumMatchesIn("Reign in Blood LP rules").map((m) => m.name); assert.ok(b.includes("Reign in Blood")); const c = albumMatchesIn("the record called Bonus Tracks").map((m) => m.name); assert.ok(c.includes("Bonus Tracks")); }); test("albumMatchesIn cue positions map back to the text", () => { const text = "the album Scum is great"; for (const m of albumMatchesIn(text)) assert.equal(text.substr(m.index, m.length), m.name); }); test("albumMatchesIn skips pure-number and too-short quotes", () => { const names = albumMatchesIn(`"42" and "X"`).map((m) => m.name); assert.ok(!names.includes("42")); assert.ok(!names.includes("X")); // length < 2 }); test("allMatchesIn merges cue + Title-Case + album and sorts ascending by index", () => { const text = `Check out devourment and Cattle Decapitation; the album "Human Jerky" rules`; const ms = allMatchesIn(text); for (let i = 1; i < ms.length; i++) assert.ok(ms[i].index >= ms[i - 1].index); const names = ms.map((m) => m.name); assert.ok(names.includes("devourment")); // cue (lowercase) assert.ok(names.includes("Cattle Decapitation")); // title-case assert.ok(names.includes("Human Jerky")); // quoted album });