use crate::diagrams::flowchart::compile_to_graph;
use crate::engines::graph::algorithms::layered::{
LabelDummyPlacement, LabelDummyRouting, LabelSideStrategy, LayoutConfig,
build_float_layout_with_flags,
};
use crate::graph::grid::GridLayoutConfig;
use crate::graph::measure::default_proportional_text_metrics;
use crate::graph::routing::{EdgeRouting, route_graph_geometry};
use crate::mermaid::parse_flowchart;
#[test]
fn labeled_edge_emits_two_waypoints_under_bend_routing_td() {
let flowchart = parse_flowchart("graph TD\n A -->|hi| B\n").expect("fixture parses");
let mut diagram = compile_to_graph(&flowchart);
let _ = &mut diagram;
let grid_config = GridLayoutConfig::default();
let engine_flags = LayoutConfig {
label_dummy_placement: LabelDummyPlacement::WidestLayer,
label_dummy_routing: LabelDummyRouting::Bend,
label_side_selection: true,
..Default::default()
};
let metrics = default_proportional_text_metrics();
let geom = build_float_layout_with_flags(
&diagram,
&grid_config,
&metrics,
EdgeRouting::PolylineRoute,
true,
Some(&engine_flags),
);
let edge = geom
.edges
.iter()
.find(|e| e.index == 0)
.expect("the single labeled edge must be present");
let path = edge
.layout_path_hint
.as_ref()
.expect("edge layout_path_hint must be populated for TD labeled edges");
assert!(
path.len() >= 4,
"expected ≥4 waypoints (two around the label), got {path:?}"
);
let pre = path[1];
let post = path[2];
assert!(
(pre.x - post.x).abs() < 0.01,
"pre/post x must align in TD: {pre:?} vs {post:?}"
);
assert!(
pre.y < post.y,
"pre must be above post in TD: {pre:?} vs {post:?}"
);
}
#[test]
fn user_rl_repro_edges_do_not_cross_peer_label_rects() {
let src = "graph RL\n A -->|x| B\n A -->|yes<br>no| B\n";
let flowchart = parse_flowchart(src).expect("fixture parses");
let diagram = compile_to_graph(&flowchart);
let grid_config = GridLayoutConfig::default();
let engine_flags = LayoutConfig {
label_dummy_placement: LabelDummyPlacement::WidestLayer,
label_dummy_routing: LabelDummyRouting::Bend,
label_side_selection: true,
label_side_strategy: LabelSideStrategy::DirectionDown,
..Default::default()
};
let metrics = default_proportional_text_metrics();
let geom = build_float_layout_with_flags(
&diagram,
&grid_config,
&metrics,
EdgeRouting::PolylineRoute,
true,
Some(&engine_flags),
);
let routed = route_graph_geometry(&diagram, &geom, EdgeRouting::PolylineRoute, &metrics);
fn rect_contains(
rect: &crate::graph::space::FRect,
p: &crate::graph::geometry::FPoint,
) -> bool {
p.x >= rect.x && p.x <= rect.x + rect.width && p.y >= rect.y && p.y <= rect.y + rect.height
}
for (i, e_i) in routed.edges.iter().enumerate() {
let Some(lg_i) = e_i.label_geometry.as_ref() else {
continue;
};
for (j, e_j) in routed.edges.iter().enumerate() {
if i == j {
continue;
}
for p in &e_j.path {
assert!(
!rect_contains(&lg_i.rect, p),
"edge {j}'s path point {p:?} passes through edge {i}'s label rect {:?}",
lg_i.rect
);
}
}
}
}
#[test]
fn bent_label_dummy_waypoints_flip_symmetrically_on_reversed_edges() {
let src = "graph TD\n A --> B\n B -->|loop| A\n";
let flowchart = parse_flowchart(src).expect("fixture parses");
let diagram = compile_to_graph(&flowchart);
let grid_config = GridLayoutConfig::default();
let engine_flags = LayoutConfig {
label_dummy_placement: LabelDummyPlacement::WidestLayer,
label_dummy_routing: LabelDummyRouting::Bend,
label_side_selection: true,
acyclic: true,
..Default::default()
};
let metrics = default_proportional_text_metrics();
let geom = build_float_layout_with_flags(
&diagram,
&grid_config,
&metrics,
EdgeRouting::PolylineRoute,
true,
Some(&engine_flags),
);
let loop_edge = geom
.edges
.iter()
.find(|e| diagram.edges[e.index].label.as_deref() == Some("loop"))
.expect("loop edge present");
let path = loop_edge
.layout_path_hint
.as_ref()
.expect("loop edge has layout_path_hint");
assert!(
path.len() >= 4,
"expected 2 label waypoints + 2 endpoints, got {path:?}"
);
let (pre, post) = (path[1], path[2]);
assert!(
pre.y > post.y,
"reversed edge's label waypoints must flip: pre.y={} post.y={}",
pre.y,
post.y
);
}
#[test]
fn edge_label_info_padding_includes_edge_label_spacing_and_thickness() {
let flowchart = parse_flowchart("graph TD\n A -->|label| B\n").expect("fixture parses");
let diagram = compile_to_graph(&flowchart);
let grid_config = GridLayoutConfig::default();
let engine_flags = LayoutConfig {
label_dummy_placement: LabelDummyPlacement::WidestLayer,
label_dummy_routing: LabelDummyRouting::Bend,
label_side_selection: true,
edge_label_spacing: 40.0,
..Default::default()
};
let metrics = default_proportional_text_metrics();
let (_unpadded_w, unpadded_h) = metrics.edge_label_dimensions("label");
let geom = build_float_layout_with_flags(
&diagram,
&grid_config,
&metrics,
EdgeRouting::PolylineRoute,
true,
Some(&engine_flags),
);
let edge = geom.edges.iter().find(|e| e.index == 0).unwrap();
let path = edge.layout_path_hint.as_ref().unwrap();
let (pre, post) = (path[1], path[2]);
let effective_h = (post.y - pre.y).abs();
let default_thickness = 1.0_f64;
let expected_min = unpadded_h + engine_flags.edge_label_spacing + default_thickness - 0.01;
assert!(
effective_h >= expected_min,
"label dummy height {effective_h} not padded to ≥{expected_min} (ELK formula: {unpadded_h} + spacing {} + thickness {default_thickness}); the caller override was likely dropped before reaching the kernel",
engine_flags.edge_label_spacing
);
}
#[test]
fn text_render_honors_edge_label_spacing() {
use crate::engines::graph::LayoutConfig as PublicLayoutConfig;
use crate::{OutputFormat, RenderConfig};
let src = "graph TD\n A -->|label| B\n";
let mk = |spacing: f64| RenderConfig {
layout: PublicLayoutConfig {
edge_label_spacing: spacing,
..PublicLayoutConfig::default()
},
..RenderConfig::default()
};
let text_small = crate::render_diagram(src, OutputFormat::Text, &mk(2.0)).unwrap();
let text_big = crate::render_diagram(src, OutputFormat::Text, &mk(40.0)).unwrap();
assert_ne!(
text_small, text_big,
"edge_label_spacing must visibly affect Text output"
);
let small_rows = text_small.lines().count();
let big_rows = text_big.lines().count();
assert!(
big_rows > small_rows + 1,
"increasing edge_label_spacing must add rows between labeled ranks: small={small_rows}, big={big_rows}"
);
}
#[test]
fn canonical_proportional_solve_honors_edge_label_spacing_override() {
use crate::engines::graph::algorithms::layered::run_layered_layout;
use crate::engines::graph::contracts::{EngineConfig, MeasurementMode};
let flowchart = parse_flowchart("graph TD\n A -->|label| B\n").expect("fixture parses");
let diagram = compile_to_graph(&flowchart);
let metrics = default_proportional_text_metrics();
let mode = MeasurementMode::Proportional(metrics);
let baseline = LayoutConfig {
label_dummy_placement: LabelDummyPlacement::WidestLayer,
label_dummy_routing: LabelDummyRouting::Bend,
label_side_selection: true,
edge_label_spacing: 2.0,
..Default::default()
};
let widened = LayoutConfig {
edge_label_spacing: 40.0,
..baseline.clone()
};
let baseline_geom =
run_layered_layout(&mode, &diagram, &EngineConfig::Layered(baseline)).unwrap();
let widened_geom =
run_layered_layout(&mode, &diagram, &EngineConfig::Layered(widened)).unwrap();
let dy = widened_geom.bounds.height - baseline_geom.bounds.height;
assert!(
dy >= 37.0,
"widening edge_label_spacing 2→40 must grow TD bounds by ≥ (40 − 2) px; got ΔH={dy} (bounds baseline={:?}, widened={:?})",
baseline_geom.bounds,
widened_geom.bounds,
);
}
#[test]
fn grid_text_bounds_grow_with_edge_label_spacing() {
use crate::engines::graph::algorithms::layered::run_layered_layout;
use crate::engines::graph::contracts::{EngineConfig, MeasurementMode};
let flowchart = parse_flowchart("graph TD\n A -->|label| B\n").expect("fixture parses");
let diagram = compile_to_graph(&flowchart);
let mode = MeasurementMode::Grid;
let small = LayoutConfig {
label_dummy_placement: LabelDummyPlacement::WidestLayer,
label_dummy_routing: LabelDummyRouting::Bend,
label_side_selection: true,
edge_label_spacing: 2.0,
..Default::default()
};
let big = LayoutConfig {
edge_label_spacing: 20.0,
..small.clone()
};
let small_geom = run_layered_layout(&mode, &diagram, &EngineConfig::Layered(small)).unwrap();
let big_geom = run_layered_layout(&mode, &diagram, &EngineConfig::Layered(big)).unwrap();
let dy = big_geom.bounds.height - small_geom.bounds.height;
assert!(
dy >= 1.0,
"Grid bounds must grow with edge_label_spacing 2→20; got ΔH={dy} (small={:?}, big={:?})",
small_geom.bounds,
big_geom.bounds,
);
}