gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 684HTML 640Markdown 12TOML 62286 Definiciones

⎇ main scripts/gentest/src/main.rs Rust · 535 líneas · 21.6 KB Historia Quién tocó qué crudo

Block: return margin-collapsing outputs from vertical axis `ComputeSize` calls (#976) b9067e743 · Nico Burns ·

1use std::borrow::Cow;
2use std::ffi::{OsStr, OsString};
3use std::fmt::Display;
4use std::fs::{self, OpenOptions};
5use std::io::Write as _;
6use std::path::{Component, Path, PathBuf};
7use std::process::Command;
8
9use fantoccini::{Client, ClientBuilder};
10use log::*;
11use serde_json::Value;
12use walkdir::WalkDir;
13use xmlwriter::{Indent, Options, XmlWriter};
14
15#[tokio::main]
16async fn main() {
17    env_logger::init();
18    let root_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
19    let repo_root = root_dir.parent().and_then(Path::parent).unwrap();
20
21    let fixtures_root = repo_root.join("test_fixtures");
22
23    info!("reading test fixtures from disk");
24    let mut fixtures: Vec<_> = WalkDir::new(fixtures_root.clone())
25        .into_iter()
26        .filter_map(|a| a.ok())
27        .filter(|f| !f.path().components().any(|c| c.as_os_str() == OsStr::new("_scratch")))
28        .filter(|f| !f.file_name().to_string_lossy().starts_with('x')) // ignore tests beginning with x
29        .filter(|f| f.path().is_file() && f.path().extension().map(|p| p == "html").unwrap_or(false))
30        .map(|f| {
31            let fixture_path = f.path().to_path_buf();
32            let name = fixture_path.file_stem().unwrap().to_str().unwrap().to_string();
33            (name, fixture_path)
34        })
35        .collect();
36    fixtures.sort_unstable_by_key(|f| f.1.clone());
37
38    info!("starting webdriver instance");
39    let webdriver_url = "http://localhost:4444";
40    let mut webdriver_handle = Command::new("chromedriver")
41        .arg("--port=4444")
42        .spawn()
43        .expect("ChromeDriver not found: Make sure you have it installed and added to your PATH.");
44
45    // this is silly, but it works
46    std::thread::sleep(std::time::Duration::from_secs(1));
47
48    let mut caps = serde_json::map::Map::new();
49    let chrome_opts = serde_json::json!({ "args": ["--headless", "--disable-gpu"] });
50    caps.insert("goog:chromeOptions".to_string(), chrome_opts.clone());
51
52    info!("spawning webdriver client and collecting test descriptions");
53    let client = ClientBuilder::native().capabilities(caps.clone()).connect(webdriver_url).await.unwrap();
54
55    asserts_non_zero_width_scrollbars(client.clone()).await;
56
57    let mut test_descs = vec![];
58    for (name, fixture_path) in fixtures.iter() {
59        test_descs.push(test_root_element(client.clone(), name.clone(), fixture_path).await);
60    }
61
62    info!("killing webdriver instance...");
63    webdriver_handle.kill().unwrap();
64    webdriver_handle.wait().unwrap();
65
66    info!("generating test sources and concatenating...");
67
68    let test_descs: Vec<_> = test_descs
69        .iter()
70        .flat_map(|(name, fixture_path, description)| {
71            debug!("generating test contents for {}", name);
72
73            let border_box_ltr_test =
74                generate_test(format!("{name}__border_box_ltr"), &description["borderBoxLtrData"]);
75            let content_box_ltr_test =
76                generate_test(format!("{name}__content_box_ltr"), &description["contentBoxLtrData"]);
77            let border_box_rtl_test =
78                generate_test(format!("{name}__border_box_rtl"), &description["borderBoxRtlData"]);
79            let content_box_rtl_test =
80                generate_test(format!("{name}__content_box_rtl"), &description["contentBoxRtlData"]);
81
82            [
83                (format!("{name}__border_box_ltr"), fixture_path, border_box_ltr_test),
84                (format!("{name}__content_box_ltr"), fixture_path, content_box_ltr_test),
85                (format!("{name}__border_box_rtl"), fixture_path, border_box_rtl_test),
86                (format!("{name}__content_box_rtl"), fixture_path, content_box_rtl_test),
87            ]
88        })
89        .collect();
90
91    info!("writing generated test file to disk...");
92    let tests_base_path = repo_root.join("tests");
93    let xml_base_path = tests_base_path.join("xml");
94    let _ = fs::remove_dir_all(&xml_base_path);
95    fs::create_dir(&xml_base_path).unwrap();
96
97    let mut mod_file = OpenOptions::new().create(true).append(true).open(xml_base_path.join("mod.rs")).unwrap();
98    writeln!(&mut mod_file, "//! Generated XML tests").unwrap();
99    writeln!(&mut mod_file, "#![allow(non_snake_case)]").unwrap();
100
101    let mut current_module: Option<OsString> = None;
102
103    for (name, fixture_path, test_body) in test_descs {
104        // Create test directory if it doesn't exist
105        let test_path_stripped = fixture_path.parent().unwrap().strip_prefix(&fixtures_root).unwrap();
106        let test_path = xml_base_path.join(test_path_stripped);
107        if !test_path.exists() {
108            fs::create_dir(&test_path).unwrap();
109        }
110
111        let Some(Component::Normal(module)) = test_path_stripped.components().next() else {
112            panic!("unexpected module name")
113        };
114
115        if current_module.as_deref() != Some(module) {
116            if current_module.is_some() {
117                writeln!(&mut mod_file, "}}\n").unwrap();
118            }
119            current_module = Some(module.to_owned());
120            writeln!(&mut mod_file, "mod {} {{", module.display()).unwrap();
121        }
122
123        if name.starts_with("grid") {
124            writeln!(&mut mod_file, r#"#[cfg(feature = "grid")]"#).unwrap();
125        }
126        writeln!(
127            &mut mod_file,
128            "#[test]
129            fn {name} () {{
130                crate::run_xml_test(\"{}\", \"{name}\");
131            }}
132        ",
133            module.display()
134        )
135        .unwrap();
136
137        let mut test_filename = test_path.join(&name);
138        test_filename.set_extension("xml");
139        debug!("writing {} to disk...", name);
140        fs::write(test_filename, test_body).unwrap();
141    }
142
143    if current_module.is_some() {
144        writeln!(&mut mod_file, "}}\n").unwrap();
145    }
146
147    info!("formatting the source directory");
148    Command::new("cargo").arg("fmt").current_dir(repo_root).status().unwrap();
149}
150
151async fn asserts_non_zero_width_scrollbars(client: Client) {
152    // Load minimal test page defined in the string
153    const TEST_PAGE: &str = r#"data:text/html;charset=utf-8,<html><body><div style="overflow:scroll" /></body></html>"#;
154    client.goto(TEST_PAGE).await.unwrap();
155
156    // Determine the width of the scrollbar
157    let scrollbar_width = client
158        .execute("return document.body.firstChild.clientWidth - document.body.firstChild.offsetWidth;", vec![])
159        .await
160        .unwrap();
161    let Value::Number(scrollbar_width) = scrollbar_width else { panic!("Error retrieving scrollbar_width") };
162    let scrollbar_width = scrollbar_width.as_f64().unwrap();
163
164    if scrollbar_width == 0.0 {
165        panic!(concat!(
166            "\n\n    Error: Scrollbar width of zero detected. This test generation script must be run with scrollbars set to take up space.\n",
167            "    On macOS this can be done by setting Show Scrollbars to 'always' in the Appearance section of the System Settings app.\n\n"
168        ))
169    }
170}
171
172async fn test_root_element(client: Client, name: String, fixture_path: impl AsRef<Path>) -> (String, PathBuf, Value) {
173    let fixture_path = fixture_path.as_ref();
174
175    let url = format!("file://{}", fixture_path.display());
176
177    client.goto(&url).await.unwrap();
178    let description = client.execute("return getTestData()", vec![]).await.unwrap();
179    let description_string = description.as_str().unwrap();
180    let description = serde_json::from_str(description_string).unwrap();
181    (name.to_string(), fixture_path.to_path_buf(), description)
182}
183
184fn generate_test(name: impl AsRef<str>, description: &Value) -> String {
185    let use_rounding = description["useRounding"].as_bool().unwrap();
186
187    let mut w =
188        XmlWriter::new(Options { use_single_quote: false, indent: Indent::Spaces(2), attributes_indent: Indent::None });
189    w.start_element("test");
190    w.write_attribute("name", name.as_ref());
191    w.write_attribute("use-rounding", &use_rounding);
192
193    // Viewport
194    let viewport = &description["viewport"];
195    w.start_element("viewport");
196    w.write_attribute("width", &serialize_dimension(&viewport["width"]).unwrap());
197    w.write_attribute("height", &serialize_dimension(&viewport["height"]).unwrap());
198    w.end_element();
199
200    // Input styles
201    w.start_element("input");
202    generate_node(&mut w, description);
203    w.end_element();
204
205    // Expectations
206    w.start_element("expectations");
207    let use_rounding = description["useRounding"].as_bool().unwrap();
208    generate_assertions(&mut w, description, use_rounding);
209    w.end_element();
210
211    w.end_document()
212}
213
214fn generate_assertions(w: &mut XmlWriter, node: &Value, use_rounding: bool) {
215    let layout = if use_rounding { &node["smartRoundedLayout"] } else { &node["unroundedLayout"] };
216
217    let read_f32 = |s: &str| layout[s].as_f64().unwrap() as f32;
218    let read_naive_f32 = |s: &str| node["naivelyRoundedLayout"][s].as_f64().unwrap() as f32;
219    let scroll_width = (read_f32("scrollWidth") - read_naive_f32("clientWidth")).max(0.0);
220    let scroll_height = (read_f32("scrollHeight") - read_naive_f32("clientHeight")).max(0.0);
221
222    fn is_scrollable(overflow: &Value) -> bool {
223        match overflow {
224            Value::String(ref value) => matches!(value.as_ref(), "hidden" | "scroll" | "auto"),
225            _ => false,
226        }
227    }
228    let is_scroll_container = is_scrollable(&node["style"]["overflowX"]) || is_scrollable(&node["style"]["overflowY"]);
229
230    w.start_element("node");
231
232    w.write_attribute("x", &read_f32("x"));
233    w.write_attribute("y", &read_f32("y"));
234    w.write_attribute("width", &read_f32("width"));
235    w.write_attribute("height", &read_f32("height"));
236
237    if is_scroll_container {
238        w.write_attribute("scroll_width", &scroll_width);
239        w.write_attribute("scroll_height", &scroll_height);
240    }
241
242    if let Value::Array(ref value) = node["children"] {
243        for child in value {
244            generate_assertions(w, child, use_rounding);
245        }
246    };
247
248    w.end_element();
249}
250
251fn generate_node(w: &mut XmlWriter, node: &Value) {
252    let style = &node["style"];
253
254    fn get_string_value(value: &Value) -> Option<&str> {
255        match value {
256            Value::String(ref value) => Some(value),
257            _ => None,
258        }
259    }
260
261    // Handle text/leaf node case
262    let text_content = get_string_value(&node["textContent"]);
263    if text_content.is_some() {
264        w.start_element("text");
265    } else {
266        w.start_element("div");
267    }
268
269    fn maybe_write<T: Display>(w: &mut XmlWriter, name: &str, value: Option<T>) {
270        if let Some(attr) = value {
271            w.write_attribute(name, &attr);
272        }
273    }
274
275    maybe_write(w, "display", get_str_attr(&style["display"], None));
276    maybe_write(w, "box-sizing", get_str_attr(&style["boxSizing"], Some("border-box")));
277    maybe_write(w, "direction", get_str_attr(&style["direction"], None));
278    maybe_write(w, "writing-mode", get_str_attr(&style["writingMode"], None));
279    maybe_write(w, "position", get_str_attr(&style["position"], Some("relative")));
280    maybe_write(w, "float", get_str_attr(&style["cssFloat"], None));
281    maybe_write(w, "clear", get_str_attr(&style["clear"], None));
282    maybe_write(w, "flex-direction", get_str_attr(&style["flexDirection"], Some("row")));
283    maybe_write(w, "flex-wrap", get_str_attr(&style["flexWrap"], Some("nowrap")));
284    maybe_write(w, "overflow-x", get_str_attr(&style["overflowX"], Some("visible")));
285    maybe_write(w, "overflow-y", get_str_attr(&style["overflowY"], Some("visible")));
286
287    let overflow_x = get_str_attr(&style["overflowX"], Some("visible"));
288    let overflow_y = get_str_attr(&style["overflowY"], Some("visible"));
289    if overflow_x.is_some() || overflow_y.is_some() {
290        maybe_write(w, "scrollbar-width", get_num_attr(&style["scrollbarWidth"], None));
291    }
292
293    maybe_write(w, "text-align", get_str_attr(&style["textAlign"], None));
294    maybe_write(w, "align-items", get_str_attr(&style["alignItems"], None));
295    maybe_write(w, "align-self", get_str_attr(&style["alignSelf"], None));
296    maybe_write(w, "justify-items", get_str_attr(&style["justifyItems"], None));
297    maybe_write(w, "justify-self", get_str_attr(&style["justifySelf"], None));
298    maybe_write(w, "align-content", get_str_attr(&style["alignContent"], None));
299    maybe_write(w, "justify-content", get_str_attr(&style["justifyContent"], None));
300
301    maybe_write(w, "flex-grow", get_num_attr(&style["flexGrow"], Some(0.0)));
302    maybe_write(w, "flex-shrink", get_num_attr(&style["flexShrink"], Some(1.0)));
303    maybe_write(w, "flex-basis", get_dim_attr(&style["flexBasis"], Some("auto")));
304
305    maybe_write(w, "width", get_dim_attr(&style["size"]["width"], Some("auto")));
306    maybe_write(w, "height", get_dim_attr(&style["size"]["height"], Some("auto")));
307    maybe_write(w, "min-width", get_dim_attr(&style["minSize"]["width"], Some("auto")));
308    maybe_write(w, "min-height", get_dim_attr(&style["minSize"]["height"], Some("auto")));
309    maybe_write(w, "max-width", get_dim_attr(&style["maxSize"]["width"], Some("auto")));
310    maybe_write(w, "max-height", get_dim_attr(&style["maxSize"]["height"], Some("auto")));
311
312    maybe_write(w, "aspect-ratio", get_num_attr(&style["aspectRatio"], None));
313
314    // TODO: null check in no gap case
315    maybe_write(w, "row-gap", get_dim_attr(&style["gap"]["row"], None));
316    maybe_write(w, "column-gap", get_dim_attr(&style["gap"]["column"], None));
317
318    maybe_write(w, "margin-top", get_dim_attr(&style["margin"]["top"], None));
319    maybe_write(w, "margin-left", get_dim_attr(&style["margin"]["left"], None));
320    maybe_write(w, "margin-bottom", get_dim_attr(&style["margin"]["bottom"], None));
321    maybe_write(w, "margin-right", get_dim_attr(&style["margin"]["right"], None));
322
323    maybe_write(w, "padding-top", get_dim_attr(&style["padding"]["top"], None));
324    maybe_write(w, "padding-left", get_dim_attr(&style["padding"]["left"], None));
325    maybe_write(w, "padding-bottom", get_dim_attr(&style["padding"]["bottom"], None));
326    maybe_write(w, "padding-right", get_dim_attr(&style["padding"]["right"], None));
327
328    maybe_write(w, "border-top", get_dim_attr(&style["border"]["top"], None));
329    maybe_write(w, "border-left", get_dim_attr(&style["border"]["left"], None));
330    maybe_write(w, "border-bottom", get_dim_attr(&style["border"]["bottom"], None));
331    maybe_write(w, "border-right", get_dim_attr(&style["border"]["right"], None));
332
333    maybe_write(w, "top", get_dim_attr(&style["inset"]["top"], None));
334    maybe_write(w, "left", get_dim_attr(&style["inset"]["left"], None));
335    maybe_write(w, "bottom", get_dim_attr(&style["inset"]["bottom"], None));
336    maybe_write(w, "right", get_dim_attr(&style["inset"]["right"], None));
337
338    maybe_write(w, "grid-auto-flow", serialize_grid_auto_flow(&style["gridAutoFlow"]));
339    maybe_write(w, "grid-template-rows", serialize_array(&style["gridTemplateRows"], ' ', serialize_track_definition));
340    maybe_write(
341        w,
342        "grid-template-columns",
343        serialize_array(&style["gridTemplateColumns"], ' ', serialize_track_definition),
344    );
345    maybe_write(w, "grid-auto-rows", serialize_array(&style["gridAutoRows"], ' ', serialize_track_definition));
346    maybe_write(w, "grid-auto-columns", serialize_array(&style["gridAutoColumns"], ' ', serialize_track_definition));
347
348    maybe_write(w, "grid-row-start", serialize_grid_position(&style["gridRowStart"]));
349    maybe_write(w, "grid-row-end", serialize_grid_position(&style["gridRowEnd"]));
350    maybe_write(w, "grid-column-start", serialize_grid_position(&style["gridColumnStart"]));
351    maybe_write(w, "grid-column-end", serialize_grid_position(&style["gridColumnEnd"]));
352
353    // Recurse into children
354    if let Value::Array(ref value) = node["children"] {
355        for child_desc in value {
356            generate_node(w, child_desc);
357        }
358    };
359
360    if let Some(text_content) = text_content {
361        w.write_text(text_content.trim());
362    }
363
364    w.end_element();
365}
366
367fn get_str_attr<'a>(value: &'a Value, elide_if: Option<&str>) -> Option<&'a str> {
368    if let Value::String(ref value) = value {
369        if Some(value.as_str()) != elide_if {
370            return Some(value.as_str());
371        }
372    }
373
374    None
375}
376
377fn get_num_attr(value: &Value, elide_if: Option<f64>) -> Option<f64> {
378    if let Value::Number(ref value) = value {
379        if let Some(num) = value.as_f64() {
380            if Some(num) != elide_if {
381                return Some(num);
382            }
383        }
384    }
385
386    None
387}
388
389fn get_dim_attr(value: &Value, elide_if: Option<&str>) -> Option<Cow<'static, str>> {
390    if let Some(attr) = serialize_dimension(value) {
391        if Some(attr.as_ref()) != elide_if {
392            return Some(attr);
393        }
394    }
395    None
396}
397
398fn serialize_dimension(obj: &serde_json::Value) -> Option<Cow<'static, str>> {
399    if let Value::Object(ref dimen) = &obj {
400        let unit = dimen.get("unit").unwrap();
401        let value = dimen.get("value").and_then(|v| v.as_f64());
402        match unit {
403            Value::String(ref unit) => {
404                return Some(match unit.as_str() {
405                    "auto" => Cow::from("auto"),
406                    "max-content" => Cow::from("max-content"),
407                    "min-content" => Cow::from("min-content"),
408                    "px" => Cow::from(format!("{}px", value.expect("Expected value for px unit"))),
409                    "percent" => Cow::from(format!("{}%", value.expect("Expected value for % unit") * 100.0)),
410                    "fraction" => Cow::from(format!("{}fr", value.expect("Expected value for fr unit"))),
411                    _ => unreachable!(),
412                })
413            }
414            _ => panic!("Tried to serialize dimension object, but unit was not a string"),
415        }
416    };
417
418    None
419}
420
421fn serialize_grid_auto_flow(obj: &serde_json::Value) -> Option<Cow<'static, str>> {
422    if let Value::Object(ref auto_flow) = &obj {
423        let direction = auto_flow.get("direction").unwrap().as_str().unwrap();
424        let algorithm = auto_flow.get("algorithm").unwrap().as_str().unwrap();
425
426        let value = match (direction, algorithm) {
427            ("row", "sparse") => "row",
428            ("column", "sparse") => "column",
429            ("row", "dense") => "row dense",
430            ("column", "dense") => "column dense",
431            _ => unreachable!(),
432        };
433
434        return Some(Cow::from(value));
435    }
436    None
437}
438
439fn serialize_grid_position(grid_position: &serde_json::Value) -> Option<Cow<'static, str>> {
440    if let Value::Object(ref grid_position) = &grid_position {
441        let kind = grid_position.get("kind").unwrap();
442        let value = || grid_position.get("value").unwrap().as_f64().unwrap() as f32;
443
444        return match kind {
445            Value::String(ref kind) => match kind.as_ref() {
446                "auto" => None, //Some(Cow::from("auto")),
447                "span" => Some(Cow::from(format!("span {}", value()))),
448                "line" => Some(Cow::from((value() as i32).to_string())),
449                _ => unreachable!(),
450            },
451            _ => unreachable!(),
452        };
453    }
454    None
455}
456
457fn serialize_array(value: &Value, sep: char, quoter: impl Fn(&Value) -> Option<Cow<'static, str>>) -> Option<String> {
458    match &value {
459        Value::Array(ref values) => serialize_value_list(values, sep, quoter),
460        _ => None,
461    }
462}
463
464fn serialize_value_list(
465    values: &[Value],
466    sep: char,
467    quoter: impl Fn(&Value) -> Option<Cow<'static, str>>,
468) -> Option<String> {
469    let mut out = String::new();
470    for item in values {
471        out.push_str(quoter(item)?.as_ref());
472        out.push(sep);
473    }
474    out.pop();
475    Some(out)
476}
477
478fn serialize_track_definition(track_definition: &serde_json::Value) -> Option<Cow<'static, str>> {
479    let serde_json::Value::Object(map) = track_definition else {
480        return None;
481    };
482
483    let kind = map.get("kind").unwrap().as_str().unwrap();
484    let name = || map.get("name").unwrap().as_str().unwrap();
485    let arguments = || map.get("arguments").unwrap();
486
487    match kind {
488        "scalar" => serialize_dimension(track_definition),
489        "function" => match (name(), arguments()) {
490            ("fit-content", Value::Array(arguments)) => {
491                if arguments.len() != 1 {
492                    panic!("fit-content function with the wrong number of arguments");
493                }
494                let limit = serialize_dimension(&arguments[0])?;
495                Some(Cow::from(format!("fit-content({limit})")))
496            }
497            ("minmax", Value::Array(arguments)) => {
498                if arguments.len() != 2 {
499                    panic!("minmax function with the wrong number of arguments");
500                }
501                let min = serialize_dimension(&arguments[0])?;
502                let max = serialize_dimension(&arguments[1])?;
503
504                Some(Cow::from(format!("minmax({min},{max})")))
505            }
506            ("repeat", Value::Array(arguments)) => {
507                if arguments.len() < 2 {
508                    panic!("repeat function with the wrong number of arguments");
509                }
510                let repetition = match arguments[0] {
511                    Value::Object(ref arg) => {
512                        let unit = arg.get("unit").unwrap().as_str().unwrap();
513                        let value = || arg.get("value").unwrap().as_u64().unwrap() as u16;
514
515                        match unit {
516                            "auto-fill" => Cow::from("auto-fill"),
517                            "auto-fit" => Cow::from("auto-fit"),
518                            "integer" => {
519                                let repetition_count = value();
520                                Cow::from(repetition_count.to_string())
521                            }
522                            _ => unreachable!(),
523                        }
524                    }
525                    _ => unreachable!(),
526                };
527                let track_list = serialize_value_list(&arguments[1..], ' ', serialize_track_definition)?;
528                Some(Cow::from(format!("repeat({repetition}, {track_list})")))
529            }
530            // TODO: Add support for fit-content
531            _ => unreachable!(),
532        },
533        _ => unreachable!(),
534    }
535}