test.web/domain/converters/search.converter.test.ts
test.web/domain/converters/search.converter.test.ts is a file in Codebase Testing. 184 lines of code and 0 definitions.
import type { PageContent, PageDefinition } from "@banes-lab/web/types/page.types.ts";
import type { SearchCorpus, SearchPage } from "@banes-lab/web/types/search.types.ts";
import { definitionMatches, searchSpreads, spreadsOf } from "@banes-lab/web/domain/converters/search.converter.ts";
import { describe, expect, it } from "vitest";
import {
indexOf,
positionOf,
positionsOf,
sourcesOf,
} from "@banes-lab/web/domain/converters/search.index.converter.ts";
import { narrowSection, narrowSubsection } from "@banes-lab/web/domain/converters/search.fragment.converter.ts";
import type { Section } from "@banes-lab/web/types/document.types.ts";
import { createElement } from "@banes-lab/web/core/factories/element.factory.ts";
const ICON = "bi-x";
const section = function section(id: string, title: string, text: string): Section {
return { icon: ICON, id, subsections: [{ content: text, title: `${title} detail` }], title };
};
const page = function page(id: string, content: PageContent): SearchPage {
const definition: PageDefinition = {
content,
description: id,
id,
render: () => createElement("div"),
title: id.toUpperCase(),
};
return { definition, icon: ICON };
};
const GLOSSARY: Section = {
blocks: [
{
entries: [
{ description: "A slow departure from the rule.", id: "lexicon-drift", term: "Drift" },
{ description: "Unrelated.", id: "lexicon-other", term: "Other" },
],
kind: "glossary",
},
],
icon: ICON,
id: "lexicon",
subsections: [],
title: "Lexicon",
};
const METHOD = page("method", {
kind: "tabbed",
layout: "chapter",
meta: { subtitle: "s", title: "Method", version: "1" },
tabs: [
{
icon: ICON,
id: "build",
label: "Build",
sections: [
section("early", "Early", "the gate holds"),
{
...section("late", "Late", "the gate again"),
intro: 'Watch for <a href="/words/terms#lexicon-drift">drift</a>.',
},
],
},
{ icon: ICON, id: "files", label: "Files", layout: "tree", sections: [section("tree", "Tree", "gate")] },
],
tone: "methodology",
});
const WORDS = page("words", {
kind: "tabbed",
layout: "grid",
meta: { subtitle: "s", title: "Words", version: "1" },
tabs: [{ icon: ICON, id: "terms", label: "Terms", layout: "glossary", sections: [GLOSSARY] }],
tone: "ontology",
});
const LEGAL = page("legal", {
kind: "document",
meta: { closing: "c", effectiveDate: "e", lastUpdated: "l", title: "Legal", version: "1" },
sections: [section("gatehouse", "Gatehouse", "a gate by the road")],
});
const PAGES = [LEGAL, WORDS, METHOD];
const CORPUS: SearchCorpus = {
definitions: [
{ file: "presentation/gate.renderer.ts", line: 1, name: "renderGate" },
{ file: "core/gate.analyzer.ts", line: 1, name: "gateOrder" },
],
pages: PAGES,
positions: positionsOf({
evidence: [
{
nodes: [{ file: null, kind: "definition", name: "gateOrder" }],
subject: { kind: "chapter", page: "method", section: "late", tab: "build" },
},
],
pages: PAGES,
route: ["/method#late", "/method#early"],
}),
};
describe("narrowSection and narrowSubsection", () => {
it("keeps a section whole when its own prose holds every word", () => {
const whole = section("s", "The gate", "detail");
expect(narrowSection(["gate"], whole)).toBe(whole);
});
it("keeps only the matching subsection when the section itself does not match", () => {
const mixed: Section = {
icon: ICON,
id: "mixed",
intro: "Unrelated lead.",
subsections: [
{ content: "the gate", title: "One" },
{ content: "nothing", title: "Two" },
],
title: "Mixed",
};
const narrowed = narrowSection(["gate"], mixed);
expect(narrowed?.subsections.map((sub) => sub.title)).toStrictEqual(["One"]);
expect(narrowed?.intro).toBeUndefined();
expect(narrowSubsection(["gate"], { content: "nothing", title: "Two" })).toBeNull();
});
it("narrows a glossary to the entries that match", () => {
const narrowed = narrowSection(["drift"], GLOSSARY);
const [block] = narrowed?.blocks ?? [];
expect(block?.kind === "glossary" ? block.entries.map((entry) => entry.term) : []).toStrictEqual(["Drift"]);
expect(narrowSection(["absent"], GLOSSARY)).toBeNull();
});
});
describe("sourcesOf", () => {
it("searches every tab but the tree, and a document as one source", () => {
expect(sourcesOf(METHOD).map((source) => source.tab.id)).toStrictEqual(["build"]);
expect(sourcesOf(LEGAL).map((source) => [source.layout, source.tone])).toStrictEqual([["document", null]]);
});
});
describe("indexOf and positionOf", () => {
it("builds the index once per corpus and places a section by the route, else by the first link to it", () => {
const index = indexOf(CORPUS);
expect(indexOf(CORPUS)).toBe(index);
expect(index.sources.map((source) => source.page)).toStrictEqual(["legal", "words", "method"]);
const method = index.sources.find((source) => source.page === "method");
const words = index.sources.find((source) => source.page === "words");
const [early, late] = method?.tab.sections ?? [];
if (method === undefined || words === undefined || early === undefined || late === undefined) {
throw new Error("the corpus has a method source with two sections and a words source");
}
expect(positionOf(index, method, late)).toBe(0);
expect(positionOf(index, method, early)).toBe(1);
expect(positionOf(index, words, GLOSSARY)).toBe(0.5);
});
});
describe("definitionMatches", () => {
it("matches a definition name by every term it contains", () => {
const [renderGate] = CORPUS.definitions;
expect(renderGate === undefined ? false : definitionMatches(["gate"], renderGate)).toBe(true);
expect(renderGate === undefined ? true : definitionMatches(["ga"], renderGate)).toBe(false);
});
});
describe("searchSpreads", () => {
it("walks the results in teaching order, not in page order", () => {
const spreads = searchSpreads(CORPUS, "gate");
const walk = spreads.flatMap((spread) => spread.sections.map((hit) => hit.id));
expect(walk[0]).toBe("late");
expect(walk.indexOf("late")).toBeLessThan(walk.indexOf("early"));
expect(walk).toContain("gatehouse");
expect(walk.indexOf("gatehouse")).toBeGreaterThan(walk.indexOf("early"));
});
it("places a definition where a chapter cites it, and the uncited ones after", () => {
const spreads = searchSpreads(CORPUS, "gate");
const titles = spreads.flatMap((spread) => spread.sections.map((hit) => hit.title));
expect(titles.indexOf("gateOrder")).toBeLessThan(titles.indexOf("Early"));
expect(titles.indexOf("renderGate")).toBeGreaterThan(titles.indexOf("Early"));
});
it("places a glossary entry where the teaching first links to it", () => {
const spreads = searchSpreads(CORPUS, "drift");
expect(spreads.map((spread) => spread.source.page)).toStrictEqual(["method", "words"]);
expect(spreads[1]?.home).toBe("/words#lexicon");
});
it("returns nothing for a query with no words", () => {
expect(searchSpreads(CORPUS, " ")).toStrictEqual([]);
});
});
describe("spreadsOf", () => {
it("merges consecutive hits from one source into one spread", () => {
const [source] = sourcesOf(METHOD);
if (source === undefined) {
throw new Error("the method page has a build source");
}
const hit = { home: "/h", position: 0, section: GLOSSARY, sectionIndex: 0, source, sourceIndex: 0 };
expect(spreadsOf([hit, { ...hit, sectionIndex: 1 }])).toHaveLength(1);
});
});