Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/test/flow-layout.test.mjs
iris 68e289fff5 swarm-ui: rewire a notification flow by drag or keyboard
The flow editor can now change a flow's shape on the canvas, not only
through the inspector's fields:

- drag a box onto another to move it and its branch there; a drop the
  save would refuse shows the target dashed red and gives the reason;
- a palette of node kinds adds a node below the picked box on click, or
  below any box it is dragged onto; it offers what the controller lists;
- arrow keys walk the tree, M picks a box up and Enter drops it under
  the focused one, Alt+Up/Down reorder a node among its siblings, Delete
  removes a branch; focus follows the edit;
- each box with siblings says when it runs, and the inspector can move
  it earlier or later, since the router runs children in edge order;
- request_reviewer is a second leaf kind beside todo, with a reviewer
  field;
- leaving with unsaved edits asks first, also for in-app links and the
  back button (a router hook runs navigation past a page's guard).
2026-10-24 15:08:48 +02:00

162 lines
4.8 KiB
JavaScript

// Where the flow editor's canvas puts each node. Run by `npm test`.
import assert from "node:assert/strict";
import { test } from "node:test";
import {
layoutFlow,
neighbour,
nodeAt,
NODE_HEIGHT,
NODE_WIDTH,
} from "../src/pages/notifications/FlowLayout.ts";
const match = (id) => ({ id, kind: "match", when: [] });
const source = { id: "s", kind: "source", events: ["issues"] };
const flow = (nodes, edges) => ({
id: "f",
name: "f",
enabled: true,
nodes,
edges: edges.map(([from, to]) => ({ from, to })),
});
const at = (layout, id) => layout.nodes.find((n) => n.id === id);
test("a lone source sits top left", () => {
const l = layoutFlow(flow([source], []));
assert.equal(l.nodes.length, 1);
assert.equal(at(l, "s").depth, 0);
assert.equal(l.edges.length, 0);
assert.ok(l.width > NODE_WIDTH && l.height > NODE_HEIGHT);
});
test("leaves take rows in edge order and a parent sits between its children", () => {
const l = layoutFlow(
flow(
[source, match("a"), match("b"), match("c")],
[
["s", "a"],
["s", "b"],
["a", "c"],
],
),
);
// c (under a) is the first leaf, b the second; a follows c, s the middle.
assert.ok(at(l, "c").y < at(l, "b").y);
assert.equal(at(l, "a").y, at(l, "c").y);
assert.equal(at(l, "s").y, (at(l, "a").y + at(l, "b").y) / 2);
assert.equal(at(l, "c").depth, 2);
assert.ok(at(l, "c").x > at(l, "a").x && at(l, "a").x > at(l, "s").x);
});
test("no two nodes overlap", () => {
const nodes = [source];
const edges = [];
for (let i = 0; i < 6; i += 1) {
nodes.push(match(`m${i}`), match(`n${i}`));
edges.push(["s", `m${i}`], [`m${i}`, `n${i}`]);
}
const l = layoutFlow(flow(nodes, edges));
for (const a of l.nodes) {
for (const b of l.nodes) {
if (a === b) continue;
const apart =
a.x + NODE_WIDTH <= b.x ||
b.x + NODE_WIDTH <= a.x ||
a.y + NODE_HEIGHT <= b.y ||
b.y + NODE_HEIGHT <= a.y;
assert.ok(apart, `${a.id} overlaps ${b.id}`);
}
}
});
test("an edge runs from the parent's right side to the child's left", () => {
const l = layoutFlow(flow([source, match("a")], [["s", "a"]]));
const [e] = l.edges;
assert.equal(e.x1, at(l, "s").x + NODE_WIDTH);
assert.equal(e.x2, at(l, "a").x);
assert.equal(e.y1, at(l, "s").y + NODE_HEIGHT / 2);
assert.equal(e.y2, at(l, "a").y + NODE_HEIGHT / 2);
});
test("a node nothing reaches yet still gets a place, below the tree", () => {
const l = layoutFlow(flow([source, match("a"), match("lost")], [["s", "a"]]));
assert.equal(l.nodes.length, 3);
assert.ok(at(l, "lost").y > at(l, "a").y);
assert.equal(l.edges.length, 1);
});
test("edges to unknown nodes are left out, and a cycle terminates", () => {
const l = layoutFlow(
flow(
[source, match("a"), match("b")],
[
["s", "a"],
["a", "b"],
["b", "a"],
["s", "ghost"],
],
),
);
assert.equal(l.nodes.length, 3);
assert.ok(l.edges.every((e) => e.to !== "ghost"));
});
test("a node with two parents is placed once", () => {
const l = layoutFlow(
flow(
[source, match("a"), match("b"), match("x")],
[
["s", "a"],
["s", "b"],
["a", "x"],
["b", "x"],
],
),
);
assert.equal(l.nodes.length, 4);
const spots = new Set(l.nodes.map((n) => `${n.x},${n.y}`));
assert.equal(spots.size, 4, "no two nodes share a spot");
});
test("an empty flow has an empty layout", () => {
const l = layoutFlow(flow([], []));
assert.deepEqual(l.nodes, []);
assert.ok(l.width >= 0 && l.height >= 0);
});
test("nodeAt finds the box holding a point and nothing between boxes", () => {
const l = layoutFlow(flow([source, match("a")], [["s", "a"]]));
const a = at(l, "a");
assert.equal(nodeAt(l, a.x + 5, a.y + 5), "a");
assert.equal(
nodeAt(l, a.x + NODE_WIDTH, a.y + NODE_HEIGHT),
"a",
"the edge counts",
);
assert.equal(nodeAt(l, a.x - 1, a.y + 5), undefined);
assert.equal(nodeAt(l, 5000, 5000), undefined);
});
test("neighbour walks to the parent, the first child and the box above or below", () => {
const f = flow(
[source, match("a"), match("b"), match("c")],
[
["s", "a"],
["s", "b"],
["a", "c"],
],
);
const l = layoutFlow(f);
assert.equal(neighbour(l, f, "a", "left"), "s");
assert.equal(neighbour(l, f, "s", "right"), "a", "the first in edge order");
assert.equal(
neighbour(l, f, "s", "left"),
undefined,
"nothing left of the source",
);
assert.equal(neighbour(l, f, "c", "right"), undefined, "a leaf has no child");
// a sits above b in the same column.
assert.equal(neighbour(l, f, "a", "down"), "b");
assert.equal(neighbour(l, f, "b", "up"), "a");
assert.equal(neighbour(l, f, "a", "up"), undefined);
assert.equal(neighbour(l, f, "nope", "up"), undefined);
});