testing_library_dom/
query_helpers.rs

1use web_sys::{Element, HtmlElement};
2
3use crate::{
4    config::get_config,
5    error::QueryError,
6    matches::{fuzzy_matches, make_normalizer, matches},
7    util::node_list_to_vec,
8    Matcher, MatcherOptions, NormalizerOptions,
9};
10
11pub fn get_element_error(message: Option<String>, container: Element) -> QueryError {
12    (get_config().get_element_error)(message, container)
13}
14
15pub fn get_multiple_elements_found_error(message: String, container: Element) -> QueryError {
16    get_element_error(Some(format!("{message}\n\n(If this is intentional, then use the `*_all_by_*` variant of the query (like `query_all_by_text`, `get_all_by_text`, or `find_all_by_text`)).")), container)
17}
18
19pub fn query_all_by_attribute<M: Into<Matcher>>(
20    attribute: String,
21    container: &HtmlElement,
22    text: M,
23    MatcherOptions {
24        exact,
25        trim,
26        collapse_whitespace,
27        normalizer,
28        ..
29    }: MatcherOptions,
30) -> Result<Vec<HtmlElement>, QueryError> {
31    let text = text.into();
32    let exact = exact.unwrap_or(true);
33
34    let matcher = match exact {
35        true => matches,
36        false => fuzzy_matches,
37    };
38    let match_normalizer = make_normalizer(NormalizerOptions {
39        trim,
40        collapse_whitespace,
41        normalizer,
42    })?;
43
44    Ok(node_list_to_vec::<HtmlElement>(
45        container
46            .query_selector_all(&format!("[{attribute}]"))
47            .map_err(QueryError::JsError)?,
48    )
49    .into_iter()
50    .filter(|node| {
51        matcher(
52            node.get_attribute(&attribute),
53            Some(node),
54            &text,
55            match_normalizer.as_ref(),
56        )
57    })
58    .collect())
59}
60
61pub fn query_by_attribute<M: Into<Matcher>>(
62    attribute: String,
63    container: &HtmlElement,
64    text: M,
65    options: MatcherOptions,
66) -> Result<Option<HtmlElement>, QueryError> {
67    let text = text.into();
68
69    let mut els = query_all_by_attribute(attribute.clone(), container, text.clone(), options)?;
70    if els.len() > 1 {
71        Err(get_multiple_elements_found_error(
72            format!("Found multiple elements by [{attribute}={text}]"),
73            container.clone().into(),
74        ))
75    } else {
76        Ok(els.pop())
77    }
78}
79
80pub fn get_suggestion_error(suggestion: String, container: Element) -> QueryError {
81    (get_config().get_element_error)(
82        Some(format!(
83            "A better query is available, try this: {suggestion}",
84        )),
85        container,
86    )
87}
88
89#[macro_export]
90macro_rules! make_single_query {
91    ($all_query:ident, $get_multiple_error:ident, $name:ident, $matcher_type:ty, $options_type:ty) => {
92        pub fn $name<M: Into<$matcher_type>>(
93            container: &HtmlElement,
94            matcher: M,
95            options: $options_type,
96        ) -> Result<Option<HtmlElement>, QueryError> {
97            let matcher = matcher.into();
98
99            let mut els = $all_query(container, matcher.clone(), options.clone())?;
100            if els.len() > 1 {
101                let element_strings = els
102                    .into_iter()
103                    .map(|element| format!("{}", get_element_error(None, element.into())))
104                    .collect::<Vec<_>>()
105                    .join("\n\n");
106
107                Err(get_multiple_elements_found_error(
108                    format!(
109                        "{}\n\nHere are the matching elements:\n\n{}",
110                        $get_multiple_error(container, matcher, options)?,
111                        element_strings
112                    ),
113                    container.clone().into(),
114                ))
115            } else {
116                Ok(els.pop())
117            }
118        }
119    };
120}
121
122#[macro_export]
123macro_rules! make_get_all_query {
124    ($all_query:ident, $get_missing_error:ident, $name:ident, $matcher_type:ty, $options_type:ty) => {
125        pub fn $name<M: Into<$matcher_type>>(
126            container: &HtmlElement,
127            matcher: M,
128            options: $options_type,
129        ) -> Result<Vec<HtmlElement>, QueryError> {
130            let matcher = matcher.into();
131
132            let els = $all_query(container, matcher.clone(), options.clone())?;
133            if els.is_empty() {
134                return Err((get_config().get_element_error)(
135                    Some($get_missing_error(container, matcher, options)?),
136                    container.clone().into(),
137                ));
138            } else {
139                Ok(els)
140            }
141        }
142    };
143}
144
145#[macro_export]
146macro_rules! make_find_query {
147    ($getter:ident, $name:ident, $matcher_type:ty, $options_type:ty, $return_type:ty) => {
148        pub fn $name<M: Into<$matcher_type>>(
149            container: &HtmlElement,
150            matcher: M,
151            options: $options_type,
152            wait_for_options: WaitForOptions,
153        ) -> Result<$return_type, QueryError> {
154            wait_for(
155                {
156                    let matcher = matcher.into();
157                    let container = container.clone();
158                    Box::new(move || $getter(&container, matcher.clone(), options.clone()))
159                },
160                wait_for_options.container(container.clone()),
161            )
162        }
163    };
164}
165
166#[macro_export]
167macro_rules! wrap_single_query_with_suggestion {
168    ($query:ident, $query_by_all_name:expr, $variant:expr, $name:ident, $matcher_type:ty, $options_type:ty) => {
169        pub fn $name<M: Into<$matcher_type>>(
170            container: &HtmlElement,
171            matcher: M,
172            options: $options_type,
173        ) -> Result<Option<HtmlElement>, QueryError> {
174            let element = $query(container, matcher, options)?;
175            let suggest = get_config().throw_suggestions;
176
177            if let Some(element) = element.as_ref() {
178                if suggest {
179                    let suggestion = get_suggested_query(element, Some($variant), None);
180                    if let Some(suggestion) = suggestion {
181                        if !$query_by_all_name.ends_with(&suggestion.query_name.to_string()) {
182                            return Err(get_suggestion_error(
183                                suggestion.to_string(),
184                                container.clone().into(),
185                            ));
186                        }
187                    }
188                }
189            }
190
191            Ok(element)
192        }
193    };
194}
195
196#[macro_export]
197macro_rules! wrap_all_by_query_with_suggestion {
198    ($query:ident, $query_by_all_name:expr, $variant:expr, $name:ident, $matcher_type:ty, $options_type:ty) => {
199        pub fn $name<M: Into<$matcher_type>>(
200            container: &HtmlElement,
201            matcher: M,
202            options: $options_type,
203        ) -> Result<Vec<HtmlElement>, QueryError> {
204            let els = $query(container, matcher, options)?;
205            let suggest = get_config().throw_suggestions;
206
207            if !els.is_empty() && suggest {
208                // TODO
209            }
210
211            Ok(els)
212        }
213    };
214}
215
216#[macro_export]
217macro_rules! build_queries {
218    ($query_by_all:ident, $get_multiple_error:ident, $get_missing_error:ident, $name:ident, $matcher_type:ty, $options_type:ty) => {
219        paste::paste! {
220            mod internal {
221                use web_sys::HtmlElement;
222
223                use $crate::{
224                    config::get_config,
225                    error::QueryError,
226                    query_helpers::{get_element_error, get_multiple_elements_found_error, get_suggestion_error},
227                    types::{Variant, WaitForOptions},
228                    suggestions::{get_suggested_query},
229                    wait_for::{wait_for},
230                };
231
232                use super::{$query_by_all, $get_multiple_error, $get_missing_error};
233
234                // Query all by
235                $crate::wrap_all_by_query_with_suggestion!($query_by_all, stringify!($query_by_all), Variant::QueryAll, [<query_all_by_ $name>], $matcher_type, $options_type);
236
237                // Internal query by
238                $crate::make_single_query!($query_by_all, $get_multiple_error, [<_query_by_ $name>], $matcher_type, $options_type);
239
240                // Query by
241                $crate::wrap_single_query_with_suggestion!([<_query_by_ $name>], stringify!($query_by_all), Variant::Query, [<query_by_ $name>], $matcher_type, $options_type);
242
243                // Internal get all by
244                $crate::make_get_all_query!($query_by_all, $get_missing_error, [<_get_all_by_ $name>], $matcher_type, $options_type);
245
246                // Get all by
247                $crate::wrap_all_by_query_with_suggestion!([<_get_all_by_ $name>], stringify!($query_by_all).replace("query", "get"), Variant::GetAll, [<get_all_by_ $name>], $matcher_type, $options_type);
248
249                // Internal get by
250                $crate::make_single_query!([<_get_all_by_ $name>], $get_multiple_error, [<_get_by_ $name>], $matcher_type, $options_type);
251
252                // Get by
253                $crate::wrap_single_query_with_suggestion!([<_get_by_ $name>], stringify!($query_by_all), Variant::Get, [<get_by_ $name>], $matcher_type, $options_type);
254
255                // Internal find all by
256                $crate::wrap_all_by_query_with_suggestion!([<_get_all_by_ $name>], stringify!($query_by_all), Variant::FindAll, [<_find_all_by_ $name>], $matcher_type, $options_type);
257
258                // Find all by
259                $crate::make_find_query!([<_find_all_by_ $name>], [<find_all_by_ $name>], $matcher_type, $options_type, Vec<HtmlElement>);
260
261                // Internal find by
262                $crate::wrap_single_query_with_suggestion!([<_get_by_ $name>], stringify!($query_by_all), Variant::Find, [<_find_by_ $name>], $matcher_type, $options_type);
263
264                // Find by
265                $crate::make_find_query!([<_find_by_ $name>], [<find_by_ $name>], $matcher_type, $options_type, Option<HtmlElement>);
266            }
267        }
268    };
269}