156 lines
5.4 KiB
JavaScript
156 lines
5.4 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { hoverDrawItems, linkIdAtPoint, animDensityScale, slotLinkIdsAt } from "../web/draw-policy.js";
|
|
|
|
const item = (id, origin, target) => ({
|
|
entry: { link: { id, origin_id: origin, target_id: target } },
|
|
});
|
|
|
|
test("batched hover overlays only related links", () => {
|
|
const routed = [item(1, 1, 2), item(2, 3, 4), item(3, 5, 1)];
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, 1, true).map((x) => x.entry.link.id),
|
|
[1, 3],
|
|
);
|
|
assert.deepEqual(hoverDrawItems(routed, null, true), []);
|
|
});
|
|
|
|
test("fallback hover preserves unrelated order and draws related links last", () => {
|
|
const routed = [item(1, 1, 2), item(2, 3, 4), item(3, 5, 1), item(4, 6, 7)];
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, 1, false).map((x) => x.entry.link.id),
|
|
[2, 4, 1, 3],
|
|
);
|
|
assert.equal(hoverDrawItems(routed, null, false), routed);
|
|
});
|
|
|
|
|
|
// --- single-link hover hit test ---
|
|
|
|
const seg = (id, pts) => ({
|
|
entry: { link: { id, origin_id: 0, target_id: 0 } },
|
|
cached: { pts },
|
|
});
|
|
|
|
test("linkIdAtPoint hits a polyline within tolerance", () => {
|
|
const routed = [seg(1, [{ x: 0, y: 0 }, { x: 100, y: 0 }])];
|
|
assert.equal(linkIdAtPoint(routed, 50, 3, 5), 1);
|
|
assert.equal(linkIdAtPoint(routed, 0, 0, 5), 1); // endpoint
|
|
assert.equal(linkIdAtPoint(routed, 50, 6, 5), null); // just outside tol
|
|
assert.equal(linkIdAtPoint(routed, 200, 0, 5), null); // outside bounds
|
|
assert.equal(linkIdAtPoint(routed, 50, 1, 0), null); // zero tol, off the line
|
|
});
|
|
|
|
test("linkIdAtPoint returns the topmost (last drawn) link on overlap", () => {
|
|
const routed = [
|
|
seg(1, [{ x: 0, y: 0 }, { x: 100, y: 0 }]),
|
|
seg(2, [{ x: 0, y: 2 }, { x: 100, y: 2 }]),
|
|
];
|
|
assert.equal(linkIdAtPoint(routed, 50, 1, 5), 2);
|
|
assert.equal(linkIdAtPoint([], 50, 1, 5), null);
|
|
});
|
|
|
|
// --- adaptive marker density ---
|
|
|
|
test("animDensityScale keeps full density at or below threshold", () => {
|
|
assert.equal(animDensityScale(0, 10), 1);
|
|
assert.equal(animDensityScale(10, 10), 1);
|
|
});
|
|
|
|
test("animDensityScale halves above threshold, drops to 20% past 3x", () => {
|
|
assert.equal(animDensityScale(11, 10), 2);
|
|
assert.equal(animDensityScale(30, 10), 2);
|
|
assert.equal(animDensityScale(31, 10), 5);
|
|
});
|
|
|
|
test("animDensityScale disabled or zero threshold stays full", () => {
|
|
assert.equal(animDensityScale(100, 10, false), 1);
|
|
assert.equal(animDensityScale(100, 0), 1);
|
|
});
|
|
|
|
// --- hoverDrawItems with single-link hover ---
|
|
|
|
test("hoverDrawItems honors hoveredLinkId without node hover", () => {
|
|
const routed = [item(1, 1, 2), item(2, 3, 4), item(3, 5, 1)];
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, null, true, 2).map((x) => x.entry.link.id),
|
|
[2],
|
|
);
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, null, false, 2).map((x) => x.entry.link.id),
|
|
[1, 3, 2],
|
|
);
|
|
});
|
|
|
|
test("hoverDrawItems combines node hover and link hover", () => {
|
|
const routed = [item(1, 1, 2), item(2, 3, 4), item(3, 5, 1)];
|
|
// Hovered items keep their original relative order (routed order).
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, 1, true, 2).map((x) => x.entry.link.id),
|
|
[1, 2, 3],
|
|
);
|
|
// Fallback, node hover only: unrelated link stays first, related last.
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, 1, false, null).map((x) => x.entry.link.id),
|
|
[2, 1, 3],
|
|
);
|
|
});
|
|
|
|
|
|
// --- slot-label hover hit test ---
|
|
|
|
const slotNode = () => ({
|
|
inputs: [
|
|
{ name: "model", link: 42 },
|
|
{ name: "clip", link: null },
|
|
],
|
|
outputs: [{ name: "MODEL", links: [7, 9] }],
|
|
getInputPos: (i) => [100, 100 + i * 25],
|
|
getOutputPos: () => [200, 100],
|
|
});
|
|
|
|
test("slotLinkIdsAt hits input dot and label text", () => {
|
|
const n = slotNode();
|
|
assert.deepEqual(slotLinkIdsAt(n, 95, 100), [42]); // dot area
|
|
assert.deepEqual(slotLinkIdsAt(n, 130, 100), [42]); // label text ("model" = 5 chars → w 55, x in [90,145))
|
|
assert.equal(slotLinkIdsAt(n, 146, 100), null); // past the label
|
|
assert.equal(slotLinkIdsAt(n, 95, 112), null); // between rows
|
|
});
|
|
|
|
test("slotLinkIdsAt on unconnected input returns null (falls back to node hover)", () => {
|
|
const n = slotNode();
|
|
assert.equal(slotLinkIdsAt(n, 95, 125), null); // "clip" dot, no link
|
|
assert.equal(slotLinkIdsAt(n, 125, 125), null); // "clip" label, no link
|
|
});
|
|
|
|
test("slotLinkIdsAt hits output label left of the dot and fans out to all links", () => {
|
|
const n = slotNode();
|
|
assert.deepEqual(slotLinkIdsAt(n, 205, 100), [7, 9]); // dot area
|
|
assert.deepEqual(slotLinkIdsAt(n, 165, 100), [7, 9]); // label text ("MODEL" → w 55, x in (155,210])
|
|
assert.equal(slotLinkIdsAt(n, 154, 100), null); // past the label
|
|
});
|
|
|
|
test("slotLinkIdsAt tolerates missing geometry helpers", () => {
|
|
assert.equal(slotLinkIdsAt({}, 0, 0), null);
|
|
assert.equal(slotLinkIdsAt(null, 0, 0), null);
|
|
assert.equal(slotLinkIdsAt({ inputs: [{ name: "x", link: 1 }] }, 0, 0), null); // no getInputPos
|
|
});
|
|
|
|
// --- hoverDrawItems with slot hover ---
|
|
|
|
test("hoverDrawItems honors hoveredSlotIds set", () => {
|
|
const routed = [item(1, 1, 2), item(2, 3, 4), item(3, 5, 6), item(4, 7, 8)];
|
|
const slotIds = new Set([2, 4]);
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, null, true, null, slotIds).map((x) => x.entry.link.id),
|
|
[2, 4],
|
|
);
|
|
assert.deepEqual(
|
|
hoverDrawItems(routed, null, false, null, slotIds).map((x) => x.entry.link.id),
|
|
[1, 3, 2, 4],
|
|
);
|
|
// Empty set behaves like no slot hover.
|
|
assert.deepEqual(hoverDrawItems(routed, null, true, null, new Set()), []);
|
|
});
|