test.web/presentation/components/walk.component.test.ts
test.web/presentation/components/walk.component.test.ts is a file in Codebase Testing. 149 lines of code and 0 definitions.
import {
GHOST_DECAY_PROPERTY,
GHOST_REFERENCE_CELLS,
GHOST_STEP_MS,
GHOST_TAIL_SHARE,
WALK_CURRENT_CLASS,
WALK_GHOST_CLASS,
WALK_STATUS_CLASS,
} from "@banes-lab/web/configuration/constants/anatomy.constants.ts";
import { LOCATION_RELATION, WALK_HINT } from "@banes-lab/web/configuration/strings/walk.strings.ts";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
cellRecord,
cellText,
createStatus,
ghostPace,
loadSnippet,
runGhost,
} from "@banes-lab/web/presentation/components/walk.component.ts";
import { enableInspector, renderWalk } from "@banes-lab/web/presentation/renderers/walk.renderer.ts";
import { OPEN_CLASS } from "@banes-lab/web/configuration/constants/element.constants.ts";
import { WALK_PANEL_ID } from "@banes-lab/web/core/ids/anatomy.ids.ts";
import type { WalkCellView } from "@banes-lab/web/types/anatomy.types.ts";
import { WalkInspector } from "@banes-lab/web/presentation/components/step.component.ts";
import { createVectorElement } from "@banes-lab/web/core/factories/element.factory.ts";
import { declaredStyle } from "@banes-lab/web/core/registries/style.registry.ts";
const CELL_FILE = ["core", "assets", "link.assets.ts"].join("/");
const CELL: WalkCellView = {
depth: 2,
file: CELL_FILE,
label: "identifier",
line: 19,
name: "tabLink",
ref: "0",
severity: null,
state: "call",
text: "tabLink(page, tabs, id)",
};
const BARE: WalkCellView = { ...CELL, file: "", line: null, name: "", ref: "1", severity: "high", state: "structure" };
const MARKUP =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><polygon class="hex" data-ref="0" points="0,0 1,0 1,1"/><polygon class="hex" data-ref="1" points="2,0 3,0 3,1"/></svg>';
type Intersection = (entries: readonly { readonly isIntersecting: boolean }[]) => void;
class Observer {
private readonly callback: Intersection;
public constructor(callback: Intersection) {
this.callback = callback;
}
public observe(): void {
this.callback([{ isIntersecting: true }]);
}
public disconnect(): void {
this.callback([]);
}
}
afterEach(() => {
document.body.replaceChildren();
vi.unstubAllGlobals();
vi.useRealTimers();
});
describe("cellText and cellRecord", () => {
it("summarizes a cell as location, kind and text, and records it with a linked location and a severity when flagged", () => {
expect(cellText(CELL)).toBe(`${CELL_FILE}:19 · identifier · tabLink(page, tabs, id)`);
expect(cellText(BARE)).toBe("depth 2 · identifier · tabLink(page, tabs, id)");
const record = cellRecord(CELL);
expect(record.name).toBe("tabLink");
expect(record.code).toBe("call");
expect(record.relations[0]?.relation).toBe(LOCATION_RELATION);
expect(record.relations[0]?.edges[0]?.ref?.endsWith("#file-core-assets-link-assets-ts:19")).toBe(true);
expect(record.relations).toHaveLength(2);
const bare = cellRecord(BARE);
expect(bare.name).toBe("identifier");
expect(bare.relations[0]?.edges[0]?.ref).toBeNull();
expect(bare.relations).toHaveLength(3);
});
});
describe("createStatus and loadSnippet", () => {
it("starts the status with the hint and answers no snippet when the source cannot be fetched", async () => {
expect(createStatus().textContent).toBe(WALK_HINT);
expect(createStatus().classList.contains(WALK_STATUS_CLASS)).toBe(true);
vi.stubGlobal("fetch", async () => {
await Promise.resolve();
throw new Error("offline");
});
expect(await loadSnippet(CELL)).toBeNull();
expect(await loadSnippet(BARE)).toBeNull();
});
});
describe("runGhost", () => {
it("moves a ghost head across the cells while visible and clears it on disposal", () => {
vi.useFakeTimers();
vi.stubGlobal("IntersectionObserver", Observer);
const element = document.createElement("div");
element.append(createVectorElement(MARKUP));
document.body.append(element);
const dispose = runGhost(element);
vi.advanceTimersByTime(1);
const cells = element.querySelectorAll("polygon");
const pace = ghostPace(cells.length);
expect(declaredStyle(element, GHOST_DECAY_PROPERTY)).toBe(`${String(pace.decay)}ms`);
expect(cells[0]?.classList.contains(WALK_GHOST_CLASS)).toBe(true);
vi.advanceTimersByTime(pace.step);
expect(cells[1]?.classList.contains(WALK_GHOST_CLASS)).toBe(true);
dispose();
expect(element.querySelectorAll(`.${WALK_GHOST_CLASS}`)).toHaveLength(0);
});
it("paces the ghost by the cell count: the reference count keeps the base step, fewer cells slow it, and the tail is a quarter of the cells", () => {
const decay = (GHOST_STEP_MS * GHOST_REFERENCE_CELLS) / GHOST_TAIL_SHARE;
expect(ghostPace(GHOST_REFERENCE_CELLS)).toStrictEqual({ decay, step: GHOST_STEP_MS, stride: 1 });
expect(ghostPace(GHOST_REFERENCE_CELLS / 10)).toStrictEqual({ decay, step: GHOST_STEP_MS * 10, stride: 1 });
expect(ghostPace(GHOST_REFERENCE_CELLS * 4)).toStrictEqual({ decay, step: GHOST_STEP_MS, stride: 4 });
expect(ghostPace(0).step).toBe(GHOST_STEP_MS * GHOST_REFERENCE_CELLS);
});
});
describe("WalkInspector and attachInspector", () => {
it("shows a cell's record in the shared panel, marks the cell, and steps to its neighbors", () => {
vi.stubGlobal("IntersectionObserver", Observer);
const element = document.createElement("div");
element.append(createVectorElement(MARKUP));
document.body.append(element);
const status = createStatus();
enableInspector(element, status, [CELL, BARE]);
const inspector = new WalkInspector(element, status, [CELL, BARE]);
inspector.show(0, true);
const panel = document.getElementById(WALK_PANEL_ID);
expect(panel?.classList.contains(OPEN_CLASS)).toBe(true);
expect(panel?.textContent.includes("tabLink")).toBe(true);
expect(element.querySelector<HTMLElement>(`.${WALK_CURRENT_CLASS}`)?.dataset.ref).toBe("0");
expect(status.textContent).toBe(cellText(CELL));
expect(inspector.isOpen()).toBe(true);
inspector.show(1, false);
expect(element.querySelector<HTMLElement>(`.${WALK_CURRENT_CLASS}`)?.dataset.ref).toBe("1");
inspector.hide();
expect(inspector.isOpen()).toBe(false);
expect(panel?.classList.contains(OPEN_CLASS)).toBe(false);
});
});
describe("renderWalk", () => {
it("renders a diagram figure with the caption, the step count and a status line", () => {
const figure = renderWalk({
caption: "a walk",
kind: "walk",
walk: { cells: "c.json", steps: 13, vector: "v.svg" },
});
expect(figure.classList.contains("diagram-figure")).toBe(true);
expect(figure.querySelector(".diagram-caption")?.textContent).toBe("a walk");
expect(figure.querySelector(".walk-steps")?.textContent).toBe("13 steps");
expect(figure.querySelector(`.${WALK_STATUS_CLASS}`)?.textContent).toBe(WALK_HINT);
});
});