import assert from "node:assert/strict"; import test from "node:test"; import type { DiagramIRV2 } from "../../model/diagram-ir.js"; import { applyMetricsHeatmap } from "./heatmap.js"; import { contrastRatio } from "../themes/theme-service.js"; const IR: DiagramIRV2 = { version: 2, pages: [{ id: "p", title: "Page", nodes: [ { id: "low", label: "Low", style: "shape=hexagon;strokeColor=#000000;", geometry: { x: 0, y: 0, width: 100, height: 60 } }, { id: "high", label: "High", style: "shape=ellipse;", geometry: { x: 200, y: 0, width: 100, height: 60 } }, ], edges: [{ id: "flow", source: "low", target: "high", waypoints: [{ x: 150, y: 30 }] }] }], }; test("metrics heatmap is deterministic with legend metadata and preserves topology", () => { const request = { metric: "latency_ms", min: 0, max: 100, values: { "p/low": 0, "p/high": 100 } }; const snapshot = structuredClone(IR); const first = applyMetricsHeatmap(IR, request); assert.deepEqual(first, applyMetricsHeatmap(IR, request)); assert.deepEqual(IR, snapshot); assert.deepEqual(first.legend, { metric: "latency_ms", min: 0, max: 100, stops: [ { value: 0, color: "#440154", fontColor: "#FFFFFF" }, { value: 25, color: "#3B528B", fontColor: "#FFFFFF" }, { value: 50, color: "#21918C", fontColor: "#1A1A1A" }, { value: 75, color: "#5EC962", fontColor: "#1A1A1A" }, { value: 100, color: "#FDE725", fontColor: "#1A1A1A" }, ], }); assert.match(first.diagram.pages[0].nodes[0].style!, /shape=hexagon/); assert.match(first.diagram.pages[0].nodes[0].style!, /fillColor=#440154/); assert.match(first.diagram.pages[0].nodes[1].style!, /fillColor=#FDE725/); assert.deepEqual(first.diagram.pages[0].edges, IR.pages[0].edges); assert.deepEqual(first.diagram.pages[0].nodes.map(({ id, geometry }) => ({ id, geometry })), IR.pages[0].nodes.map(({ id, geometry }) => ({ id, geometry }))); }); test("metrics heatmap sets readable text for every palette stop", () => { const result = applyMetricsHeatmap(IR, { metric: "latency_ms", min: 0, max: 100, values: { "p/low": 0, "p/high": 100 }, }); for (const stop of result.legend.stops) { assert.ok(contrastRatio(stop.fontColor, stop.color) >= 4.5, `${stop.fontColor} on ${stop.color}`); } assert.match(result.diagram.pages[0].nodes[0].style!, /fontColor=#FFFFFF/); assert.match(result.diagram.pages[0].nodes[1].style!, /fontColor=#1A1A1A/); }); test("metrics heatmap switches colors at the legend stop boundaries", () => { const colorsAt = (low: number, high: number): string[] => applyMetricsHeatmap(IR, { metric: "latency_ms", min: 0, max: 100, values: { "p/low": low, "p/high": high }, }).diagram.pages[0].nodes.map((node) => /fillColor=(#[0-9A-F]{6})/i.exec(node.style!)?.[1] ?? ""); assert.deepEqual(colorsAt(24.999, 25), ["#440154", "#3B528B"]); assert.deepEqual(colorsAt(49.999, 50), ["#3B528B", "#21918C"]); assert.deepEqual(colorsAt(74.999, 75), ["#21918C", "#5EC962"]); assert.deepEqual(colorsAt(99.999, 100), ["#5EC962", "#FDE725"]); }); test("metrics heatmap rejects palettes without a readable text color", () => { assert.throws(() => applyMetricsHeatmap(IR, { metric: "x", min: 0, max: 1, values: {}, colors: ["#777777", "#777777"], }), /no readable font color/i); }); test("metrics heatmap rejects invalid bounds, colors, keys, and out-of-range values", () => { assert.throws(() => applyMetricsHeatmap(IR, { metric: "x", min: 1, max: 1, values: {} }), /min.*max/i); assert.throws(() => applyMetricsHeatmap(IR, { metric: "x", min: 0, max: 1, values: { "p/low": 2 } }), /bounded/i); assert.throws(() => applyMetricsHeatmap(IR, { metric: "x", min: 0, max: 1, values: { missing: 0 } }), /unknown/i); assert.throws(() => applyMetricsHeatmap(IR, { metric: "x", min: 0, max: 1, values: {}, colors: ["red"] }), /color/i); });