Planned approach

I am really fond of testing frontend applications using @testing-library. I came across a post by the author discussing The Testing Trophy in relation to the traditional Testing Pyramid. It boils down to testing the applications as much as possible from the user perspective. Using testing-library this came quite naturally. I became less fun to write the components scoped tests, and in the end it matters most that the user can use the application as intended.

I wanted to test my rust dominator explaination with the same ease of querying & asserting the state of the application.

Running wasm application in jest

I build the application that results in a build.js file that has the .wasm included. First I tried to run it in jest by importing the build.js file and bootstrapping the wasm function by calling the main_js entrypoint defined in lib.rs. After a lot of painful debugging, and workarounds (TextDecoder not implemented, unable to inject js into the jsdom context.) It turns out jest / jsdom was not able to get the window object using the web-sys crate.

This resulted in jsdom not supporting using the window object. It probably mocks it somehow, but apparently something as simple as window instanceof Window being true is too much to ask.

Other options

I looked into testing the rust dominator application using wasm-bindgen-test and it turned out pretty well. It needs to have a chromedriver installed to actually run the code. If that means it no longer require me to debug annoying edge cases, and Just Works™ I won’t complain.

Some slight bumps in the road:

  • Turns out chromedriver and chrome need to be the exact same version
  • It requires arcane knowledge to configure wasm-bindgen-test
  • Injecting js in the page context is slightly offputting

Basic config

TODO

Bonus snippets

Give the browser time to process changes:

    pub async fn yield_to_browser() {
        JsFuture::from(js_sys::Promise::resolve(&JsValue::null()))
            .await
            .unwrap();
    }
 
    // Call it like this to give the browser a chance to render things
    yield_to_browser().await;
 

Render arbitrary components using rust dominator in test

    pub fn render_test_dom(dom: Dom) {
        dominator::replace_dom(
            &dominator::body(),
            &dominator::body().first_child().unwrap(),
            dom,
        );
    }

Getting elements by data-test-id

    pub fn get_by_all_data_test_id(test_id: &str) -> NodeList {
        web_sys::window()
            .unwrap()
            .document()
            .unwrap()
            .query_selector_all(&format!("[data-test-id={}]", test_id))
            .unwrap()
    }
 
    pub fn get_by_data_test_id(test_id: &str) -> Element {
        let nodes = get_by_all_data_test_id(test_id);
        if nodes.length() != 1 {
            panic!("Expected only one element to be returned");
        }
        let node = nodes.get(0).unwrap();
 
        node.dyn_into::<Element>().unwrap()
    }

Get the element to click out of the NodeList

    let todo_items = get_by_all_data_test_id("todo-item");
    let button_ref = todo_items.get(0).unwrap().dyn_into::<HtmlElement>().unwrap();
    button_ref.click();

Add js to the context where the test is ran

    let window = window().expect("no global `window` exists");
    let document = window.document().expect("should have a document on window");
 
    let script = document.create_element("script").unwrap();
    script.set_inner_html(&include_str!("./test-setup.js"));
 
    document.body().unwrap().append_child(&script).unwrap();

Alternatively with wasm bindgen you can also add inline js

    #[wasm_bindgen(inline_js = r#"
        import "./path/to/test-setup.js";
        alert(123);
    "#)]
    extern "C" {
    }