test.web/presentation/widgets/reference.widget.test.ts
test.web/presentation/widgets/reference.widget.test.ts is a file in Codebase Testing. 220 lines of code and 0 definitions.
import { FILE_ANCHOR, TREE_TAB } from "@banes-lab/web/core/ids/anatomy.ids.ts";
import {
REFERENCE_ARROW_CLASS,
REFERENCE_CHAIN_MS,
REFERENCE_CHAIN_PROPERTY,
REFERENCE_DIAL_CLASS,
REFERENCE_HIDE_MS,
REFERENCE_PENDING_CLASS,
REFERENCE_SHOW_MS,
} from "@banes-lab/web/configuration/constants/reference.constants.ts";
import type { ReferenceIndex, ReferenceRecord } from "@banes-lab/web/types/reference.types.ts";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ANATOMY_FACE } from "@banes-lab/web/configuration/constants/vocabulary.constants.ts";
import { ANATOMY_INDEX } from "@banes-lab/web/core/generated/anatomy.index.generated.ts";
import { ANATOMY_PAGE } from "@banes-lab/web/core/ids/page.ids.ts";
import type { AnatomyRecords } from "@banes-lab/web/types/record.types.ts";
import { ESCAPE_KEY } from "@banes-lab/web/configuration/constants/overlay.constants.ts";
import type { GraphChunk } from "@banes-lab/web/types/graph.types.ts";
import { REFERENCE_ARROW } from "@banes-lab/web/configuration/strings/reference.strings.ts";
import { REFERENCE_PANEL_ID } from "@banes-lab/web/core/ids/reference.ids.ts";
import { createElement } from "@banes-lab/web/core/factories/element.factory.ts";
import { declaredStyle } from "@banes-lab/web/core/registries/style.registry.ts";
import { fileId } from "@banes-lab/web/domain/converters/source.converter.ts";
import { hrefOf } from "@banes-lab/web/domain/converters/ontology.converter.ts";
import { loadReference } from "@banes-lab/web/domain/loaders/reference.loader.ts";
import { mountReference } from "@banes-lab/web/presentation/widgets/reference.widget.ts";
import { sourceLocation } from "@banes-lab/web/core/assets/walk.assets.ts";
import { tabLink } from "@banes-lab/web/core/assets/link.assets.ts";
const OPEN = "open";
const FACTORY = ["core", "factories", "element.factory.ts"].join("/");
const FACTORY_NAME = "element.factory.ts";
const CREATE = "createElement";
const LINE = "3";
const FAIL_FAST = "architecture:fail-fast";
const SUMMARY = "Stop at the first error.";
const INDEX: ReferenceIndex = {
[FAIL_FAST]: {
code: null,
kind: "principle",
layer: { label: "Correctness Core", ref: "layer:correctness-core" },
name: "Fail Fast",
relations: [
{
edges: [{ label: "Single Responsibility Principle", ref: "architecture:single-responsibility" }],
relation: "requires",
},
],
summary: SUMMARY,
},
"architecture:single-responsibility": {
code: "SRP",
kind: "principle",
layer: null,
name: "Single Responsibility Principle",
relations: [],
summary: null,
},
};
vi.mock("@banes-lab/web/core/generated/reference.generated.ts", () => ({
loadReferences: async (face: string): Promise<ReferenceIndex | null> => {
await Promise.resolve();
return face === "architecture" ? INDEX : null;
},
}));
const GRAPH: { chunk: GraphChunk | null } = { chunk: null };
vi.mock("@banes-lab/web/core/generated/graph.generated.ts", () => ({
loadGraph: async (): Promise<GraphChunk | null> => {
await Promise.resolve();
return GRAPH.chunk;
},
}));
const sourceRecord = function sourceRecord(name: string, kind: string): ReferenceRecord {
return { code: null, kind, layer: null, name, relations: [], summary: null };
};
const anatomyContent = async function anatomyContent(fragment: string): Promise<ReferenceRecord | null> {
const content = await loadReference(
{ face: ANATOMY_FACE, ref: `${ANATOMY_FACE}:${fragment}` },
tabLink(ANATOMY_PAGE, TREE_TAB, fragment),
);
return content?.kind === "record" ? content.record : null;
};
const panel = function panel(): HTMLElement | null {
return document.getElementById(REFERENCE_PANEL_ID);
};
const linkTo = function linkTo(ref: string): HTMLAnchorElement {
const link = createElement("a", { attributes: { href: hrefOf(ref) ?? "" }, text: ref });
document.body.append(link);
return link;
};
const hover = async function hover(link: Element, wait: number): Promise<void> {
link.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await vi.advanceTimersByTimeAsync(wait);
};
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
document.body.replaceChildren();
});
describe("loadReference", () => {
it("loads an ontology record by its collection and answers null for an unknown one", async () => {
const loaded = await loadReference({ face: "architecture", ref: FAIL_FAST }, hrefOf(FAIL_FAST) ?? "");
expect(loaded?.kind === "record" && loaded.record.name).toBe("Fail Fast");
expect(
await loadReference({ face: "lexicon", ref: "lexicon:nothing" }, hrefOf("lexicon:nothing") ?? ""),
).toBeNull();
});
it("adds the baked tooltip relations of the record and drops the ones a tooltip does not show", async () => {
GRAPH.chunk = {
[FAIL_FAST]: {
kind: "principle",
number: null,
relations: [
{ relation: "linked-from", targets: [{ href: "/method#loop", number: "3", title: "The loop" }] },
{ relation: "contains", targets: [{ href: null, number: null, title: "hidden" }] },
],
title: "Fail Fast",
},
};
const loaded = await loadReference({ face: "architecture", ref: FAIL_FAST }, hrefOf(FAIL_FAST) ?? "");
GRAPH.chunk = null;
expect(loaded?.kind === "record" && loaded.record.relations.at(-1)).toStrictEqual({
edges: [{ label: "The loop", ref: "chapter:/method#loop" }],
relation: "linked-from",
});
expect(loaded?.kind === "record" && loaded.record.relations.map((held) => held.relation)).toStrictEqual([
"requires",
"linked-from",
]);
});
it("answers a tree node's record or its definition's record from the node's baked records file", async () => {
const id = fileId(FACTORY);
const name = ANATOMY_INDEX.nodes[id];
const records: AnatomyRecords = {
definitions: { [LINE]: sourceRecord(CREATE, "definition") },
node: sourceRecord(FACTORY_NAME, "file"),
};
const fetched = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (url) =>
url === sourceLocation(name ?? "") ? Response.json(records) : new Response("", { status: 404 }),
);
const node = await anatomyContent(id);
const definition = await anatomyContent(`${id}:${LINE}`);
const fallback = await anatomyContent(`${id}:0`);
const missing = await anatomyContent(`${FILE_ANCHOR}nowhere`);
fetched.mockRestore();
expect(name).toBeDefined();
expect(node).toStrictEqual(records.node);
expect(definition).toStrictEqual(records.definitions[LINE]);
expect(fallback).toStrictEqual(records.node);
expect(missing).toBeNull();
});
});
describe("mountReference", () => {
it("opens a panel with the record's name, summary and relation links after the show delay", async () => {
const dispose = mountReference();
await hover(linkTo(FAIL_FAST), REFERENCE_SHOW_MS);
const shown = panel();
expect(shown?.classList.contains(OPEN)).toBe(true);
expect(shown?.textContent).toContain("Fail Fast");
expect(shown?.textContent).toContain(SUMMARY);
const links = [...(shown?.querySelectorAll("a") ?? [])].map((link) => link.getAttribute("href"));
expect(links).toStrictEqual([hrefOf("layer:correctness-core"), hrefOf("architecture:single-responsibility")]);
dispose();
});
it("arms a relation link inside the panel with a dial and an arrow, and re-anchors only after the cooldown", async () => {
const dispose = mountReference();
await hover(linkTo(FAIL_FAST), REFERENCE_SHOW_MS);
const inner = [...(panel()?.querySelectorAll("a") ?? [])].at(-1);
expect(inner).toBeDefined();
await hover(inner ?? document.body, REFERENCE_CHAIN_MS - 1);
expect(inner?.classList.contains(REFERENCE_PENDING_CLASS)).toBe(true);
expect(inner?.querySelector(`.${REFERENCE_DIAL_CLASS} svg circle`)).not.toBeNull();
expect(inner?.querySelector(`.${REFERENCE_ARROW_CLASS}`)?.textContent).toBe(REFERENCE_ARROW);
expect(panel()?.textContent).toContain(SUMMARY);
await vi.advanceTimersByTimeAsync(1);
expect(panel()?.textContent).toContain("SRP");
expect(panel()?.textContent).not.toContain(SUMMARY);
expect(declaredStyle(panel() ?? document.body, REFERENCE_CHAIN_PROPERTY)).toBe(
`${String(REFERENCE_CHAIN_MS)}ms`,
);
dispose();
});
it("disarms a pending relation link when the pointer leaves it for the panel body, and arms it again on return", async () => {
const dispose = mountReference();
await hover(linkTo(FAIL_FAST), REFERENCE_SHOW_MS);
const inner = [...(panel()?.querySelectorAll("a") ?? [])].at(-1);
await hover(inner ?? document.body, REFERENCE_CHAIN_MS / 2);
inner?.dispatchEvent(new MouseEvent("mouseout", { bubbles: true, relatedTarget: panel() }));
await vi.advanceTimersByTimeAsync(REFERENCE_CHAIN_MS);
expect(inner?.classList.contains(REFERENCE_PENDING_CLASS)).toBe(false);
expect(inner?.querySelector(`.${REFERENCE_DIAL_CLASS}`)).toBeNull();
expect(panel()?.textContent).toContain(SUMMARY);
await hover(inner ?? document.body, REFERENCE_CHAIN_MS);
expect(panel()?.textContent).toContain("SRP");
dispose();
});
it("hides on mouse out after the hide delay, on escape, and stays closed for a link with no record", async () => {
const dispose = mountReference();
const link = linkTo(FAIL_FAST);
await hover(link, REFERENCE_SHOW_MS);
link.dispatchEvent(new MouseEvent("mouseout", { bubbles: true }));
await vi.advanceTimersByTimeAsync(REFERENCE_HIDE_MS);
expect(panel()?.classList.contains(OPEN)).toBe(false);
await hover(link, REFERENCE_SHOW_MS);
document.dispatchEvent(new KeyboardEvent("keydown", { key: ESCAPE_KEY }));
expect(panel()?.classList.contains(OPEN)).toBe(false);
await hover(linkTo("lexicon:nothing"), REFERENCE_SHOW_MS);
expect(panel()?.classList.contains(OPEN)).toBe(false);
dispose();
});
it("removes the panel and stops reacting once disposed", async () => {
mountReference()();
expect(panel()).toBeNull();
await hover(linkTo(FAIL_FAST), REFERENCE_SHOW_MS);
expect(panel()).toBeNull();
});
});