test.web/presentation/components/learning.component.test.ts
test.web/presentation/components/learning.component.test.ts is a file in Codebase Testing. 142 lines of code and 0 definitions.
import type { Geometry, LearningView, PlacedStop } from "@banes-lab/web/types/learning.types.ts";
import {
aimOf,
attachPrerequisites,
cameraFit,
framedOf,
marksOf,
paceAt,
pixelsOfLength,
shortestOf,
spansOf,
touchTargetOf,
widestOf,
} from "@banes-lab/web/presentation/components/learning.fragment.component.ts";
import { describe, expect, it } from "vitest";
import { attachLearningMap } from "@banes-lab/web/presentation/components/learning.component.ts";
import { layoutLearning } from "@banes-lab/web/domain/converters/learning.converter.ts";
import { renderLearning } from "@banes-lab/web/presentation/renderers/learning.renderer.ts";
const MODEL = layoutLearning([
{
code: "aa",
label: "Start",
owner: "disciplined-methodology",
stops: [
{ id: "aa1", label: "01 - The loop", path: "/disciplined-methodology", requires: [] },
{ id: "aa2", label: "02 - Who does what", path: "/disciplined-methodology#who", requires: ["aa1"] },
],
},
]);
const GEOMETRY: Geometry = { getPointAtLength: (at) => ({ x: at, y: at }), getTotalLength: () => 100 };
describe("framedOf", () => {
it("answers with nothing when the vector carries no measurable geometry", () => {
const view = renderLearning(MODEL);
expect(framedOf(view)).toBeNull();
});
it("answers with nothing for a view whose trace cannot be measured", () => {
const view: LearningView = {
stops: [],
trace: document.createElement("div"),
vector: document.createElement("div"),
};
expect(framedOf(view)).toBeNull();
});
});
describe("marksOf", () => {
it("places one mark per stop at the nearest point on the trace", () => {
const { stops } = renderLearning(MODEL);
const marks = marksOf(GEOMETRY, stops, GEOMETRY.getTotalLength());
expect(marks).toHaveLength(stops.length);
for (const mark of marks) {
expect(mark.lit).toBe(false);
expect(mark.at).toBeGreaterThanOrEqual(0);
}
});
});
describe("spansOf", () => {
it("spans each neighboring pair and flags the ones that cross a block", () => {
const { stops } = renderLearning(MODEL);
const marks = marksOf(GEOMETRY, stops, GEOMETRY.getTotalLength());
const spans = spansOf(marks);
expect(spans).toHaveLength(Math.max(0, marks.length - 1));
for (const span of spans) {
expect(span.fast).toBe(false);
}
});
});
describe("paceAt", () => {
it("runs slow inside a block and fast across one", () => {
expect(paceAt([{ fast: true, from: 0, to: 10 }], 5)).toBeGreaterThan(1);
expect(paceAt([{ fast: false, from: 0, to: 10 }], 5)).toBe(1);
expect(paceAt([], 5)).toBe(1);
});
});
describe("widestOf", () => {
it("measures the widest stop", () => {
const { stops } = renderLearning(MODEL);
expect(widestOf(stops)).toBeGreaterThan(0);
expect(widestOf([] as readonly PlacedStop[])).toBe(0);
});
});
describe("the camera fit", () => {
it("reads a length in rem or px and the touch token from the root", () => {
expect(pixelsOfLength("1.5rem", 16)).toBe(24);
expect(pixelsOfLength("30px", 16)).toBe(30);
expect(pixelsOfLength("", 16)).toBe(0);
expect(touchTargetOf(document.documentElement)).toBeGreaterThanOrEqual(0);
});
it("aims one device pixel past the target, so rounding never lands a stop below it", () => {
expect(aimOf(24, 2)).toBe(24.5);
expect(aimOf(24, 0)).toBe(25);
expect(aimOf(0, 2)).toBe(0);
});
it("keeps the desktop zoom where a stop already meets the target, and narrows it on a phone", () => {
const { stops } = renderLearning(MODEL);
const shortest = shortestOf(stops);
const widest = widestOf(stops);
const phoneWidth = (widest * 2 * 24) / shortest;
expect(shortest).toBeGreaterThan(0);
expect(cameraFit(10_000, stops, 24, false).zoom).toBeCloseTo(2.6);
expect(cameraFit(phoneWidth, stops, 24, false).zoom).toBeCloseTo(2);
expect(cameraFit(phoneWidth, stops, 0, false)).toStrictEqual({ lift: 3.4, zoom: 2.6 });
});
it("caps the zoom-out between laps on a touch screen only", () => {
const { stops } = renderLearning(MODEL);
const phoneWidth = (widestOf(stops) * 2 * 24) / shortestOf(stops);
expect(cameraFit(phoneWidth, stops, 24, true).lift).toBeCloseTo(1);
expect(cameraFit(phoneWidth, stops, 24, false).lift).toBeCloseTo(3.4);
expect(cameraFit(10_000, stops, 24, true).lift).toBeCloseTo(3.4);
});
});
describe("attachPrerequisites", () => {
it("lights the stops a hovered stop requires, and clears them when the pointer leaves", () => {
const view = renderLearning(MODEL);
const [first, second] = view.stops;
const target = second?.node.parentElement;
const dispose = attachPrerequisites(view);
target?.dispatchEvent(new Event("mouseenter"));
expect(first?.node.classList.contains("required")).toBe(true);
expect(second?.node.classList.contains("focused")).toBe(true);
target?.dispatchEvent(new Event("mouseleave"));
expect(first?.node.classList.contains("required")).toBe(false);
dispose();
});
it("lights nothing for a stop that requires nothing", () => {
const view = renderLearning(MODEL);
const [first, second] = view.stops;
const dispose = attachPrerequisites(view);
first?.node.parentElement?.dispatchEvent(new Event("focusin"));
expect(second?.node.classList.contains("required")).toBe(false);
dispose();
expect(first?.node.classList.contains("focused")).toBe(false);
});
});
describe("attachLearningMap", () => {
it("disposes without animating when the vector cannot be measured", () => {
const view = renderLearning(MODEL);
const dispose = attachLearningMap(document.createElement("div"), view);
expect(() => {
dispose();
}).not.toThrow();
});
});