use pretty_assertions::assert_eq;
use ratatui::style::{Color, Style};
use super::{
art_from_layout, layout_canvas, Compartment, Edge, EdgeHead, Graph, GraphArt, GraphError,
GraphStyles, Node, NodeExtra, NodeStyle, RankOrdering, TextAlignment, WRAP_WIDTH,
};
#[test]
fn preserves_per_node_styles_and_node_geometry() {
let waiting = NodeStyle::new(
Style::default().fg(Color::Blue),
Style::default().fg(Color::Cyan),
);
let running = NodeStyle::new(
Style::default().fg(Color::Red),
Style::default().fg(Color::Yellow),
);
let graph = Graph::top_down(
vec![
Node::rectangular("Waiting node", waiting),
Node::rectangular("Running node", running),
],
vec![Edge::directed(0, 1)],
RankOrdering::PreserveInput,
)
.unwrap();
let art = graph.render(Style::default().fg(Color::DarkGray)).unwrap();
let waiting_text = text_style(&art.lines, "Waiting node");
let running_text = text_style(&art.lines, "Running node");
assert_eq!(waiting_text, waiting.text);
assert_eq!(running_text, running.text);
assert_eq!(
art.plain_lines
.iter()
.flat_map(|line| line.chars())
.filter(|character| *character == '▼')
.count(),
1
);
assert!(art
.lines
.iter()
.flat_map(|line| &line.spans)
.any(|span| { span.style == waiting.border && span.content.chars().any(is_box_drawing) }));
assert!(art
.lines
.iter()
.flat_map(|line| &line.spans)
.any(|span| { span.style == running.border && span.content.chars().any(is_box_drawing) }));
assert!(art.node_rects[0].y < art.node_rects[1].y);
}
#[test]
fn paints_zwj_graphemes_without_shifting_node_borders() {
let graph = Graph::top_down(
vec![Node::rectangular("👩\u{200d}💻", NodeStyle::default())],
Vec::new(),
RankOrdering::PreserveInput,
)
.unwrap();
let art = graph.render(Style::default()).unwrap();
assert_eq!(
art.plain_lines,
vec![
"┌────┐".to_owned(),
"│ 👩\u{200d}💻 │".to_owned(),
"└────┘".to_owned()
]
);
}
#[test]
fn paints_self_loop_endpoint_decorations() {
let mut edge = Edge::directed(0, 0);
edge.head_to = EdgeHead::None;
edge.head_from = EdgeHead::Circle;
let graph = Graph::top_down(
vec![Node::rectangular("Node", NodeStyle::default())],
vec![edge],
RankOrdering::PreserveInput,
)
.unwrap();
let art = graph.render(Style::default()).unwrap();
assert_eq!(
art.plain_lines,
vec![
"┌──────┐".to_owned(),
"│ Node │".to_owned(),
"└────o─┘".to_owned(),
" ││".to_owned(),
" ╰╯".to_owned(),
]
);
}
#[test]
fn aligns_each_compartment_by_its_model() {
let graph = Graph::top_down(
vec![Node::rectangular("unused", NodeStyle::default())],
Vec::new(),
RankOrdering::PreserveInput,
)
.unwrap();
let extras = [NodeExtra::Compartments(vec![
Compartment {
lines: Vec::new(),
alignment: TextAlignment::Left,
},
Compartment {
lines: vec!["T".to_owned()],
alignment: TextAlignment::Center,
},
Compartment {
lines: vec!["field".to_owned()],
alignment: TextAlignment::Left,
},
])];
let layout = layout_canvas(&graph, &extras, None, WRAP_WIDTH).unwrap();
let styles = GraphStyles::for_nodes(&graph.nodes, Style::default());
let art = art_from_layout(&graph, layout, &styles);
assert_eq!(
art.plain_lines,
vec![
" ┌───────┐".to_owned(),
" │ T │".to_owned(),
" ├───────┤".to_owned(),
" │ field │".to_owned(),
" └───────┘".to_owned(),
]
);
}
#[test]
fn renders_an_empty_graph_as_empty_art() {
let art = Graph::top_down(Vec::new(), Vec::new(), RankOrdering::PreserveInput)
.unwrap()
.render(Style::default())
.unwrap();
assert_eq!(
art,
GraphArt {
lines: Vec::new(),
plain_lines: Vec::new(),
width: 0,
height: 0,
node_rects: Vec::new(),
}
);
}
#[test]
fn applies_the_requested_rank_ordering_policy() {
let nodes = vec![
Node::rectangular("A", NodeStyle::default()),
Node::rectangular("B", NodeStyle::default()),
Node::rectangular("X", NodeStyle::default()),
Node::rectangular("Y", NodeStyle::default()),
];
let edges = vec![Edge::directed(0, 3), Edge::directed(1, 2)];
let preserved = Graph::top_down(nodes.clone(), edges.clone(), RankOrdering::PreserveInput)
.unwrap()
.render(Style::default())
.unwrap();
let minimized = Graph::top_down(nodes, edges, RankOrdering::MinimizeCrossings)
.unwrap()
.render(Style::default())
.unwrap();
assert!(preserved.node_rects[2].x < preserved.node_rects[3].x);
assert!(minimized.node_rects[3].x < minimized.node_rects[2].x);
}
#[test]
fn rejects_invalid_edge_endpoints() {
let error = Graph::top_down(
vec![Node::rectangular("only", NodeStyle::default())],
vec![Edge::directed(0, 1)],
RankOrdering::PreserveInput,
)
.unwrap_err();
assert_eq!(error, GraphError::InvalidEdgeEndpoint);
}
fn text_style(lines: &[ratatui::text::Line<'_>], needle: &str) -> Style {
lines
.iter()
.flat_map(|line| &line.spans)
.find(|span| span.content == needle)
.map(|span| span.style)
.expect("node label is rendered")
}
fn is_box_drawing(character: char) -> bool {
matches!(character, '┌' | '┐' | '└' | '┘' | '─' | '│')
}
#[test]
fn fan_in_edges_share_one_bus_row_per_target() {
let style = NodeStyle::default();
let nodes = vec![
Node::rectangular("first", style),
Node::rectangular("second", style),
Node::rectangular("left", style),
Node::rectangular("right", style),
];
let edges = vec![
Edge::directed(0, 2),
Edge::directed(1, 2),
Edge::directed(0, 3),
Edge::directed(1, 3),
];
let graph = Graph::top_down(nodes, edges, RankOrdering::PreserveInput).unwrap();
let art = graph.render(Style::default()).unwrap();
assert_eq!(
art.plain_lines,
vec![
" ┌───────┐ ┌────────┐".to_owned(),
" │ first │ │ second │".to_owned(),
" └───┬───┘ └────┬───┘".to_owned(),
" ├───────────┤".to_owned(),
" ▼ ▼".to_owned(),
" ┌──────┐ ┌───────┐".to_owned(),
" │ left │ │ right │".to_owned(),
" └──────┘ └───────┘".to_owned(),
]
);
}
#[test]
fn rank_skipping_edge_drops_into_the_target_from_above() {
let style = NodeStyle::default();
let nodes = vec![
Node::rectangular("setup", style),
Node::rectangular("review", style),
Node::rectangular("apply", style),
Node::rectangular("skip", style),
];
let edges = vec![
Edge::directed(0, 1),
Edge::directed(1, 2),
Edge::directed(1, 3),
Edge::directed(0, 3),
];
let graph = Graph::top_down(nodes, edges, RankOrdering::PreserveInput).unwrap();
let art = graph.render(Style::default()).unwrap();
assert!(art
.plain_lines
.iter()
.all(|line| !line.contains('\u{25c4}')));
assert_eq!(
art.plain_lines,
vec![
" ┌───────┐".to_owned(),
" │ setup │".to_owned(),
" └───┬───┘".to_owned(),
" ┌──┴───────┐".to_owned(),
" ▼ │".to_owned(),
" ┌────────┐ │".to_owned(),
" │ review │ │".to_owned(),
" └────┬───┘ │".to_owned(),
" ┌────┤ │".to_owned(),
" │ └─────┬────┘".to_owned(),
" ▼ ▼".to_owned(),
" ┌───────┐ ┌──────┐".to_owned(),
" │ apply │ │ skip │".to_owned(),
" └───────┘ └──────┘".to_owned(),
]
);
}
#[test]
fn skip_edges_join_the_shared_fan_in_bus_row() {
let style = NodeStyle::default();
let nodes = vec![
Node::rectangular("collect", style),
Node::rectangular("boundaries", style),
Node::rectangular("spaghetti", style),
Node::rectangular("structure", style),
Node::rectangular("apply", style),
Node::rectangular("none", style),
];
let edges = vec![
Edge::directed(0, 1),
Edge::directed(0, 2),
Edge::directed(0, 3),
Edge::directed(1, 4),
Edge::directed(2, 4),
Edge::directed(3, 4),
Edge::directed(0, 4),
Edge::directed(1, 5),
Edge::directed(2, 5),
Edge::directed(3, 5),
Edge::directed(0, 5),
];
let graph = Graph::top_down(nodes, edges, RankOrdering::PreserveInput).unwrap();
let art = graph.render(Style::default()).unwrap();
assert_eq!(
art.plain_lines,
vec![
" ┌─────────┐".to_owned(),
" │ collect │".to_owned(),
" └─────┬───┘".to_owned(),
" ┌────────────────┼───────────────┬───────┐".to_owned(),
" ▼ ▼ ▼ │".to_owned(),
"┌────────────┐ ┌───────────┐ ┌───────────┐ │".to_owned(),
"│ boundaries │ │ spaghetti │ │ structure │ │".to_owned(),
"└──────┬─────┘ └─────┬─────┘ └─────┬─────┘ │".to_owned(),
" └──────────┬─────┴────┬──────────┴───────┘".to_owned(),
" ▼ ▼".to_owned(),
" ┌───────┐ ┌──────┐".to_owned(),
" │ apply │ │ none │".to_owned(),
" └───────┘ └──────┘".to_owned(),
]
);
}