Implement React v18 from Scratch Using WASM and Rust - [9] Unit Test with Jest

ayou - Apr 29 - - Dev Community

Based on big-react,I am going to implement React v18 core features from scratch using WASM and Rust.

Code Repository:https://github.com/ParadeTo/big-react-wasm

The tag related to this article:v9

A mature and stable man library like big-react-wasm definitely needs unit tests. So, in this article, we will pause the feature development for now and add unit tests to big-react-wasm. The goal this time is to run the 17 test cases provided by the react official documentation for ReactElement.

Since the test case code runs in a Node environment, we need to modify our build output. First, let's add a new script:

"build:test": "node scripts/build.js --test",
Enter fullscreen mode Exit fullscreen mode

Next, let's add handling for --test in our build.js file. There are two main points to consider. First, we need to change the output target of wasm-pack to nodejs:

execSync(
  `wasm-pack build packages/react --out-dir ${cwd}/dist/react --out-name jsx-dev-runtime ${
    isTest ? '--target nodejs' : ''
  }`
)
Enter fullscreen mode Exit fullscreen mode

In react-dom/index.js, the statement that imports updateDispatcher from react needs to be changed to the commonjs format:

isTest
  ? 'const {updateDispatcher} = require("react");\n'
  : 'import {updateDispatcher} from "react";\n'
Enter fullscreen mode Exit fullscreen mode

After setting up the Jest environment, we'll copy the ReactElement-test.js file from big-react and modify the module import paths:

// ReactElement-test.js
React = require('../../dist/react')
ReactDOM = require('../../dist/react-dom')
ReactTestUtils = require('../utils/test-utils')

// test-utils.js
const ReactDOM = require('../../dist/react-dom')

exports.renderIntoDocument = (element) => {
  const div = document.createElement('div')
  return ReactDOM.createRoot(div).render(element)
}
Enter fullscreen mode Exit fullscreen mode

When executing jest, you may notice that several test cases fail due to the following issues:

  • Type of REACT_ELEMENT_TYPE

Since REACT_ELEMENT_TYPE in big-react-wasm is of type string, we need to modify these test cases accordingly:

it('uses the fallback value when in an environment without Symbol', () => {
  expect((<div />).$$typeof).toBe('react.element')
})
Enter fullscreen mode Exit fullscreen mode

This difference will also affect the execution of the following test case:

const jsonElement = JSON.stringify(React.createElement('div'))
expect(React.isValidElement(JSON.parse(jsonElement))).toBe(false)
Enter fullscreen mode Exit fullscreen mode

The reason is that the normal value of $$typeof is of type Symbol. Therefore, when ReactElement is JSON.stringify-ed, this property gets removed. In React.isValidElement, it checks whether $$typeof is equal to REACT_ELEMENT_TYPE, resulting in false as the output. However, in big-react-wasm, REACT_ELEMENT_TYPE is a string, so the result is true.

Why not change it to Symbol then? Well, Rust has many restrictions in place to ensure thread safety, making it cumbersome to define a constant of type Symbol. Let me provide an example given by ChatGPT to illustrate this:

use wasm_bindgen::prelude::*;
use js_sys::Symbol;
use std::sync::Mutex;

pub static REACT_ELEMENT_TYPE: Mutex<Option<Symbol>> = Mutex::new(None);

// Somewhere in your initialization code, you would set the symbol:
fn initialize() {
    let mut symbol = REACT_ELEMENT_TYPE.lock().unwrap();
    *symbol = Some(Symbol::for_("react.element"));
}

// And when you need to use the symbol, you would lock the Mutex to safely access it:
fn use_symbol() {
    let symbol = REACT_ELEMENT_TYPE.lock().unwrap();
    if let Some(ref symbol) = *symbol {
        // Use the symbol here
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Object without a prototype

The following test case creates an object without a prototype using Object.create. In JavaScript, it is possible to iterate over the keys of this object.

However, when calling config.dyn_ref::<Object>() to convert it to an Object in Rust, it returns None. But when calling config.is_object(), the result is indeed true.

it('does not fail if config has no prototype', () => {
  const config = Object.create(null, {foo: {value: 1, enumerable: true}})
  const element = React.createElement(ComponentFC, config)
  console.log(element)
  expect(element.props.foo).toBe(1)
})
Enter fullscreen mode Exit fullscreen mode

So, for this situation, we can simply use the original config as the props:

Reflect::set(&react_element, &"props".into(), &config).expect("props panic");
Enter fullscreen mode Exit fullscreen mode
  • react-dom Host Config

In the original implementation of react-dom's HostConfig, an error occurs if the window object does not exist:

fn create_text_instance(&self, content: String) -> Rc<dyn Any> {
    let window = window().expect("no global `window` exists");
    let document = window.document().expect("should have a document on window");
    Rc::new(Node::from(document.create_text_node(content.as_str())))
}
Enter fullscreen mode Exit fullscreen mode

So, we need to make some modifications:

fn create_text_instance(&self, content: String) -> Rc<dyn Any> {
    match window() {
        None => {
            log!("no global `window` exists");
            Rc::new(())
        }
        Some(window) => {
            let document = window.document().expect("should have a document on window");
            Rc::new(Node::from(document.create_text_node(content.as_str())))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

But wait, why doesn't big-react throw an error? It's because big-react specifies the testing environment as jsdom. According to the official documentation, jsdom is a pure JavaScript implementation of the web standards, specifically designed for Node.js, including the WHATWG DOM and HTML standards.

module.exports = {
  testEnvironment: 'jsdom',
}
Enter fullscreen mode Exit fullscreen mode

If that's the case, why doesn't big-react-wasm work in the same jsdom environment? After studying the source code, I found that when window() is called, it actually executes the following code:

js_sys::global().dyn_into::<Window>().ok()
Enter fullscreen mode Exit fullscreen mode

In the code snippet, when dyn_into::<Window>() is called, it uses instanceof to check if the current object is a Window. Could this be the reason? Let's experiment by adding a code snippet like this to the test cases:

console.log(window instanceof Window)
Enter fullscreen mode Exit fullscreen mode

The result is false, surprisingly. It seems to be a bug in jsdom. Let's search on GitHub and indeed, we found an issue related to this. Moreover, someone has already provided a solution:

// jest-config.js
module.exports = {
  setupFilesAfterEnv: ['<rootDir>/setup-jest.js'],
}

// setup-jest.js
Object.setPrototypeOf(window, Window.prototype)
Enter fullscreen mode Exit fullscreen mode

Let's add that solution and revert the Host Config back to its original state.

With these changes, all 17 test cases pass successfully.

Image description

Please kindly give me a star!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player