# test.web/domain/converters/link.converter.test.ts

> 241 lines of code and 0 definitions.

Tree: Codebase Testing
Language: typescript
Canonical: https://banes-lab.com/anatomy/testing#file-testing-test-web-domain-converters-link-converter-test-ts
Source text: https://banes-lab.com/source/testing/test.web/domain/converters/link.converter.test.ts.txt

Listed in [test.web/domain/converters](https://banes-lab.com/api/source/testing/test.web/domain/converters.md), after [test.web/domain/converters/lexicon.converter.test.ts](https://banes-lab.com/source/testing/test.web/domain/converters/lexicon.converter.test.ts.md) and before [test.web/domain/converters/loop.converter.test.ts](https://banes-lab.com/source/testing/test.web/domain/converters/loop.converter.test.ts.md).

## Contained in

- [test.web/domain/converters](https://banes-lab.com/anatomy/testing/folder-testing-test-web-domain-converters.md)

## Source

```typescript
import type { Block, LessonBlock } from "@banes-lab/web/types/block.types.ts";
import type { LinkedPage, VocabularyEntry } from "@banes-lab/web/types/vocabulary.types.ts";
import type { Section, Tab } from "@banes-lab/web/types/document.types.ts";
import { chapterRef, chapterVocabulary } from "@banes-lab/web/domain/converters/chapter.converter.ts";
import { describe, expect, it } from "vitest";
import {
    entryFor,
    joinGlossary,
    linkPages,
    linkSection,
    linkTabs,
    linkText,
} from "@banes-lab/web/domain/converters/link.vocabulary.converter.ts";
import { layerLink, nodeLink, stageLink } from "@banes-lab/web/domain/converters/link.converter.ts";
import { buildPhraseIndex } from "@banes-lab/web/core/matchers/vocabulary.matcher.ts";

const STRUCTURAL = "Structural Core";
const RESOURCE = "Resource Core";

const VOCABULARY: readonly VocabularyEntry[] = [
    {
        code: null,
        kind: "principle",
        layer: "Correctness Core",
        phrase: "Fail Fast",
        prose: true,
        ref: "architecture:fail-fast",
    },
    {
        code: "SRP",
        kind: "principle",
        layer: STRUCTURAL,
        phrase: "Single Responsibility Principle",
        prose: true,
        ref: "architecture:single-responsibility",
    },
    {
        code: null,
        kind: "quality-attribute",
        layer: STRUCTURAL,
        phrase: "High Cohesion",
        prose: true,
        ref: "architecture:high-cohesion",
    },
    { code: null, kind: "layer", layer: RESOURCE, phrase: RESOURCE, prose: true, ref: "layer:resource-core" },
    { code: null, kind: "principle", layer: STRUCTURAL, phrase: "YAGNI", prose: false, ref: "lexicon:yagni" },
    { code: null, kind: "principle", layer: null, phrase: "Unknown Collection", prose: true, ref: "nowhere:unknown" },
];

const INDEX = buildPhraseIndex(VOCABULARY);

const section = function section(): Section {
    return {
        blocks: [{ kind: "text", text: "Fail fast here." }],
        icon: "icon",
        id: "a-section",
        intro: "Fail fast in the intro.",
        subsections: [
            {
                blocks: [
                    {
                        application: "Apply fail fast.",
                        cause: "High cohesion is the cause.",
                        decision: "Decide.",
                        failureMode: "It fails.",
                        kind: "lesson",
                        principle: "High cohesion holds.",
                        problem: "A problem.",
                        validation: "Validate.",
                    },
                    { code: "fail fast", kind: "code", language: "text", title: "Fail fast" },
                ],
                content: "Fail fast again.",
                title: "Sub",
            },
        ],
        title: "Section",
    };
};

describe("linkText", () => {
    it("wraps the matched phrase in a link to the ontology record and leaves an unresolvable collection as text", () => {
        expect(linkText("Fail fast now.", { index: INDEX, seen: new Set() })).toBe(
            '<a href="/records/architecture/fail-fast">Fail fast</a> now.',
        );
        expect(linkText("An unknown collection.", { index: INDEX, seen: new Set() })).toBe("An unknown collection.");
    });
});

const textOf = function textOf(block: Block | undefined): string {
    return block?.kind === "text" ? block.text : "";
};

const lessonOf = function lessonOf(block: Block | undefined): LessonBlock | null {
    return block?.kind === "lesson" ? block : null;
};

describe("linkSection", () => {
    const linked = linkSection(section(), INDEX);

    it("links the first occurrence of a record once per section, in reading order, and leaves code untouched", () => {
        expect(linked.intro).toContain("<a href=");
        expect(textOf(linked.blocks?.[0])).toBe("Fail fast here.");
        expect(linked.subsections[0]?.content).toBe("Fail fast again.");
        expect(linked.subsections[0]?.blocks?.[1]).toStrictEqual(section().subsections[0]?.blocks?.[1]);
    });

    it("links lesson fields and keeps every other field intact", () => {
        const lesson = lessonOf(linked.subsections[0]?.blocks?.[0]);
        expect(lesson?.cause).toContain('<a href="/records/architecture/high-cohesion">High cohesion</a>');
        expect(lesson?.principle).toBe("High cohesion holds.");
        expect(lesson?.decision).toBe("Decide.");
    });
});

describe("linkTabs", () => {
    it("links every section of every tab with its own scope", () => {
        const tabs: readonly Tab[] = [
            { icon: "i", id: "one", label: "One", sections: [section(), { ...section(), id: "b" }] },
        ];
        const [tab] = linkTabs(tabs, VOCABULARY);
        expect(tab?.sections.every((held) => held.intro?.includes("<a href=") === true)).toBe(true);
    });

    it("links a chapter of another page by its title, never a section to itself", () => {
        const own: Section = { ...section(), id: "the-loop", intro: "The loop and fail fast.", title: "The loop" };
        const other: Section = { ...section(), id: "elsewhere", intro: "The loop again.", title: "Elsewhere" };
        const tabs: readonly Tab[] = [{ icon: "i", id: "one", label: "One", sections: [own, other] }];
        const [tab] = linkTabs(tabs, [...VOCABULARY, ...chapterVocabulary([{ page: "a-page", tabs }])], "a-page");
        expect(tab?.sections[0]?.intro).toBe('The loop and <a href="/records/architecture/fail-fast">fail fast</a>.');
        expect(tab?.sections[1]?.intro).toBe('<a href="/a-page#the-loop">The loop</a> again.');
    });
});

describe("linkPages", () => {
    it("links each baked page with the ontology vocabulary and the chapter titles of every page, keeping its own fields", () => {
        const own: Section = { ...section(), id: "the-loop", intro: "The loop and fail fast.", title: "The loop" };
        const reader: Section = { ...section(), id: "reader", intro: "See the loop.", title: "Reader" };
        const baked: LinkedPage = {
            name: "B_TABS",
            page: "b-page",
            stem: "b.page",
            tabs: [{ icon: "i", id: "two", label: "Two", sections: [reader] }],
        };
        const pages = [{ page: "a-page", tabs: [{ icon: "i", id: "one", label: "One", sections: [own] }] }, baked];
        const [linked] = linkPages(pages, [baked], VOCABULARY);
        expect(linked?.name).toBe("B_TABS");
        expect(linked?.stem).toBe("b.page");
        expect(linked?.tabs[0]?.sections[0]?.intro).toBe('See <a href="/a-page#the-loop">the loop</a>.');
    });
});

describe("stageLink, layerLink and nodeLink", () => {
    it("link a loop stage, a reasoning layer and a reasoning node to their records on the ontology page", () => {
        expect(stageLink("orient")).toBe('<a href="/records/stage/orient">orient</a>');
        expect(layerLink("epistemic", "the epistemic layer")).toBe(
            '<a href="/records/reasoning/layer-epistemic">the epistemic layer</a>',
        );
        expect(nodeLink("tel-priority", "priority")).toBe('<a href="/records/reasoning/node-tel-priority">priority</a>');
    });
});

describe("chapterRef and chapterVocabulary", () => {
    const tabs: readonly Tab[] = [
        { icon: "i", id: "first", label: "First", sections: [{ ...section(), id: "one", title: "One thing" }] },
        { icon: "i", id: "second", label: "Second", sections: [{ ...section(), id: "two", title: "Two things" }] },
    ];

    it("addresses a section on the page path for the first tab and on the tab path otherwise", () => {
        expect(chapterRef("a-page", tabs, "first", "one")).toBe("chapter:/a-page#one");
        expect(chapterRef("a-page", tabs, "second", "two")).toBe("chapter:/a-page/second#two");
    });

    it("yields one prose entry per section, titled by the section", () => {
        expect(chapterVocabulary([{ page: "a-page", tabs }])).toStrictEqual([
            { code: null, kind: "chapter", layer: null, phrase: "One thing", prose: true, ref: "chapter:/a-page#one" },
            {
                code: null,
                kind: "chapter",
                layer: null,
                phrase: "Two things",
                prose: true,
                ref: "chapter:/a-page/second#two",
            },
        ]);
    });
});

describe("joinGlossary and entryFor", () => {
    it("joins a term to its record, deriving kind and code, keeping an authored placement over the record's layer, and a placed term to its layer", () => {
        const [srp, owner, stray, yagni] = joinGlossary(
            [
                { description: "d", term: "Single Responsibility Principle" },
                { description: "d", domains: [RESOURCE], term: "Single Owner" },
                { description: "d", term: "Nothing Known" },
                { description: "d", domains: [RESOURCE], term: "YAGNI" },
            ],
            VOCABULARY,
        );
        expect(srp).toStrictEqual({
            code: "SRP",
            description: "d",
            domains: ["principle", STRUCTURAL],
            href: "/records/architecture/single-responsibility",
            term: "Single Responsibility Principle",
        });
        expect(owner?.href).toBe("/records/layer/resource-core");
        expect(owner?.domains).toStrictEqual([RESOURCE]);
        expect(stray?.href).toBeUndefined();
        expect(yagni?.href).toBe("/records/lexicon/yagni");
        expect(yagni?.domains).toStrictEqual(["principle", RESOURCE]);
        expect(entryFor("high cohesion", INDEX)?.ref).toBe("architecture:high-cohesion");
        expect(entryFor("cohesion", INDEX)).toBeNull();
    });

    it("drops a derived code that only repeats the term", () => {
        const [cqrs] = joinGlossary(
            [{ description: "d", term: "CQRS" }],
            [
                {
                    code: "CQRS",
                    kind: "pattern",
                    layer: "Execution Core",
                    phrase: "CQRS",
                    prose: true,
                    ref: "architecture:cqrs",
                },
            ],
        );
        expect(cqrs?.code).toBeUndefined();
        expect(cqrs?.domains).toStrictEqual(["pattern", "Execution Core"]);
    });

    it("never links a glossary entry's description to the entry's own record", () => {
        const glossary: Section = {
            blocks: [
                {
                    entries: [
                        {
                            description: "Fail fast is its own thing; high cohesion helps.",
                            href: "/records/architecture/fail-fast",
                            term: "Fail Fast",
                        },
                        { description: "High cohesion again, and fail fast again.", term: "High Cohesion" },
                    ],
                    kind: "glossary",
                },
            ],
            icon: "icon",
            id: "glossary",
            subsections: [],
            title: "Glossary",
        };
        const [block] = linkSection(glossary, INDEX).blocks ?? [];
        const entries = block?.kind === "glossary" ? block.entries : [];
        expect(entries[0]?.description).toBe(
            'Fail fast is its own thing; <a href="/records/architecture/high-cohesion">high cohesion</a> helps.',
        );
        expect(entries[1]?.description).toBe("High cohesion again, and fail fast again.");
    });
});
```
