Comine integration tests into one binary (#951)
1use roxmltree::Document;
2use std::{fmt::Debug, io::Write, path::PathBuf, str::FromStr};
3use taffy::{
4 prelude::TaffyZero as _, AvailableSpace, CheapCloneStr, Dimension, GridAutoTracks, GridTemplateComponent,
5 GridTemplateTracks, LengthPercentage, LengthPercentageAuto, Line, NodeId, Point, PrintTree, Rect, Size, TaffyTree,
6};
7use taffy_test_helpers::{test_measure_function, TestNodeContext};
8
9#[path = "./xml/mod.rs"]
10mod xml;
11
12#[derive(Debug)]
13struct OutputNode {
14 node_id: NodeId,
15 location: Point<f32>,
16 size: Size<f32>,
17 children: Vec<OutputNode>,
18}
19
20impl PartialEq for OutputNode {
21 fn eq(&self, other: &Self) -> bool {
22 self.node_id == other.node_id
23 && (self.location.x - other.location.x).abs() < 0.1
24 && (self.location.y - other.location.y).abs() < 0.1
25 && (self.size.width - other.size.width).abs() < 0.1
26 && (self.size.height - other.size.height).abs() < 0.1
27 && self.children == other.children
28 }
29}
30
31impl OutputNode {
32 fn write_metrics(&self, w: &mut impl Write) {
33 write!(
34 w,
35 "[x: {:<4} y: {:<4} w: {:<4} h: {:<4}]",
36 self.location.x, self.location.y, self.size.width, self.size.height
37 )
38 .unwrap();
39 }
40
41 /// Prints a debug representation of the computed layout for a tree of nodes
42 fn write_tree(&self, out: &mut impl Write) {
43 writeln!(out, "TREE").unwrap();
44 print_node(out, self, false, String::new());
45
46 /// Recursive function that prints each node in the tree
47 fn print_node(out: &mut impl Write, node: &OutputNode, has_sibling: bool, lines_string: String) {
48 let num_children = node.children.len();
49 let fork_string = if has_sibling { "├── " } else { "└── " };
50
51 write!(out, "{lines_string}{fork_string} {:?} ", node.node_id).unwrap();
52 node.write_metrics(out);
53 let bar = if has_sibling { "│ " } else { " " };
54 let new_string = lines_string + bar;
55 writeln!(out).unwrap();
56
57 // Recurse into children
58 for (index, child) in node.children.iter().enumerate() {
59 let has_sibling = index < num_children - 1;
60 print_node(out, child, has_sibling, new_string.clone());
61 }
62 }
63 }
64
65 fn print_tree(&self) {
66 let mut s: Vec<u8> = Vec::new();
67 self.write_tree(&mut s);
68 let s = String::from_utf8(s).unwrap();
69 println!("{s}");
70 }
71}
72
73#[test]
74fn get_dir() {
75 let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
76 let test_dir = root_dir.join("tests");
77 let xml_test_dir = test_dir.join("xml");
78 println!("{}", xml_test_dir.display())
79}
80
81fn run_xml_test(group: &str, name: &str) {
82 let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
83 let test_dir = root_dir.join("tests");
84 let xml_test_dir = test_dir.join("xml");
85
86 let test_xml_file = {
87 let mut path = xml_test_dir.join(group).join(name);
88 path.set_extension("xml");
89 path
90 };
91
92 let raw_xml = std::fs::read_to_string(&test_xml_file).expect("test file should exist");
93 let test = Document::parse(&raw_xml).unwrap();
94
95 let root = test.root_element();
96 let use_rounding: bool = parse_or(root.attribute("use-rounding"), true);
97 let viewport = root.children().find(|node| node.has_tag_name("viewport")).unwrap();
98 let input = root.children().find(|node| node.has_tag_name("input")).unwrap();
99 let expectations = root.children().find(|node| node.has_tag_name("expectations")).unwrap();
100
101 let available_space = Size {
102 width: parse_or(viewport.attribute("width"), AvailableSpace::MaxContent),
103 height: parse_or(viewport.attribute("height"), AvailableSpace::MaxContent),
104 };
105
106 // Construct tree and expectations
107 let mut tree = TaffyTree::<TestNodeContext>::new();
108 if use_rounding {
109 tree.enable_rounding();
110 } else {
111 tree.disable_rounding();
112 }
113 let expected_output = construct_tree(
114 input.first_element_child().unwrap(),
115 expectations.first_element_child().unwrap(),
116 &mut tree,
117 None,
118 );
119 let root_node_id = expected_output.node_id;
120
121 // Compute layout
122 tree.compute_layout_with_measure(root_node_id, available_space, test_measure_function).unwrap();
123 let actual_output = get_computed_expectations(&tree, root_node_id);
124
125 println!("\nINPUT");
126 println!("{raw_xml}");
127
128 tree.print_tree(root_node_id);
129
130 println!("\nEXPECTED");
131 expected_output.print_tree();
132 println!("\nACTUAL");
133 actual_output.print_tree();
134
135 assert_eq!(expected_output, actual_output);
136}
137
138fn construct_tree(
139 input: roxmltree::Node,
140 expected_x: roxmltree::Node,
141 tree: &mut TaffyTree<TestNodeContext>,
142 parent: Option<taffy::NodeId>,
143) -> OutputNode {
144 if input.first_element_child().is_some() {
145 let tnode = tree.new_with_children(build_style(input), &[]).unwrap();
146 let mut expected = build_expectations(expected_x, tnode);
147
148 if let Some(parent) = parent {
149 tree.add_child(parent, tnode).unwrap();
150 }
151
152 for (child, child_expected_x) in input
153 .children()
154 .filter(|node| node.is_element())
155 .zip(expected_x.children().filter(|node| node.is_element()))
156 {
157 expected.children.push(construct_tree(child, child_expected_x, tree, Some(tnode)));
158 }
159 expected
160 } else {
161 let text_content = input.text().map(|text| text.trim());
162 // let aspect_ratio = input.attribute("aspect-ratio");
163 let writing_mode = parse_or_default(input.attribute("writing-mode"));
164
165 let tnode = tree.new_leaf(build_style(input)).unwrap();
166 tree.set_node_context(
167 tnode,
168 text_content.map(|text_content| TestNodeContext::ahem_text(text_content.to_string(), writing_mode)),
169 )
170 .unwrap();
171
172 if let Some(parent) = parent {
173 tree.add_child(parent, tnode).unwrap();
174 }
175
176 build_expectations(expected_x, tnode)
177 }
178}
179
180fn parse_or_default<T: FromStr<Err: Debug> + Default>(input: Option<&str>) -> T {
181 parse_or(input, Default::default())
182}
183
184fn parse_or<T: FromStr<Err: Debug>>(input: Option<&str>, fallback: T) -> T {
185 input.map(|input| input.parse().unwrap()).unwrap_or(fallback)
186}
187
188fn maybe_parse<T: FromStr>(input: Option<&str>) -> Option<T> {
189 input.and_then(|input| input.parse().ok())
190}
191
192fn get_computed_expectations(tree: &TaffyTree<TestNodeContext>, node_id: NodeId) -> OutputNode {
193 let layout = tree.get_final_layout(node_id);
194 let mut output = OutputNode { node_id, location: layout.location, size: layout.size, children: Vec::new() };
195
196 for child_id in tree.children(node_id).unwrap() {
197 output.children.push(get_computed_expectations(tree, child_id));
198 }
199
200 output
201}
202
203fn build_expectations(xnode: roxmltree::Node, node_id: NodeId) -> OutputNode {
204 OutputNode {
205 node_id,
206 location: Point {
207 x: xnode.attribute("x").unwrap().parse().unwrap(),
208 y: xnode.attribute("y").unwrap().parse().unwrap(),
209 },
210 size: Size {
211 width: xnode.attribute("width").unwrap().parse().unwrap(),
212 height: xnode.attribute("height").unwrap().parse().unwrap(),
213 },
214 children: Vec::new(),
215 }
216}
217
218fn build_style<S: CheapCloneStr>(xnode: roxmltree::Node) -> taffy::Style<S> {
219 let grid_template_rows: GridTemplateTracks<S, GridTemplateComponent<S>> =
220 parse_or_default(xnode.attribute("grid-template-rows"));
221 let grid_template_columns: GridTemplateTracks<S, GridTemplateComponent<S>> =
222 parse_or_default(xnode.attribute("grid-template-columns"));
223
224 taffy::Style {
225 dummy: std::marker::PhantomData,
226 display: parse_or_default(xnode.attribute("display")),
227 direction: parse_or_default(xnode.attribute("direction")),
228 item_is_table: false,
229 item_is_replaced: false,
230 box_sizing: parse_or_default(xnode.attribute("box-sizing")),
231 overflow: Point {
232 x: parse_or_default(xnode.attribute("overflow-x")),
233 y: parse_or_default(xnode.attribute("overflow-y")),
234 },
235 scrollbar_width: parse_or_default(xnode.attribute("scrollbar-width")),
236 float: parse_or_default(xnode.attribute("float")),
237 clear: parse_or_default(xnode.attribute("clear")),
238 position: parse_or_default(xnode.attribute("position")),
239
240 size: Size {
241 width: parse_or(xnode.attribute("width"), Dimension::auto()),
242 height: parse_or(xnode.attribute("height"), Dimension::auto()),
243 },
244 min_size: Size {
245 width: parse_or(xnode.attribute("min-width"), Dimension::auto()),
246 height: parse_or(xnode.attribute("min-height"), Dimension::auto()),
247 },
248 max_size: Size {
249 width: parse_or(xnode.attribute("max-width"), Dimension::auto()),
250 height: parse_or(xnode.attribute("max-height"), Dimension::auto()),
251 },
252 inset: Rect {
253 top: parse_or(xnode.attribute("top"), LengthPercentageAuto::auto()),
254 left: parse_or(xnode.attribute("left"), LengthPercentageAuto::auto()),
255 bottom: parse_or(xnode.attribute("bottom"), LengthPercentageAuto::auto()),
256 right: parse_or(xnode.attribute("right"), LengthPercentageAuto::auto()),
257 },
258 margin: Rect {
259 top: parse_or(xnode.attribute("margin-top"), LengthPercentageAuto::ZERO),
260 left: parse_or(xnode.attribute("margin-left"), LengthPercentageAuto::ZERO),
261 bottom: parse_or(xnode.attribute("margin-bottom"), LengthPercentageAuto::ZERO),
262 right: parse_or(xnode.attribute("margin-right"), LengthPercentageAuto::ZERO),
263 },
264 padding: Rect {
265 top: parse_or(xnode.attribute("padding-top"), LengthPercentage::ZERO),
266 left: parse_or(xnode.attribute("padding-left"), LengthPercentage::ZERO),
267 bottom: parse_or(xnode.attribute("padding-bottom"), LengthPercentage::ZERO),
268 right: parse_or(xnode.attribute("padding-right"), LengthPercentage::ZERO),
269 },
270 border: Rect {
271 top: parse_or(xnode.attribute("border-top"), LengthPercentage::ZERO),
272 left: parse_or(xnode.attribute("border-left"), LengthPercentage::ZERO),
273 bottom: parse_or(xnode.attribute("border-bottom"), LengthPercentage::ZERO),
274 right: parse_or(xnode.attribute("border-right"), LengthPercentage::ZERO),
275 },
276 gap: Size {
277 width: parse_or(xnode.attribute("column-gap"), LengthPercentage::ZERO),
278 height: parse_or(xnode.attribute("row-gap"), LengthPercentage::ZERO),
279 },
280
281 aspect_ratio: maybe_parse(xnode.attribute("aspect-ratio")),
282 align_items: maybe_parse(xnode.attribute("align-items")),
283 align_self: maybe_parse(xnode.attribute("align-self")),
284 justify_items: maybe_parse(xnode.attribute("justify-items")),
285 justify_self: maybe_parse(xnode.attribute("justify-self")),
286 align_content: maybe_parse(xnode.attribute("align-content")),
287 justify_content: maybe_parse(xnode.attribute("justify-content")),
288
289 text_align: parse_or_default(xnode.attribute("text-align")),
290 flex_direction: parse_or_default(xnode.attribute("flex-direction")),
291 flex_wrap: parse_or_default(xnode.attribute("flex-wrap")),
292 flex_grow: parse_or(xnode.attribute("flex-grow"), 0.0),
293 flex_shrink: parse_or(xnode.attribute("flex-shrink"), 1.0),
294 flex_basis: parse_or(xnode.attribute("flex-basis"), Dimension::auto()),
295
296 grid_auto_flow: parse_or_default(xnode.attribute("grid-auto-flow")),
297
298 grid_template_rows: grid_template_rows.tracks,
299 grid_template_row_names: grid_template_rows.line_names,
300
301 grid_template_columns: grid_template_columns.tracks,
302 grid_template_column_names: grid_template_columns.line_names,
303
304 // TODO
305 grid_auto_rows: parse_or_default::<GridAutoTracks>(xnode.attribute("grid-auto-rows")).0,
306 grid_auto_columns: parse_or_default::<GridAutoTracks>(xnode.attribute("grid-auto-columns")).0,
307 grid_template_areas: Default::default(),
308
309 grid_row: Line {
310 start: parse_or_default(xnode.attribute("grid-row-start")),
311 end: parse_or_default(xnode.attribute("grid-row-end")),
312 },
313 grid_column: Line {
314 start: parse_or_default(xnode.attribute("grid-column-start")),
315 end: parse_or_default(xnode.attribute("grid-column-end")),
316 },
317 }
318}