Files
learnharness/tests/render-settings.test.ts
T
amosblomqvist 11bca055dd Label gap, proportional glow, hover-out fade, centered coarse sliders
- LABEL_GAP 6 -> 2 (user: labels sit way too far below their nodes).
- Hover glow is now PROPORTIONAL to the disc — outer radius = node radius
  x 1.7 (GLOW_SCALE), matching the blur-a-copy-of-the-disc mental model,
  instead of the old constant +6px pad that read huge on small nodes and
  thin on big ones. GLOW_ALPHA 0.45 -> 0.3.
- Hover-OUT fade was broken by construction: draw() gated the glow on
  hoverId, which nulls instantly on leave, so the 180ms fade-out ran
  invisibly and the glow popped off in one frame. New hoverNode field
  outlives hoverId through the outgoing fade (cleared when the fade
  completes); verified live: 8 distinct frames over ~200ms then stable.
- Sliders rescaled per spec: ALL five are now log-scaled 0-100 with the
  default dead-center at 50 (ranges chosen as default/4..default*4:
  node/arrow/label size 0.5-8, edge width 0.25-4, labelZoom 0.0625-4),
  coarse step 5 (21 stops; labels-until-zoom was "way too granular" at
  201). Generic toSliderPosition/fromSliderPosition replace the
  labelZoom-only pair; sanitize clamps follow the same range constants;
  size sliders display compact values ("2", "1.6", "0.75").
EOF
2026-09-13 07:49:00 +03:00

118 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests for src/core/render-settings.ts — the shared settings contract
* (shape, defaults, sanitizer, label-zoom slider mapping). Pure module,
* pure tests: whatever JSON arrives from disk or the network goes through
* sanitizeRenderSettings and must come out valid, never throw.
*/
import assert from "node:assert/strict";
import { describe, it as test } from "node:test";
import {
ARROW_LENGTH_RANGE,
DEFAULT_RENDER_SETTINGS,
EDGE_WIDTH_RANGE,
LABEL_SIZE_RANGE,
LABEL_ZOOM_MAX,
LABEL_ZOOM_MIN,
NODE_SIZE_RANGE,
fromSliderPosition,
labelZoomDisplay,
sanitizeRenderSettings,
toSliderPosition,
} from "../src/core/render-settings.ts";
describe("sanitizeRenderSettings", () => {
test("garbage in → full defaults out", () => {
assert.deepEqual(sanitizeRenderSettings(null), DEFAULT_RENDER_SETTINGS);
assert.deepEqual(sanitizeRenderSettings(undefined), DEFAULT_RENDER_SETTINGS);
assert.deepEqual(sanitizeRenderSettings("junk"), DEFAULT_RENDER_SETTINGS);
assert.deepEqual(sanitizeRenderSettings(42), DEFAULT_RENDER_SETTINGS);
});
test("partial input → defaults fill the gaps", () => {
const s = sanitizeRenderSettings({ nodeSize: 6 });
assert.equal(s.nodeSize, 6);
assert.equal(s.edgeWidth, DEFAULT_RENDER_SETTINGS.edgeWidth);
assert.equal(s.labelSize, DEFAULT_RENDER_SETTINGS.labelSize);
assert.equal(s.labelZoom, DEFAULT_RENDER_SETTINGS.labelZoom);
assert.equal(s.clusterLabels, DEFAULT_RENDER_SETTINGS.clusterLabels);
});
test("out-of-range values clamp; wrong types reset", () => {
const s = sanitizeRenderSettings({
nodeSize: 999,
edgeWidth: 0,
arrowLength: -5,
labelZoom: 100,
labelSize: "big" as unknown as number,
clusterLabels: "yes" as unknown as boolean,
});
assert.equal(s.nodeSize, NODE_SIZE_RANGE.max); // clamped to max
assert.equal(s.edgeWidth, EDGE_WIDTH_RANGE.min); // clamped to min
assert.equal(s.arrowLength, ARROW_LENGTH_RANGE.min);
assert.equal(s.labelZoom, LABEL_ZOOM_MAX);
assert.equal(s.labelSize, DEFAULT_RENDER_SETTINGS.labelSize); // NaN-safe reset
assert.equal(s.clusterLabels, DEFAULT_RENDER_SETTINGS.clusterLabels);
});
test("the low ends accept small sizes (log-slider floors)", () => {
const s = sanitizeRenderSettings({ nodeSize: 0.5, edgeWidth: 0.25, arrowLength: 0.5, labelSize: 0.5 });
assert.deepEqual(
{ nodeSize: s.nodeSize, edgeWidth: s.edgeWidth, arrowLength: s.arrowLength, labelSize: s.labelSize },
{ nodeSize: 0.5, edgeWidth: 0.25, arrowLength: 0.5, labelSize: 0.5 },
);
});
test("a fully valid object passes through unchanged", () => {
const valid = { nodeSize: 4, edgeWidth: 2, arrowLength: 3, labelZoom: 0.25, labelSize: 4, clusterLabels: false };
assert.deepEqual(sanitizeRenderSettings(valid), valid);
});
test("the pre-wave stored shape (no labelSize) still loads", () => {
// localStorage-era records lacked labelSize — must not shift anything.
const s = sanitizeRenderSettings({ nodeSize: 4, edgeWidth: 1, arrowLength: 5, labelZoom: 0.5, clusterLabels: true });
assert.equal(s.labelSize, DEFAULT_RENDER_SETTINGS.labelSize);
assert.equal(s.nodeSize, 4);
});
});
describe("slider mapping (log scale, default-centered)", () => {
const RANGES: [string, number, number, number][] = [
["nodeSize", NODE_SIZE_RANGE.min, NODE_SIZE_RANGE.max, DEFAULT_RENDER_SETTINGS.nodeSize],
["edgeWidth", EDGE_WIDTH_RANGE.min, EDGE_WIDTH_RANGE.max, DEFAULT_RENDER_SETTINGS.edgeWidth],
["arrowLength", ARROW_LENGTH_RANGE.min, ARROW_LENGTH_RANGE.max, DEFAULT_RENDER_SETTINGS.arrowLength],
["labelSize", LABEL_SIZE_RANGE.min, LABEL_SIZE_RANGE.max, DEFAULT_RENDER_SETTINGS.labelSize],
["labelZoom", LABEL_ZOOM_MIN, LABEL_ZOOM_MAX, DEFAULT_RENDER_SETTINGS.labelZoom],
];
test("every default sits dead-center at position 50", () => {
for (const [name, min, max, dflt] of RANGES)
assert.equal(Math.round(toSliderPosition(dflt, min, max)), 50, name);
});
test("round-trips both directions across the whole span", () => {
for (const [name, min, max] of RANGES) {
for (let p = 0; p <= 100; p += 5) {
const v = fromSliderPosition(p, min, max);
assert.ok(Math.abs(toSliderPosition(v, min, max) - p) < 1e-9, `${name} @ ${p}`);
}
}
});
test("slider extremes map to the range ends and clamp beyond", () => {
for (const [name, min, max] of RANGES) {
assert.equal(fromSliderPosition(0, min, max), min, name);
assert.ok(Math.abs(fromSliderPosition(100, min, max) - max) < 1e-9, name); // exp/log float noise
assert.equal(toSliderPosition(min * 0.1, min, max), 0, name); // clamps
assert.equal(toSliderPosition(max * 10, min, max), 100, name);
}
});
test("display units normalize the default to 1", () => {
assert.equal(labelZoomDisplay(0.5), "1×");
assert.equal(labelZoomDisplay(0.25), "0.5×");
assert.equal(labelZoomDisplay(4), "8×");
assert.equal(labelZoomDisplay(0.0625), "0.13×"); // rounded to 2 decimals
});
});