tests/landing.test.ts
Rama main · réplica de lectura del repositorio original.
// Landing on the line: the wire numbers a click stakes everything on.
//
// Backlinks and search matches now carry a 1-based line in the note's FULL
// source — frontmatter included — because that is the coordinate the editor
// counts in. The indexer parses `body`, which LOST the frontmatter block, so
// the offset arithmetic here is the whole feature: off by the size of a
// properties block, every landing sits above the mention it promised.
import assert from "node:assert/strict";
import { after, before, describe, it } from "node:test";
import { backlinks, initIndexer, searchMatches } from "../server/indexer.ts";
import { initSite } from "../server/site.ts";
import { initVault } from "../server/vault.ts";
import { makeDir, makeVault, note, removeVault } from "./helpers/vault.ts";
const data = makeDir();
// `note(fm, body)` writes `---\n<one line per key>\n---\n<body>`, so a block
// with K keys costs K + 2 file lines and body line N is file line N + K + 2.
const fmLines = (keys: number): number => keys + 2;
const root = makeVault({
"Target.md": "# Target\n",
// Frontmatter + the mention on the 4th BODY line = file line 7.
"WithFm.md": note(
{ publish: "true", tags: "x" },
"intro line\n\nmore prose here\nA mention of [[Target]] mid-note.\n",
),
// No frontmatter: body lines ARE file lines. Two mentions, one per line.
"Bare.md": "First [[Target]] here.\nplain\nSecond [[Target]] here.\n",
"Findable.md": note(
{ publish: "true" },
[
"# Heading about tulips",
"",
"The tulip season starts in March.",
"Nothing relevant on this line.",
"TULIPS again, uppercased.",
// One very long hard-wrapped line: the quote must window, not ship it.
`${"pad ".repeat(120)}a tulip buried mid-paragraph ${"pad ".repeat(120)}`,
].join("\n") + "\n",
),
// A TABLE, which is fields and not a sentence. The link and the search term
// each sit in a cell that is not the first one (UX audit F44: the context
// line used to be every cell of the row joined with " · ").
"Shelf.md": [
"# Shelf",
"",
"| Title | Author | Note |",
"| --- | --- | --- |",
"| Dune | Herbert | see [[Target]] for the reread |",
"| Piranesi | Clarke | a tulip in the vestibule |",
].join("\n") + "\n",
"Unpublished.md": "secret tulip\n",
// 150 matching lines: the cap must hold at 100.
"Many.md": Array.from({ length: 150 }, (_, i) => `tulip row ${i}`).join("\n") + "\n",
});
before(async () => {
initSite({ ASTROLABE_DATA: data });
initVault(root);
await initIndexer();
});
after(() => {
removeVault(root);
removeVault(data);
});
describe("backlink lines", () => {
it("counts frontmatter back in: the wire line is a FULL-file line", () => {
const hits = backlinks("Target.md", false, null);
const fromFm = hits.find((h) => h.path === "WithFm.md");
assert.ok(fromFm, "WithFm.md links to Target and must appear");
// body line 4 ("A mention of…") + the 2-key frontmatter block.
assert.equal(fromFm.line, 4 + fmLines(2));
});
it("is 1-based and unshifted when there is no frontmatter", () => {
const lines = backlinks("Target.md", false, null)
.filter((h) => h.path === "Bare.md")
.map((h) => h.line)
.sort((a, b) => a - b);
assert.deepEqual(lines, [1, 3]);
});
});
describe("context lines out of a table", () => {
it("quotes the CELL the link is in, not a join of the whole row", () => {
const hit = backlinks("Target.md", false, null).find((h) => h.path === "Shelf.md");
assert.ok(hit, "the table row links to Target and must appear");
assert.ok(hit.context.includes("[[Target]]"), hit.context);
assert.ok(!hit.context.includes("Herbert"), `a neighbouring cell leaked: ${hit.context}`);
assert.ok(!hit.context.includes("|"), `a raw table pipe reached the reader: ${hit.context}`);
// The separator this replaced. A `·` between two runs of text is banned
// everywhere in this product — the Eastern Arabic zero is a raised dot.
assert.ok(!hit.context.includes("·"), `the cell join came back: ${hit.context}`);
});
it("quotes the cell the SEARCH TERM is in, and never the alignment row", () => {
const out = searchMatches("Shelf.md", "tulip", false, null);
assert.equal(out.length, 1);
assert.match(out[0].text, /<mark>tulip<\/mark>/);
assert.ok(!out[0].text.includes("Clarke"), `a neighbouring cell leaked: ${out[0].text}`);
// The `---` row matches nothing, but it must not be quotable for anything
// either: it is punctuation, not a line of the note.
assert.deepEqual(searchMatches("Shelf.md", "---", false, null), []);
});
});
describe("searchMatches", () => {
it("quotes every matching line with its full-file line number", () => {
const out = searchMatches("Findable.md", "tulips", false, null);
// Heading (body line 1 → file 4), "TULIPS again" (body 5 → file 8):
// substring semantics, case-insensitive. "tulip season"/"a tulip" do NOT
// contain "tulips" and must not be quoted for it.
assert.deepEqual(out.map((m) => m.line), [1 + fmLines(1), 5 + fmLines(1)]);
for (const m of out) assert.match(m.text, /<mark>tulips<\/mark>/i);
});
it("matches any term of a multi-word query, `#` stripped", () => {
const out = searchMatches("Findable.md", "#march nothing", false, null);
assert.deepEqual(out.map((m) => m.line), [3 + fmLines(1), 4 + fmLines(1)]);
});
it("windows a long line around the match instead of shipping it", () => {
const out = searchMatches("Findable.md", "buried", false, null);
assert.equal(out.length, 1);
const text = out[0].text;
assert.ok(text.length < 400, `windowed quote still ${text.length} chars`);
assert.match(text, /<mark>buried<\/mark>/);
assert.match(text, /…/); // a cut is marked as a cut
});
it("caps at 100 matches", () => {
const out = searchMatches("Many.md", "tulip", false, null);
assert.equal(out.length, 100);
assert.equal(out[0].line, 1);
assert.equal(out.at(-1)?.line, 100);
});
it("answers a visitor asking about an unpublished note with [], never 404", () => {
// The same refusal shape backlinks() makes: [] for hidden and for missing
// alike, so the response cannot confirm the path exists.
assert.deepEqual(searchMatches("Unpublished.md", "tulip", true, null), []);
assert.deepEqual(searchMatches("Nope.md", "tulip", false, null), []);
assert.equal(searchMatches("Unpublished.md", "tulip", false, null).length, 1);
});
it("escapes note text: a line is data, never markup", () => {
// The <mark> wrapper is the ONLY markup in `text` — anything the note
// itself spells in angle brackets must arrive escaped (the client renders
// through the same escape-aware snippet renderer search hits use).
const out = searchMatches("Findable.md", "relevant", false, null);
assert.equal(out.length, 1);
assert.ok(!/<(?!\/?mark>)/.test(out[0].text), "unescaped markup leaked");
});
it("an empty or whitespace query matches nothing", () => {
assert.deepEqual(searchMatches("Findable.md", " ", false, null), []);
});
});