use abstracttui_graph::{layered, Direction, GraphDesc, LayeredOpts, NodeDesc, Point};
fn opts(direction: Direction) -> LayeredOpts {
LayeredOpts {
direction,
..Default::default()
}
}
#[test]
fn bt_rl_waypoints_mirror_like_rects_and_stay_out_of_cards() {
let desc = GraphDesc::new()
.node("a", 5, 3)
.node("b", 5, 3)
.edge("a", "b");
let td = layered(&desc, &opts(Direction::TopDown));
let bt = layered(&desc, &opts(Direction::BottomTop));
assert_eq!(td.bounds, bt.bounds, "mirroring must not move bounds");
let h = td.bounds.h;
let e_td = &td.edges[0];
let e_bt = &bt.edges[0];
assert_eq!(e_td.waypoints.len(), e_bt.waypoints.len());
for (p, q) in e_td.waypoints.iter().zip(&e_bt.waypoints) {
assert_eq!(q.x, p.x, "BT keeps the cross axis");
assert_eq!(
q.y,
h - 1 - p.y,
"BT mirrors waypoint cells exactly like rect cells: {p:?} -> {q:?}"
);
}
for layout in [&td, &bt] {
for e in &layout.edges {
for (name, p) in [
("first", e.waypoints[0]),
("last", *e.waypoints.last().unwrap()),
] {
for n in &layout.nodes {
assert!(
!n.rect.contains(p),
"{name} anchor {p:?} sits inside card {} {:?}",
n.id,
n.rect
);
}
}
}
}
let lr = layered(&desc, &opts(Direction::LeftRight));
let rl = layered(&desc, &opts(Direction::RightLeft));
assert_eq!(lr.bounds, rl.bounds);
let w = lr.bounds.w;
for (p, q) in lr.edges[0].waypoints.iter().zip(&rl.edges[0].waypoints) {
assert_eq!(q.y, p.y, "RL keeps the cross axis");
assert_eq!(q.x, w - 1 - p.x, "RL mirrors waypoint cells");
}
}
#[test]
fn bt_rl_mirror_bounds_never_grow_on_the_stress_fixture() {
let desc = GraphDesc::new()
.node("a", 7, 3)
.node("b", 5, 5)
.node("c", 9, 3)
.node("d", 5, 3)
.edge("a", "b")
.edge("a", "d") .edge("b", "c")
.edge("c", "d")
.edge("d", "a") .edge("b", "b"); let td = layered(&desc, &opts(Direction::TopDown));
let bt = layered(&desc, &opts(Direction::BottomTop));
let lr = layered(&desc, &opts(Direction::LeftRight));
let rl = layered(&desc, &opts(Direction::RightLeft));
assert_eq!(td.bounds, bt.bounds, "BT bounds equal TD");
assert_eq!(lr.bounds, rl.bounds, "RL bounds equal LR");
assert_eq!((td.bounds.x, td.bounds.y), (0, 0), "origin-normalized");
assert_eq!((bt.bounds.x, bt.bounds.y), (0, 0));
}
#[test]
fn self_loop_attaches_to_its_own_node_across_components() {
let desc = GraphDesc::new()
.node("x", 4, 2)
.node("y", 4, 2)
.node("s", 6, 3)
.edge("x", "y") .edge("s", "s"); let layout = layered(&desc, &LayeredOpts::default());
let s = layout.node("s").unwrap().rect;
let lobe = layout
.edges
.iter()
.find(|e| e.from == "s" && e.to == "s")
.expect("self-loop present");
let first = lobe.waypoints[0];
assert_eq!(
first.x,
s.right(),
"lobe anchors on s's right face: {first:?} vs {s:?}"
);
assert!(
first.y >= s.y && first.y < s.bottom(),
"lobe anchors within s's rows: {first:?} vs {s:?}"
);
let ids: Vec<&str> = layout.nodes.iter().map(|n| n.id.as_str()).collect();
assert_eq!(ids, ["x", "y", "s"]);
}
#[test]
fn rank_gap_one_anchors_coincide_in_the_single_corridor_cell() {
let desc = GraphDesc::new()
.node("a", 5, 3)
.node("b", 5, 3)
.edge("a", "b");
let layout = layered(
&desc,
&LayeredOpts {
rank_gap: 1,
..Default::default()
},
);
let e = &layout.edges[0];
assert_eq!(e.waypoints.len(), 2);
assert_eq!(
e.waypoints[0], e.waypoints[1],
"one-cell corridor: both anchors share it"
);
let a = layout.node("a").unwrap().rect;
let b = layout.node("b").unwrap().rect;
let p = e.waypoints[0];
assert!(p.y == a.bottom() && p.y == b.y - 1, "the corridor row");
}
#[test]
fn pava_three_pass_coordinates_are_pinned_on_the_w_fixture() {
let desc = GraphDesc::new()
.node("p1", 6, 3)
.node("p2", 6, 3)
.node("c1", 6, 3)
.node("c2", 6, 3)
.node("c3", 6, 3)
.edge("p1", "c1")
.edge("p1", "c2")
.edge("p2", "c2")
.edge("p2", "c3");
let layout = layered(&desc, &LayeredOpts::default());
let rect = |id: &str| layout.node(id).unwrap().rect;
let again = layered(&desc, &LayeredOpts::default());
assert_eq!(layout.nodes, again.nodes);
assert_eq!(layout.edges, again.edges);
let (c1, c2, c3) = (rect("c1"), rect("c2"), rect("c3"));
assert!(
c1.x < c2.x && c2.x < c3.x,
"children ordered: {c1:?} {c2:?} {c3:?}"
);
let (p1, p2) = (rect("p1"), rect("p2"));
assert!(p1.x < p2.x, "parents ordered");
assert_eq!(
(p1.x, p2.x, c1.x, c2.x, c3.x),
(5, 14, 0, 9, 18),
"W-fixture cross coordinates moved — pass-schedule change?"
);
}
#[test]
fn unresolvable_edges_drop_but_desc_index_keeps_the_join() {
let desc = GraphDesc::new()
.with_node(NodeDesc::new("a", 5, 3))
.with_node(NodeDesc::new("b", 5, 3))
.edge("a", "ghost")
.edge("a", "b");
let layout = layered(&desc, &LayeredOpts::default());
assert_eq!(layout.edges.len(), 1, "ghost edge dropped");
assert_eq!(
layout.edges[0].desc_index, 1,
"survivor still names its OWN desc slot"
);
assert!(
layout.fallback.as_deref().unwrap_or("").contains("skipped"),
"and the drop is labeled: {:?}",
layout.fallback
);
let _ = Point::new(0, 0); }