From 64fabe616f6c12a950d8fa6fae37e595364587c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABlle=20Huisman?= Date: Mon, 9 Sep 2024 19:57:36 +0200 Subject: [PATCH] Fix DOM element filter and pretty-format --- Cargo.lock | 7 ++ packages/dom/Cargo.toml | 5 +- packages/dom/src/dom_element_filter.rs | 118 ++++++++++++++++++------ packages/dom/src/pretty_dom.rs | 22 ++++- packages/dom/src/util.rs | 38 +++++--- packages/dom/tests/element_queries.rs | 123 +++++++++++++------------ packages/dom/tests/pretty_dom.rs | 24 +++++ packages/pretty-format/src/lib.rs | 93 ++++++++++++++++--- packages/pretty-format/src/types.rs | 58 +++++++----- 9 files changed, 351 insertions(+), 137 deletions(-) create mode 100644 packages/dom/tests/pretty_dom.rs diff --git a/Cargo.lock b/Cargo.lock index 635d564..e4724d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "js-sys" version = "0.3.70" @@ -260,6 +266,7 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" name = "testing-library-dom" version = "0.0.1" dependencies = [ + "indoc", "log", "mockall", "paste", diff --git a/packages/dom/Cargo.toml b/packages/dom/Cargo.toml index 5fd77a7..ca95a20 100644 --- a/packages/dom/Cargo.toml +++ b/packages/dom/Cargo.toml @@ -17,6 +17,7 @@ regex.workspace = true thiserror.workspace = true wasm-bindgen.workspace = true web-sys = { workspace = true, features = [ + "Attr", "Comment", "Document", "DocumentFragment", @@ -27,11 +28,13 @@ web-sys = { workspace = true, features = [ "HtmlOptionElement", "HtmlOptionsCollection", "HtmlSelectElement", + "NamedNodeMap", "NodeList", "Text", "Window", ] } [dev-dependencies] -wasm-bindgen-test.workspace = true +indoc = "2.0.5" mockall = "0.13.0" +wasm-bindgen-test.workspace = true diff --git a/packages/dom/src/dom_element_filter.rs b/packages/dom/src/dom_element_filter.rs index 8825765..72338ea 100644 --- a/packages/dom/src/dom_element_filter.rs +++ b/packages/dom/src/dom_element_filter.rs @@ -1,39 +1,88 @@ +use std::collections::HashMap; + use pretty_format::{Config, Plugin, Printer, Refs}; use regex::Regex; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{Comment, Element, Node, Text}; +use crate::util::{named_node_map_to_hashmap, node_list_to_vec}; + fn escape_html(text: String) -> String { text.replace('<', "<").replace('>', ">") } fn print_props( - _config: &Config, - _indentation: String, - _depth: usize, - _refs: &Refs, - _printer: &Printer, + attributes: HashMap, + config: &Config, + indentation: String, + depth: usize, + refs: Refs, + printer: &Printer, ) -> String { - todo!() + let indentation_next = format!("{}{}", indentation, config.indent); + + attributes + .into_iter() + .map(|(key, value)| { + let printed = printer( + &JsValue::from_str(&value), + config, + indentation_next.clone(), + depth, + refs.clone(), + None, + ); + + format!( + "{}{}{}={}", + config.spacing_inner, + indentation, + config.colors.prop.paint(&key), + config.colors.value.paint(&printed) + ) + }) + .collect::>() + .join("") } fn print_children( - _config: &Config, - _indentation: String, - _depth: usize, - _refs: &Refs, - _printer: &Printer, + children: Vec, + config: &Config, + indentation: String, + depth: usize, + refs: Refs, + printer: &Printer, ) -> String { - todo!() + children + .into_iter() + .map(|child| { + let printed_child = printer( + child.unchecked_ref::(), + config, + indentation.clone(), + depth, + refs.clone(), + None, + ); + + if printed_child.is_empty() && child.node_type() != Node::TEXT_NODE { + // A plugin serialized this Node to '' meaning we should ignore it. + "".into() + } else { + format!("{}{}{}", config.spacing_outer, indentation, printed_child) + } + }) + .collect::>() + .join("") } fn print_text(text: String, config: &Config) -> String { - let content_color = config.colors.content; + let content_color = &config.colors.content; content_color.paint(&escape_html(text)) } fn print_comment(text: String, config: &Config) -> String { - let comment_color = config.colors.comment; + let comment_color = &config.colors.comment; comment_color.paint(&format!("", escape_html(text))) } @@ -41,10 +90,10 @@ fn print_element( r#type: String, printed_props: String, printed_children: String, - config: Config, + config: &Config, indentation: String, ) -> String { - let tag_color = config.colors.tag; + let tag_color = &config.colors.tag; tag_color.paint(&format!( "<{}{}{}>", @@ -82,7 +131,7 @@ fn print_element( } fn print_element_as_leaf(r#type: String, config: &Config) -> String { - let tag_color = config.colors.tag; + let tag_color = &config.colors.tag; format!( "{} …{}", tag_color.paint(&format!("<{}", r#type)), @@ -123,11 +172,13 @@ fn node_is_fragment(node: &Node) -> bool { node.node_type() == Node::DOCUMENT_FRAGMENT_NODE } -pub struct DomElementFilter {} +pub struct DomElementFilter { + filter_node: Box bool>, +} impl DomElementFilter { - pub fn new() -> Self { - Self {} + pub fn new(filter_node: Box bool>) -> Self { + Self { filter_node } } } @@ -139,7 +190,7 @@ impl Plugin for DomElementFilter { fn serialize( &self, val: &JsValue, - config: Config, + config: &Config, indentation: String, depth: usize, refs: Refs, @@ -148,11 +199,11 @@ impl Plugin for DomElementFilter { let node: &Node = val.unchecked_ref(); if node_is_text(node) { - return print_text(node.unchecked_ref::().data(), &config); + return print_text(node.unchecked_ref::().data(), config); } if node_is_comment(node) { - return print_comment(node.unchecked_ref::().data(), &config); + return print_comment(node.unchecked_ref::().data(), config); } let r#type = if node_is_fragment(node) { @@ -163,25 +214,32 @@ impl Plugin for DomElementFilter { let depth = depth + 1; if depth > config.max_depth { - return print_element_as_leaf(r#type, &config); + return print_element_as_leaf(r#type, config); } print_element( r#type, print_props( - // TODO: props, - &config, + if node_is_fragment(node) { + HashMap::new() + } else { + named_node_map_to_hashmap(node.unchecked_ref::().attributes()) + }, + config, format!("{}{}", indentation, &config.indent), depth, - &refs, + refs.clone(), printer, ), print_children( - // TODO: children, - &config, + node_list_to_vec(node.child_nodes()) + .into_iter() + .filter(&self.filter_node) + .collect(), + config, format!("{}{}", indentation, &config.indent), depth, - &refs, + refs.clone(), printer, ), config, diff --git a/packages/dom/src/pretty_dom.rs b/packages/dom/src/pretty_dom.rs index ad56485..a54d2b1 100644 --- a/packages/dom/src/pretty_dom.rs +++ b/packages/dom/src/pretty_dom.rs @@ -2,9 +2,9 @@ use std::rc::Rc; use pretty_format::PrettyFormatOptions; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{Document, Element}; +use web_sys::{Document, Element, Node}; -use crate::{dom_element_filter::DomElementFilter, helpers::get_document}; +use crate::{dom_element_filter::DomElementFilter, get_config, helpers::get_document}; pub enum DocumentOrElement { Document(Document), @@ -25,7 +25,18 @@ impl From for DocumentOrElement { fn should_highlight() -> bool { // TODO - true + + // Don't colorize in non-node environments (e.g. browsers). + false +} + +fn filter_comments_and_default_ignore_tags_tags(node: &Node) -> bool { + node.node_type() != Node::COMMENT_NODE + && (node.node_type() != Node::ELEMENT_NODE + || !node + .unchecked_ref::() + .matches(&get_config().default_ignore) + .unwrap_or(false)) } pub fn pretty_dom(dom: Option, max_length: Option) -> String { @@ -44,11 +55,14 @@ pub fn pretty_dom(dom: Option, max_length: Option) -> DocumentOrElement::Element(element) => element.unchecked_into(), }; + // TODO: accept as option + let filter_node = filter_comments_and_default_ignore_tags_tags; + let debug_content = pretty_format::format( &dom, // TODO: pass options PrettyFormatOptions::default() - .plugins(vec![Rc::new(DomElementFilter::new())]) + .plugins(vec![Rc::new(DomElementFilter::new(Box::new(filter_node)))]) .print_function_name(false) .highlight(should_highlight()), ) diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index d064185..df71aa5 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -1,34 +1,50 @@ +use std::collections::HashMap; + use wasm_bindgen::JsCast; -use web_sys::{HtmlCollection, NodeList}; +use web_sys::{HtmlCollection, NamedNodeMap, NodeList}; -pub fn node_list_to_vec(node_list: NodeList) -> Vec { +pub fn html_collection_to_vec(collection: HtmlCollection) -> Vec { let mut result = Vec::with_capacity( - node_list + collection .length() .try_into() .expect("usize should be at least u32."), ); - for i in 0..node_list.length() { + for i in 0..collection.length() { result.push( - node_list - .get(i) - .expect("Node should exist.") + collection + .item(i) + .expect("Item should exist.") .unchecked_into::(), ); } result } -pub fn html_collection_to_vec(collection: HtmlCollection) -> Vec { +pub fn named_node_map_to_hashmap(named_node_map: NamedNodeMap) -> HashMap { + let mut result = HashMap::with_capacity( + named_node_map + .length() + .try_into() + .expect("usize should be at least u32."), + ); + for i in 0..named_node_map.length() { + let attr = named_node_map.item(i).expect("Item should exist."); + result.insert(attr.name(), attr.value()); + } + result +} + +pub fn node_list_to_vec(node_list: NodeList) -> Vec { let mut result = Vec::with_capacity( - collection + node_list .length() .try_into() .expect("usize should be at least u32."), ); - for i in 0..collection.length() { + for i in 0..node_list.length() { result.push( - collection + node_list .item(i) .expect("Item should exist.") .unchecked_into::(), diff --git a/packages/dom/tests/element_queries.rs b/packages/dom/tests/element_queries.rs index c2411dc..020bd96 100644 --- a/packages/dom/tests/element_queries.rs +++ b/packages/dom/tests/element_queries.rs @@ -2,6 +2,7 @@ mod helpers; use std::sync::{Arc, LazyLock, Mutex}; +use indoc::indoc; use testing_library_dom::{ configure, ConfigFnOrPartial, MatcherOptions, PartialConfig, QueryError, SelectorMatcherOptions, }; @@ -64,8 +65,6 @@ fn query_can_return_none() -> Result<(), QueryError> { Ok(()) } -// TODO: enable test once pretty-dom is implemented -#[ignore] #[wasm_bindgen_test] fn get_throws_a_useful_error_message() -> Result<(), QueryError> { before_each(); @@ -76,97 +75,105 @@ fn get_throws_a_useful_error_message() -> Result<(), QueryError> { assert_eq!( Err(QueryError::Element( - "Unable to find an element with the placeholder text: LucyRicardo\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element with the placeholder text: LucyRicardo + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_placeholder_text("LucyRicardo", MatcherOptions::default()) ); assert_eq!( Err(QueryError::Element( - "Unable to find an element with the text: LucyRicardo. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element with the text: LucyRicardo. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_text("LucyRicardo", SelectorMatcherOptions::default()) ); assert_eq!( Err(QueryError::Element( - "Unable to find an element with the text: Lucy Ricardo (normalized from 'Lucy Ricardo'). This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element with the text: Lucy Ricardo (normalized from 'Lucy Ricardo'). This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_text("Lucy Ricardo", SelectorMatcherOptions::default()) ); assert_eq!( Err(QueryError::Element( - "Unable to find an element with the text: LucyRicardo, which matches selector 'span'. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element with the text: LucyRicardo, which matches selector 'span'. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_text("LucyRicardo", SelectorMatcherOptions::default().selector("span".into())) ); assert_eq!( Err(QueryError::Element( - "Unable to find an element by: [data-test-id=\"LucyRicardo\"]\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element by: [data-testid=\"LucyRicardo\"] + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_test_id("LucyRicardo", MatcherOptions::default()) ); assert_eq!( Err(QueryError::Element( - "Unable to find an element with the alt text: LucyRicardo\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element with the alt text: LucyRicardo + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_alt_text("LucyRicardo", MatcherOptions::default()) ); assert_eq!( Err(QueryError::Element( - "Unable to find an element with the title: LucyRicardo\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element with the title: LucyRicardo + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_title("LucyRicardo", MatcherOptions::default()) ); assert_eq!( Err(QueryError::Element( - "Unable to find an element with the display value: LucyRicardo\n\ - \n\ - Ignored nodes: comments, script, style\n\ -
\n\ -
\n\ -
" - .into() + indoc! {" + Unable to find an element with the display value: LucyRicardo + + Ignored nodes: comments, script, style +
+
+
"} + .into() )), container_queries.get_by_display_value("LucyRicardo", MatcherOptions::default()) ); diff --git a/packages/dom/tests/pretty_dom.rs b/packages/dom/tests/pretty_dom.rs new file mode 100644 index 0000000..7416b9a --- /dev/null +++ b/packages/dom/tests/pretty_dom.rs @@ -0,0 +1,24 @@ +mod helpers; + +use indoc::indoc; +use testing_library_dom::pretty_dom; +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + +use self::helpers::test_utils::{render, RenderReturn}; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn pretty_dom_prints_out_the_given_dom_element_tree() { + let RenderReturn { container, .. } = render("
Hello World!
", None); + + assert_eq!( + indoc! {" +
+
+ Hello World! +
+
"}, + pretty_dom(Some(container.into()), None) + ); +} diff --git a/packages/pretty-format/src/lib.rs b/packages/pretty-format/src/lib.rs index 62ea089..00f97d5 100644 --- a/packages/pretty-format/src/lib.rs +++ b/packages/pretty-format/src/lib.rs @@ -6,12 +6,33 @@ mod types; use std::rc::Rc; -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsCast, JsValue}; pub use error::PrettyFormatError; pub use types::{Config, Plugin, PrettyFormatOptions, Printer, Refs}; use types::{Colors, Plugins}; +use web_sys::js_sys::{BigInt, Number, Object}; + +fn print_number(val: &Number) -> String { + if Object::is(val, &JsValue::from_f64(-0.0)) { + "-0".into() + } else { + val.to_string(10) + .expect("Number should be formatted as string.") + .into() + } +} + +fn print_big_int(val: &BigInt) -> String { + format!( + "{}n", + String::from( + val.to_string(10) + .expect("Number should be formatted as string.") + ) + ) +} pub fn print_basic_value( val: &JsValue, @@ -19,25 +40,60 @@ pub fn print_basic_value( escape_regex: bool, escape_string: bool, ) -> Option { - // TODO - None + if *val == JsValue::TRUE { + return Some("true".into()); + } + if *val == JsValue::FALSE { + return Some("false".into()); + } + if val.is_undefined() { + return Some("undefined".into()); + } + if val.is_null() { + return Some("null".into()); + } + + let type_of = val.js_typeof(); + + if type_of == "number" { + return Some(print_number(val.unchecked_ref::())); + } + if type_of == "bigint" { + return Some(print_big_int(val.unchecked_ref::())); + } + if type_of == "string" { + if escape_string { + return Some( + val.as_string() + .expect("Value should be a string.") + .replace('"', "\\\"") + .replace('\\', "\\\\"), + ); + } + return Some(format!( + "\"{}\"", + val.as_string().expect("Value should be a string.") + )); + } + + todo!("print basic value {:?}", val) } pub fn print_complex_value( val: &JsValue, - config: Config, + config: &Config, indentation: String, depth: usize, refs: Refs, has_called_to_json: Option, ) -> String { - "".into() + todo!("print complex value {:?}", val) } fn print_plugin( plugin: Rc, val: &JsValue, - config: Config, + config: &Config, indentation: String, depth: usize, refs: Refs, @@ -50,14 +106,27 @@ fn find_plugin(plugins: &Plugins, val: &JsValue) -> Option> { } fn printer( - val: JsValue, - config: Config, + val: &JsValue, + config: &Config, indentation: String, depth: usize, refs: Refs, has_called_to_json: Option, ) -> String { - "".into() + if let Some(plugin) = find_plugin(&config.plugins, val) { + return print_plugin(plugin, val, config, indentation, depth, refs); + } + + if let Some(basic_result) = print_basic_value( + val, + config.print_function_name, + config.escape_regex, + config.escape_string, + ) { + return basic_result; + } + + print_complex_value(val, config, indentation, depth, refs, has_called_to_json) } fn validate_options(options: &PrettyFormatOptions) -> Result<(), PrettyFormatError> { @@ -131,7 +200,7 @@ fn get_config(options: PrettyFormatOptions) -> Config { } fn create_indent(indent: usize) -> String { - " ".repeat(indent + 1) + " ".repeat(indent) } pub fn format(val: &JsValue, options: PrettyFormatOptions) -> Result { @@ -142,7 +211,7 @@ pub fn format(val: &JsValue, options: PrettyFormatOptions) -> Result Result String { + self.open.clone() + } + + pub fn close(&self) -> String { + self.close.clone() + } + + pub fn paint(&self, s: &str) -> String { + format!("{}{}{}", self.open(), s, self.close()) + } +} + +impl From for Color { + fn from(value: ansi_style::Color) -> Self { + Color { + open: value.open(), + close: value.close().into(), + } + } +} + +#[derive(Clone, Debug, Default)] pub struct Colors { pub comment: Color, pub content: Color, @@ -12,18 +40,6 @@ pub struct Colors { pub value: Color, } -impl Default for Colors { - fn default() -> Self { - Self { - comment: Color::Any, - content: Color::Any, - prop: Color::Any, - tag: Color::Any, - value: Color::Any, - } - } -} - #[derive(Clone, Debug)] pub struct Theme { pub comment: Color, @@ -36,11 +52,11 @@ pub struct Theme { impl Default for Theme { fn default() -> Self { Self { - comment: Color::BlackBright, - content: Color::Any, - prop: Color::Yellow, - tag: Color::Cyan, - value: Color::Green, + comment: ansi_style::Color::BlackBright.into(), + content: Color::default(), + prop: ansi_style::Color::Yellow.into(), + tag: ansi_style::Color::Cyan.into(), + value: ansi_style::Color::Green.into(), } } } @@ -150,7 +166,7 @@ pub struct Config { pub spacing_outer: String, } -pub type Printer = dyn Fn(JsValue, Config, String, usize, Refs, Option) -> String; +pub type Printer = dyn Fn(&JsValue, &Config, String, usize, Refs, Option) -> String; pub trait Plugin { fn test(&self, val: &JsValue) -> bool; @@ -158,7 +174,7 @@ pub trait Plugin { fn serialize( &self, val: &JsValue, - config: Config, + config: &Config, indentation: String, depth: usize, refs: Refs,