frontest/lib.rs
1//! A lightweight library to query and assert DOM.
2//!
3//! Frontest is heavily inspired by [`dom-testing-library`] and [`react-testing-library`].
4//! It provides a set of queries that you can use to quickly find your elements in document
5//! with respect to accessibility priorities.
6//!
7//! # Basic usage:
8//!
9//! A [`Query`] trait allows for selecting elements in a way that users would interact with them
10//! with the respect for assisstive technology.
11//!
12//! ```no_run
13//! use frontest::prelude::*;
14//! use gloo::utils::{body, document};
15//!
16//! let div = document().create_element("div").unwrap();
17//! div.set_inner_html(
18//! r#"<div>
19//! <label>
20//! I will start testing my frontend!
21//! <button>
22//! Take the red pill
23//! </button>
24//! </label>
25//! <label>
26//! It's too problematic dude...
27//! <button>
28//! Take the blue pill
29//! </button>
30//! </label>
31//! </div>"#,
32//! );
33//! body().append_child(&div).unwrap();
34//!
35//! let go_to_matrix = div
36//! .get(&HasRole("button").and(Not(HasLabel("It's too problematic dude..."))))
37//! .unwrap();
38//! go_to_matrix.click();
39//!
40//! body().remove_child(&div).unwrap();
41//! ```
42//!
43//! # About testing:
44//!
45//! This library aims to allow developers to test their application in a way that a user would interact with it.
46//! For this purpose it is recommended to prioritize certain queries above another.
47//! Currently only two matchers are implemented. More will be available in future releases.
48//! Matchers should be prioritized like so:
49//! - [`HasRole`] Should always be used where possible. It allows accessing elements that are exposed into accessibility tree.
50//! - [`HasLabel`] Also should be used where possible. Is supported by screen readers and allows for easier focusing elements.
51//! - [`HasPlaceholder`] Not as great option as predecessors, however still a better alternative than [`HasText`] for accessible elements.
52//! - [`HasText`] Matches the text in a way that it is presented to the user.
53//! All css rules applies eg. elements with `visibility: hidden;` won't be ever matched.
54//! Can be used to select non-interactive components or further restrict other queries.
55//!
56//! # Matchers:
57//!
58//! Matchers are predicates for [`HtmlElement`]. They return [`true`] if given element suffices some criteria
59//! or [`false`] otherwise.
60//!
61//! Using the matcher [`Not`] and methods from [`Joinable`] trait it is possible to combine multiple matchers into
62//! a logical expression.
63//!
64//! ## You can easily implement your own `Matcher`s.
65//!
66//! ```no_run
67//! # use frontest::prelude::*;
68//! # use gloo::utils::{document, body};
69//! # use web_sys::HtmlElement;
70//! struct IsHidden;
71//!
72//! impl Matcher for IsHidden {
73//! fn matches(&self, elem: &HtmlElement) -> bool {
74//! elem.hidden()
75//! }
76//! }
77//!
78//! let div = document().create_element("div").unwrap();
79//! div.set_inner_html(
80//! r#"<button hidden>
81//! Yayyy frontend in rust!
82//! </button>"#
83//! );
84//! div.append_child(&div).unwrap();
85//!
86//! let hidden_button = div.get(&IsHidden.and(HasRole("button"))).unwrap();
87//!
88//! body().remove_child(&div).unwrap();
89//! ```
90//!
91//! # Integration:
92//! Tests should be run using [`wasm-bindgen-test`]. It allows running them directly in browsers or in node-js.
93//!
94//! Currently this crate provides a [`render`] function that allows for quickly rendering any [`html`] created with [`yew`].
95//! It was choosen to render the html instead of directly taking a component so it is easier to wrap them with [`ContextProvider`] and so on.
96//!
97//! ## Example:
98//! ```no_run
99//! # #[cfg(feature = "yew")]
100//! # {
101//! # use yew::prelude::*;
102//! #[function_component(Incrementable)]
103//! fn incrementable() -> Html {
104//! let counter = use_state(|| 0);
105//! let onclick = {
106//! let counter = counter.clone();
107//! Callback::from(move |_| counter.set(*counter + 1))
108//! };
109//! html! {
110//! <div>
111//! <p>{ format!("Value: {}", *counter) }</p>
112//! <button {onclick}>{ "Add" }</button>
113//! </div>
114//! }
115//! }
116//!
117//! # use wasm_bindgen_test::wasm_bindgen_test;
118//! # use gloo::utils::body;
119//! use frontest::prelude::*;
120//! use frontest::yew::render;
121//! use yew::html;
122//!
123//! #[wasm_bindgen_test]
124//! async fn clicking_on_button_should_increment_value() {
125//! let mount = render(html! { <Incrementable /> }).await;
126//! let value = mount.get(&HasText("Value:")).unwrap();
127//! let button = mount.get(&HasRole("button")).unwrap();
128//!
129//! assert_eq!("Value: 0", value.inner_text());
130//! button.click();
131//! assert_eq!("Value: 1", value.inner_text());
132//!
133//! body().remove_child(&mount).unwrap();
134//! }
135//! # }
136//! ```
137//!
138//! ## Warning:
139//!
140//! [`wasm-bindgen-test`] runs all tests sequentially and let them manipulate real DOM.
141//! However it doesn't recreate full DOM for each test, so things done in one test may impact others.
142//! Always make sure you are doing a proper cleanup of DOM after your tests eg. remove mounted child element.
143//! Hopefully in future this library will provide some kind of RAII for running tests.
144//!
145//! [`dom-testing-library`]: https://testing-library.com/docs/dom-testing-library/intro
146//! [`react-testing-library`]: https://testing-library.com/docs/react-testing-library/intro
147//! [`wasm-bindgen-test`]: https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/usage.html
148//! [`render`]: yew::render
149//! [`html`]: ::yew::html!
150//! [`ContextProvider`]: ::yew::context::ContextProvider
151//! [`HtmlElement`]: web_sys::HtmlElement
152//! [`HasText`]: query::HasText
153//! [`HasLabel`]: query::HasLabel
154//! [`HasPlaceholder`]: query::HasPlaceholder
155//! [`HasRole`]: query::HasRole
156//! [`Not`]: query::Not
157//! [`Query`]: query::Query
158//! [`Joinable`]: query::Joinable
159use gloo::timers::future::sleep;
160use std::time::Duration;
161
162#[cfg(test)]
163wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
164
165/// A convenient imports for testing.
166pub mod prelude {
167 pub use crate::query::{And, Not, Or};
168 pub use crate::query::{HasLabel, HasPlaceholder, HasRole, HasText};
169
170 pub use crate::query::{Joinable, Matcher, Query};
171}
172/// Find various elements across the website as the user would.
173pub mod query;
174
175#[cfg(test)]
176#[wasm_bindgen_test::wasm_bindgen_test]
177async fn doctest_basic_usage() {
178 use crate::query::*;
179 use gloo::utils::{body, document};
180
181 let div = document().create_element("div").unwrap();
182 div.set_inner_html(
183 r#"<div>
184 <label>
185 I will start testing my frontend!
186 <button>
187 Take the red pill
188 </button>
189 </label>
190 <label>
191 It's too problematic dude...
192 <button>
193 Take the blue pill
194 </button>
195 </label>
196 </div>"#,
197 );
198 body().append_child(&div).unwrap();
199
200 let go_to_matrix = div
201 .get(&HasRole("button").and(Not(HasLabel("It's too problematic dude..."))))
202 .unwrap();
203 go_to_matrix.click();
204
205 body().remove_child(&div).unwrap();
206}
207
208/// A helpers when testing frontend made with [`yew`]
209///
210/// [`yew`]: ::yew
211#[cfg(feature = "yew")]
212pub mod yew {
213 use ::yew::prelude::*;
214 use web_sys::Element;
215
216 #[derive(Properties, PartialEq)]
217 struct WrapperProps {
218 content: Html,
219 }
220
221 #[function_component(Wrapper)]
222 fn wrapper(props: &WrapperProps) -> Html {
223 props.content.clone()
224 }
225
226 /// Render arbitrary output of [`html`] macro, mount it into body and return mount-point [`Element`]
227 ///
228 /// # Example:
229 /// ```no_run
230 /// # use yew::prelude::*;
231 /// #[function_component(Incrementable)]
232 /// fn incrementable() -> Html {
233 /// let counter = use_state(|| 0);
234 /// let onclick = {
235 /// let counter = counter.clone();
236 /// Callback::from(move |_| counter.set(*counter + 1))
237 /// };
238 /// html! {
239 /// <div>
240 /// <p>{ format!("Value: {}", *counter) }</p>
241 /// <button {onclick}>{ "Add" }</button>
242 /// </div>
243 /// }
244 /// }
245 ///
246 /// # use wasm_bindgen_test::wasm_bindgen_test;
247 /// # use gloo::utils::body;
248 /// use frontest::prelude::*;
249 /// use frontest::yew::render;
250 /// use yew::html;
251 ///
252 /// #[wasm_bindgen_test]
253 /// async fn clicking_on_button_should_increment_value() {
254 /// let mount = render(html! { <Incrementable /> }).await;
255 /// let value = mount.get(&HasText("Value:")).unwrap();
256 /// let button = mount.get(&HasRole("button")).unwrap();
257 ///
258 /// assert_eq!("Value: 0", value.inner_text());
259 /// button.click();
260 /// assert_eq!("Value: 1", value.inner_text());
261 ///
262 /// body().remove_child(&mount).unwrap();
263 /// }
264 /// ```
265 ///
266 /// [`html`]: ::yew::html!
267 /// [`element`]: web_sys::Element
268 pub async fn render(content: Html) -> Element {
269 let div = gloo::utils::document().create_element("div").unwrap();
270 gloo::utils::body().append_child(&div).unwrap();
271 let res = div.clone();
272 ::yew::Renderer::<Wrapper>::with_root_and_props(div, WrapperProps { content }).render();
273 ::yew::platform::time::sleep(std::time::Duration::ZERO).await;
274
275 res
276 }
277
278 #[cfg(test)]
279 #[wasm_bindgen_test::wasm_bindgen_test]
280 async fn doctest_yew_render() {
281 use ::yew::prelude::*;
282 #[function_component(Incrementable)]
283 fn incrementable() -> Html {
284 let counter = use_state(|| 0);
285 let onclick = {
286 let counter = counter.clone();
287 Callback::from(move |_| counter.set(*counter + 1))
288 };
289 html! {
290 <div>
291 <p>{ format!("Value: {}", *counter) }</p>
292 <button {onclick}>{ "Add" }</button>
293 </div>
294 }
295 }
296
297 use crate::query::{HasRole, HasText, Query};
298 use crate::yew::render;
299 use ::yew::html;
300 // use wasm_bindgen_test::wasm_bindgen_test;
301 use gloo::utils::body;
302
303 // #[wasm_bindgen_test]
304 // async fn clicking_on_button_should_increment_value() {
305 let mount = render(html! { <Incrementable /> }).await;
306 let value = mount.get(&HasText("Value:")).unwrap();
307 let button = mount.get(&HasRole("button")).unwrap();
308
309 assert_eq!("Value: 0", value.inner_text());
310 button.click();
311 // Events are handled when the scheduler is yielded. So we need to add this after any interaction with the DOM.
312 ::yew::platform::time::sleep(std::time::Duration::ZERO).await;
313 assert_eq!("Value: 1", value.inner_text());
314
315 body().remove_child(&mount).unwrap();
316 // }
317 }
318}
319
320/// Preempt execution of current task to let the js's main thread do things like re-render.
321///
322/// # Warning:
323/// I'm currently unsure in which condition tick is required. Most cases should work without the need of using it.
324#[doc(hidden)]
325pub async fn tick() {
326 sleep(Duration::ZERO).await;
327}