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 }
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 $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 $crate::make_single_query!($query_by_all, $get_multiple_error, [<_query_by_ $name>], $matcher_type, $options_type);
239
240 $crate::wrap_single_query_with_suggestion!([<_query_by_ $name>], stringify!($query_by_all), Variant::Query, [<query_by_ $name>], $matcher_type, $options_type);
242
243 $crate::make_get_all_query!($query_by_all, $get_missing_error, [<_get_all_by_ $name>], $matcher_type, $options_type);
245
246 $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 $crate::make_single_query!([<_get_all_by_ $name>], $get_multiple_error, [<_get_by_ $name>], $matcher_type, $options_type);
251
252 $crate::wrap_single_query_with_suggestion!([<_get_by_ $name>], stringify!($query_by_all), Variant::Get, [<get_by_ $name>], $matcher_type, $options_type);
254
255 $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 $crate::make_find_query!([<_find_all_by_ $name>], [<find_all_by_ $name>], $matcher_type, $options_type, Vec<HtmlElement>);
260
261 $crate::wrap_single_query_with_suggestion!([<_get_by_ $name>], stringify!($query_by_all), Variant::Find, [<_find_by_ $name>], $matcher_type, $options_type);
263
264 $crate::make_find_query!([<_find_by_ $name>], [<find_by_ $name>], $matcher_type, $options_type, Option<HtmlElement>);
266 }
267 }
268 };
269}