Testing DOMINATOR UI code

Testing is an essential part of any serious software project, UIs included. Our DOMINATOR UI code cannot execute directly in an x86 unit test environment though, as it relies heavily upon browser APIs being present to function.

Luckily, wasm-bindgen provide a testing tool that allows us to execute tests compiled to wasm inside a headless browser, essentially providing us with a unit test-like environment to verify our UI code in!

To get a better overview of the wasm-bindgen-test crate, head over to their official book to get started.

Requirements

To recap the required configuration, you will need to set up a few things in your project.

First of all, we have to add wasm-bindgen-tests as a dev-dependency to our crate:

[dev-dependencies]
wasm-bindgen-test = "x.y.z" # Please use the latest compatible version you can

Now, to configure the test environment to be a browser, we need to add the following to the root of our crate (usually lib.rs)

#![allow(unused)]
fn main() {
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
}

We also need to install the wasm-bindgen-cli tool, and instruct cargo to use it to execute our tests for the wasm32 target. Make sure that the x.y-z version of wasm-bindgen-cli matches the version of wasm-bindgen you are using in your projects Cargo.toml.

cargo install wasm-bindgen-cli --vers x.y.z

Then add the following to the .cargo/config.toml file in your project:

[target.wasm32-unknown-unknown]
runner = "wasm-bindgen-test-runner"

Lastly, you will need to install either chromedriver or geckodriver to execute your tests, depending on your browser choice.

Running the tests

To run your headless tests, the following commands should work:

CHROMEDRIVER=/path/to/chromedriver cargo test --target wasm32-unknown-unknown
GECKODRIVER=/path/to/geckodriver cargo test --target wasm32-unknown-unknown

If you wish to run the tests in a headed browser, i.e. in a browser instance opened in a new window, you can set the NO_HEADLESS=1 environment variable:

NO_HEADLESS=1 CHROMEDRIVER=/path/to/chromedriver cargo test --target wasm32-unknown-unknown

An example component with associated in-browser test

Here's a full example of a component with a corresponding unit test. It's not a very complex component, but it serves to illustrate how we can test that some interaction results in the expected dom structure. In this case, we make sure that clicking the button 3 times, results in 3 rows of a specific class being added to the DOM.

use dominator::{clone, Dom, events, html};
use futures_signals::signal::{Mutable, SignalExt};

pub fn my_cmp() -> Dom {
    let counter = Mutable::new(0);

    html!("div", {
        .child(html!("button", {
            .attr("id", "click-me")
            .event(clone!(counter => move |_: events::Click| {
                counter.set(counter.get() + 1);
            }))
        }))
        .children_signal_vec(counter.signal_ref(|count| {
            (0..*count).map(|v| {
                html!("div", {
                    .class("count-me")
                    .text(v.to_string().as_str())
                })
            }).collect()
        }).to_signal_vec())
    })
}
#[cfg(test)]
mod in_browser_tests {
    use futures_signals::signal::{Mutable, SignalExt};
    use wasm_bindgen::{JsCast, JsValue};
    use wasm_bindgen_futures::JsFuture;
    use wasm_bindgen_test::wasm_bindgen_test;
    use web_sys::HtmlElement;
    use crate::techniques_and_patterns::in_browser_testing::my_cmp;

    #[wasm_bindgen_test]
    async fn some_dom_test() {
        let cmp_to_test = my_cmp();

        // Make sure to replace the current body content, to avoid
        // multiple tests contaminating each other
        dominator::replace_dom(&dominator::body(), &dominator::body().first_child().unwrap(), cmp_to_test);

        let button = web_sys::window().unwrap()
            .document().unwrap()
            .get_element_by_id("click-me").unwrap();

        let button_ref = button.dyn_ref::<HtmlElement>().unwrap();

        button_ref.click();
        button_ref.click();
        button_ref.click();

        // Yield execution time to the browser!
        // This is important; if omitted, the browser will not have time to update the DOM before we do our assertion!
        JsFuture::from(js_sys::Promise::resolve(&JsValue::null()))
            .await
            .unwrap();

        // Verify the resulting dom:
        let count_mes = web_sys::window().unwrap()
            .document().unwrap()
            .get_elements_by_class_name("count-me");


        assert_eq!(count_mes.length(), 3);
    }
}