Files
90-RED 734ce4173f v1.4.3 — faster progressive reveal + fix links vanishing after drag
Worker prewarm, viewport-first stable batches, and adaptive reveal pacing
(default on, clamp 6–14ms). On drag settle force OVG rebuild and never drop
the last legal path when route/stretch fail.
2026-07-30 10:44:53 +08:00

191 lines
7.3 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {
orderHeldRouteCandidates,
orderRouteCandidates,
pathBounds,
pauseRevealCount,
shouldForceGraphRebuildAfterSettle,
shouldFreezeHiddenModeLink,
shouldQueueIdleCleanup,
shouldRacePauseLink,
shouldRouteHeldCollision,
shouldStretchDragPath,
shouldUseDragSettle,
} from "../web/drag-policy.js";
test("passive Vue node size updates do not enter drag-settle mode", () => {
assert.equal(
shouldUseDragSettle(true, { positionChanges: 0, sizeChanges: 1 }, {}),
false,
);
});
test("held reroutes prioritize self, viewport, then expand outward", () => {
const viewport = { x: 0, y: 0, x2: 100, y2: 100 };
const ordered = orderHeldRouteCandidates([
{ id: "far", direct: false, bounds: { x: 300, y: 0, x2: 320, y2: 20 } },
{ id: "visible", direct: false, bounds: { x: 20, y: 20, x2: 40, y2: 40 } },
{ id: "self", direct: true, bounds: { x: 500, y: 500, x2: 520, y2: 520 } },
{ id: "near", direct: false, bounds: { x: 120, y: 0, x2: 140, y2: 20 } },
], viewport);
assert.deepEqual(ordered.map((item) => item.id), ["self", "visible", "near", "far"]);
assert.deepEqual(pathBounds([{ x: 5, y: 9 }, { x: -2, y: 20 }]), {
x: -2, y: 9, x2: 5, y2: 20,
});
});
test("stable route candidates prioritize viewport then distance", () => {
const viewport = { x: 0, y: 0, x2: 100, y2: 100 };
const ordered = orderRouteCandidates(
[
{ id: "far", bounds: { x: 300, y: 0, x2: 320, y2: 20 } },
{ id: "visible-b", bounds: { x: 50, y: 50, x2: 70, y2: 70 } },
{ id: "visible-a", bounds: { x: 10, y: 10, x2: 30, y2: 30 } },
{ id: "near", bounds: { x: 120, y: 0, x2: 140, y2: 20 } },
],
viewport,
);
// visible first (stable original index among visibles), then near, then far
assert.deepEqual(
ordered.map((item) => item.id),
["visible-b", "visible-a", "near", "far"],
);
});
test("a held direct link bypasses the unrelated-link freeze branch", () => {
assert.equal(shouldFreezeHiddenModeLink(true, true, false), false);
assert.equal(shouldFreezeHiddenModeLink(false, true, false), true);
assert.equal(shouldFreezeHiddenModeLink(false, true, true), false);
});
test("idle cleanup only queues stale Freeze+Check paths after primary work", () => {
assert.equal(shouldQueueIdleCleanup("freeze-others", true, false, true), true);
assert.equal(shouldQueueIdleCleanup("freeze-others", true, true, true), false);
assert.equal(shouldQueueIdleCleanup("freeze-others-strict", true, false, true), false);
assert.equal(shouldQueueIdleCleanup("freeze-others", false, false, true), false);
});
test("frozen modes slowly route collisions while held still", () => {
assert.equal(shouldRouteHeldCollision("none", true, true), false);
assert.equal(shouldRouteHeldCollision("freeze-others", true, true), true);
assert.equal(shouldRouteHeldCollision("freeze-others-strict", false, true), false);
assert.equal(shouldRouteHeldCollision("freeze-others-strict", true, false), false);
assert.equal(shouldRouteHeldCollision("freeze-others-strict", true, true), true);
assert.equal(shouldRouteHeldCollision("hide-self", true, true), true);
assert.equal(shouldRouteHeldCollision("hide-self", true, true, true), false);
});
test("real node movement and resize handles still use drag settle", () => {
assert.equal(shouldUseDragSettle(true, { positionChanges: 1 }, {}), true);
assert.equal(
shouldUseDragSettle(true, { positionChanges: 0, sizeChanges: 1 }, { resizing_node: {} }),
true,
);
assert.equal(shouldUseDragSettle(false, { positionChanges: 1 }, {}), false);
});
test("sticky stretching never masks live avoidance for a collided link", () => {
assert.equal(
shouldStretchDragPath({
dragging: true,
hasCachedPath: true,
endsMoved: false,
hitDirty: true,
effectiveMode: "freeze-others",
}),
false,
);
assert.equal(
shouldStretchDragPath({
dragging: true,
hasCachedPath: true,
endsMoved: true,
hitDirty: false,
effectiveMode: "freeze-others-strict",
}),
true,
);
assert.equal(
shouldStretchDragPath({
dragging: true,
hasCachedPath: true,
endsMoved: true,
hitDirty: true,
effectiveMode: "freeze-others-strict",
}),
false,
);
});
test("small and medium live modes may stretch only after geometry validates the path", () => {
for (const effectiveMode of ["none", "freeze-others"]) {
assert.equal(
shouldStretchDragPath({
dragging: true,
hasCachedPath: true,
endsMoved: true,
hitDirty: false,
effectiveMode,
}),
true,
`${effectiveMode} should keep a collision-free path stable`,
);
}
});
test("pause reveal drains the whole queue in one frame", () => {
assert.equal(pauseRevealCount(0), 0);
assert.equal(pauseRevealCount(-3), 0);
assert.equal(pauseRevealCount(1), 1);
assert.equal(pauseRevealCount(17), 17);
assert.equal(pauseRevealCount(500), 500);
const framesToDrain = (n) => {
let remaining = n;
let frames = 0;
while (remaining > 0) {
remaining -= Math.min(pauseRevealCount(remaining), remaining);
frames++;
}
return frames;
};
// Pacing was removed: measured pauses last ~100-130ms, so trickled reveals
// never reached the tail links before the drag resumed and discarded them.
for (let n = 1; n <= 500; n++)
assert.equal(framesToDrain(n), 1, `queue ${n} should drain in one frame`);
});
test("held-pause race only takes predicted-cheap links", () => {
assert.equal(shouldRacePauseLink(3, 8, 10), true);
assert.equal(shouldRacePauseLink(9.9, 8, 10), true);
assert.equal(shouldRacePauseLink(10, 8, 10), false); // boundary: not cheaper
assert.equal(shouldRacePauseLink(42, 8, 10), false);
// Unknown links fall back to the session average (usually cheap).
assert.equal(shouldRacePauseLink(undefined, 8, 10), true);
assert.equal(shouldRacePauseLink(undefined, 10, 10), false);
// The race call site now passes an infinite fallback: links without a
// main-thread-measured cost are never raced (worker timings and session
// averages mis-predicted 19-49ms routes as cheap).
assert.equal(shouldRacePauseLink(undefined, Infinity, 10), false);
assert.equal(shouldRacePauseLink(3, Infinity, 10), true);
});
// Regression guard for "line vanishes after drag release" (profiler capture
// LinkRouter_20260730-094654.json: settle frame had routeFailures=7 with
// aStarPops=0 — layoutSignature truncates node coords with x|0, so a drag
// ending at a sub-pixel offset kept the stale OVG and every endpoint lookup
// missed). After a settle that invalidated any path, the next refreshGraph
// must rebuild the graph even when the signature is unchanged. Profiler
// regression condition: the first post-settle route frame must show
// graphRebuilt=true and must NOT show routeFailures>0 together with
// aStarPops=0; failed links must keep their last legal pts on screen
// (prefer a wrong-looking path over a vanishing link).
test("settle with stale paths forces a graph rebuild", () => {
assert.equal(shouldForceGraphRebuildAfterSettle(true), true);
// Nothing invalidated → keep the cheap signature comparison.
assert.equal(shouldForceGraphRebuildAfterSettle(false), false);
assert.equal(shouldForceGraphRebuildAfterSettle(undefined), false);
});