test.web/presentation/renderers/chapter.renderer.test.ts

test.web/presentation/renderers/chapter.renderer.test.ts is a file in Codebase Testing. 117 lines of code and 0 definitions.

import {
    PAIRED_SECTION_CLASS,
    PANEL_COLUMN_CLASS,
    PANEL_COLUMN_LIMIT,
    PANEL_FOOTER_CLASS,
    PANEL_FOOTER_COLUMNS_PROPERTY,
    PANEL_KINDS,
} from "@banes-lab/web/configuration/constants/chapter.constants.ts";
import type { Section, Tab } from "@banes-lab/web/types/document.types.ts";
import { describe, expect, it } from "vitest";
import {
    renderChapter,
    renderChapterIndex,
    renderChapterSection,
    renderChapterSectionOf,
} from "@banes-lab/web/presentation/renderers/chapter.renderer.ts";
import { sectionLabel, subsectionId } from "@banes-lab/web/presentation/renderers/section.renderer.ts";
import { chapterPageLabel } from "@banes-lab/web/configuration/strings/tab.strings.ts";
import { declaredStyle } from "@banes-lab/web/core/registries/style.registry.ts";
import { nodePath } from "@banes-lab/web/core/assets/link.assets.ts";

const PROSE_ONLY: Section = {
    icon: "",
    id: "prose-only",
    intro: "An intro.",
    subsections: [{ blocks: [{ kind: "text", text: "A paragraph." }], content: "Lead.", title: "First" }],
    title: "Prose only",
};

const PAIRED: Section = {
    icon: "",
    id: "paired",
    subsections: [
        {
            blocks: [
                { kind: "text", text: "Prose." },
                { code: "const a = 1;", kind: "code", language: "javascript", title: "A sample" },
            ],
            title: "With a panel",
        },
    ],
    title: "Paired",
};

const TAB: Tab = { icon: "", id: "chapter", label: "The chapter", sections: [PROSE_ONLY, PAIRED] };

describe("renderChapter", () => {
    const article = renderChapter(TAB);
    const sections = [...article.querySelectorAll("section.chapter-section")];

    it("titles the chapter with the tab label and gives every section its id", () => {
        expect(article.querySelector("h2.chapter-title")?.textContent).toBe(TAB.label);
        expect(sections.map((section) => section.id)).toStrictEqual([PROSE_ONLY.id, PAIRED.id]);
    });

    it("renders a section with no panel as prose alone, spanning the row", () => {
        const [prose] = sections;
        expect(prose?.classList.contains("chapter-section-paired")).toBe(false);
        expect(prose?.querySelector(".chapter-panels")).toBeNull();
        expect(prose?.querySelector("p.chapter-lead")?.textContent).toBe(PROSE_ONLY.intro);
        expect(prose?.querySelector(`#${subsectionId(PROSE_ONLY.id, 0)}`)).not.toBeNull();
    });

    it("moves every panel kind into the panel column and leaves the prose in the prose column", () => {
        const [, paired] = sections;
        expect(paired?.classList.contains("chapter-section-paired")).toBe(true);
        expect(paired?.querySelectorAll(".chapter-panels .chapter-panel")).toHaveLength(1);
        expect(paired?.querySelector(".chapter-prose .code-block")).toBeNull();
        expect(paired?.querySelector(".chapter-prose .block-text")).not.toBeNull();
        expect(PANEL_KINDS.has("code")).toBe(true);
        expect(PANEL_KINDS.has("mermaid")).toBe(true);
        expect(PANEL_KINDS.has("text")).toBe(false);
    });

    it("keeps the first panels in the side column and moves the rest into a footer grid below", () => {
        const panels = Array.from({ length: PANEL_COLUMN_LIMIT + 2 }, (_, index) => ({
            code: `const n = ${String(index)};`,
            kind: "code" as const,
            language: "javascript",
            title: `Sample ${String(index)}`,
        }));
        const section = renderChapterSection({ ...PAIRED, subsections: [{ blocks: panels, title: "Many" }] }, 1);
        expect(section.classList.contains(PAIRED_SECTION_CLASS)).toBe(true);
        expect(section.querySelectorAll(`.${PANEL_COLUMN_CLASS} > .chapter-panel`)).toHaveLength(PANEL_COLUMN_LIMIT);
        expect(section.querySelectorAll(`.${PANEL_FOOTER_CLASS} > .chapter-panel`)).toHaveLength(2);
        const footer = section.querySelector<HTMLElement>(`.${PANEL_FOOTER_CLASS}`);
        expect(section.lastElementChild).toBe(footer);
        expect(declaredStyle(footer ?? section, PANEL_FOOTER_COLUMNS_PROPERTY)).toBe("2");
        expect(renderChapterSection(PAIRED, 1).querySelector(`.${PANEL_FOOTER_CLASS}`)).toBeNull();
    });

    it("renders one section on its own, labeled by the index it is given", () => {
        const section = renderChapterSection(PAIRED, 3);
        expect(section.id).toBe(PAIRED.id);
        expect(section.classList.contains(PAIRED_SECTION_CLASS)).toBe(true);
        expect(section.querySelector(".chapter-label")?.textContent).toBe(sectionLabel(3));
    });
});

describe("renderChapterIndex", () => {
    it("lists each section by its title and intro with one link to its own page, and none of its subsections", () => {
        const index = renderChapterIndex(TAB, "the-page");
        const entries = [...index.querySelectorAll("section.chapter-section")];
        expect(entries.map((entry) => entry.id)).toStrictEqual([PROSE_ONLY.id, PAIRED.id]);
        expect(index.querySelectorAll(".chapter-subsection-title")).toHaveLength(0);
        expect(index.querySelectorAll(".chapter-panel")).toHaveLength(0);
        const links = [...index.querySelectorAll(".chapter-section-link a")];
        expect(links.map((link) => link.getAttribute("href"))).toStrictEqual([
            nodePath("the-page", TAB.id, PROSE_ONLY.id),
            nodePath("the-page", TAB.id, PAIRED.id),
        ]);
        expect(links[0]?.textContent).toBe(chapterPageLabel(PROSE_ONLY.title));
    });
});

describe("renderChapterSectionOf", () => {
    it("renders a tab's section by its id at its own index, a part of its subsections, and nothing for an unknown id", () => {
        const whole = renderChapterSectionOf(TAB, PAIRED.id);
        expect(whole?.querySelector(".chapter-label")?.textContent).toBe(sectionLabel(1));
        expect(whole?.querySelectorAll(".chapter-panel")).toHaveLength(1);
        const two = { ...PROSE_ONLY, subsections: [...PROSE_ONLY.subsections, { content: "More.", title: "Second" }] };
        const part = renderChapterSectionOf({ ...TAB, sections: [two] }, two.id, [1, 2]);
        const titles = [...(part?.querySelectorAll(".chapter-subsection-title") ?? [])];
        expect(titles).toHaveLength(1);
        expect(titles[0]?.textContent.endsWith("Second")).toBe(true);
        expect(renderChapterSectionOf(TAB, "missing")).toBeNull();
    });
});