Riot Component Unit Test with Vitest (JsDom env)
Tags: riotjs, javascript, beginners, frontend
Published: 2024-03-22 11:40:26

This article covers testing Riot Input components using Vitest in a JsDOM environment.

> A second method exists: Vitest into a Node environment with a Riot Server-Side Rendering, read [this article to learn more](https://dev.to/steeve/riot-component-unit-test-with-vitest-node-env-hk0). 

Before going forward, ensure you have a base Riot+Vite project and have created at least one component. If not, you can read my previous article on [creating an Input Component](https://dev.to/steeve/input-component-with-riotjs-material-design-30c4).

> These articles form a series focusing on RiotJS paired with BeerCSS, designed to guide you through creating components and mastering best practices for building production-ready applications. I assume you have a foundational understanding of Riot; however, feel free to refer to the documentation if needed: https://riot.js.org/documentation/

## Riot + Vitest + JsDOM: Next Generation Testing Framework 

As I am using Vite as a development server to get a real-time rendering of my Riot application, using Vitest for testing brings many advantages:

* A test runner that uses the same configuration as Vite (vite.config.js).
* It provides a compatible Jest API (one of the most used test runners)
* Performances: it uses Worker threads to run as much as possible in parallel.
* Easy setup and configuration (almost none).

The default environment in Vitest is a Node.js environment. As we build a web application, we can use a browser-like environment through JsDom, which emulates a web browser for testing Riot Components.

Add Vitest add JsDom to your Riot project:
```bash
npm install -D vitest jsdom
```
To execute the test, add the following section to your package.json:
```json
{
  "scripts": {
    "test": "vitest"
  }
}
```
And that's it for Vitest! If you need a specific configuration, add `test` property in your Vite config vite.config.js. Configuration documentation: https://vitest.dev/config/

Let's create a test file for the Input component named `test/c-input.test.js`. Vitest will automatically detect and tests files with the file name patterns `**/*.test.js`.

As mentioned earlier, the default environment for Vitest is Node, and by adding a `@vitest-environment` comment at the top of the file, it specifies another environment to be used for all tests in that file:
```
/**
 * @vitest-environment jsdom
 */
```
Voilà, the file is ready for testing.

##  First simple input test

This section describes creating the most basic test for an input component. 

Here is the Input component used in a Riot application, without value and props:
```html
<c-input/>
```

The generated HTML for the component is:
```html
<div class="field border">
  <input type="text">
</div>
```
The test for the Input component:
```js
/**
 * @vitest-environment jsdom
 */

import { assert, describe, it } from 'vitest'
import * as riot from 'riot'

import cInput from '../components/c-input.riot'

describe('Component c-input', () => {
    it('should render the input without props', () => {
        riot.register('c-input', cInput);
        const [component] = riot.mount(document.createElement('div'), {}, 'c-input')
        assert.strictEqual(component.root.querySelector('div').className.trim(), 'field border');
        assert.strictEqual(component.root.querySelector('input').type, 'text');
        assert.strictEqual(component.root.querySelector('input').value, '');
        riot.unregister('c-input');

   })
})
```
Code Breakdown:
1. The input component and modules are loaded.
2. Vitest provides common utilities for testing, similar to Mocha and Jest:
   * **describe()** is used to define a group of tests
   * **it()** for defining a test
   * **assert()** for validating tests
3. To use the component, it must be registered globally with [riot.register()](https://riot.js.org/api/#riotregister). It takes two arguments:
   * First: The component name
   * Second: The component wrapper 
4. To load the component into the Dom, it must be mounted with [riot.mount()](https://riot.js.org/api/#riotmount). It takes three arguments:
   * First, a selector selects elements from the page and mounts them with a custom component. In our case, it creates a new `div` element and returns a selector.
   * Second: An optional object is passed for the component to consume, such as an Input value, error, helper, label, and more.
   * Third: The component name we want to mount, in our case, `c-input`.
5. The mount function returns an array of all mounted components on the Dom. As in our case, we have only one, we are destructuring the array to get our [component object](https://riot.js.org/api/#component-object).
6. Thanks to the querySelector, we can access all attributes, tags, and values from the component object. For instance, we can retrieve the div element: `component.root.querySelector('div')`. Here is what you can verify: 
  * **Input value**: `component.root.querySelector('input').value`
  * **Class names**: `component.root.querySelector('input').className`
  * **Type**: `component.root.querySelector('input').type` 
  * **Content of a tag**: `component.root.querySelector('input').textContent`
4. Verify all expected results with the expression `assert.strictEqual(result, expected)`.
5. Finally, unregister the input tag `c-input` with [riot.unregister()](https://riot.js.org/api/#riotunregister). This method is required to create another test with the same tag name.

> 👉 Tips: for creating the test, I used `console.log(component.root.innerHTML)`, which gives the raw HTML of component


Now execute the test through the NPM command:
```bash
npm run test
```
The result on the console:
```bash
 ✓ tests/c-input.jsdom.test.js
   ✓ Component c-input 
     ✓ should render the input without props

 Test Files  1 passed 
      Tests  1 passed
   Start at  14:10:32
   Duration  36ms
```
✅ The test succeeds; everything is good. Vitest is listening to changes, and it will print the result when a new test is created.

## Advanced Test

Now we can replicate this testing method for all props and multiple props combined: Let's create a password input with a "Password" type, label, a value, and an error.

Here is the Input component that will be created for the Riot Application:
```html
<c-input label="Passport" type="passport" value="1234" error="The password is too show, minimum 20 characters." />
```

The generated HTML for the component is:
```html
<div class=" field border invalid label">
  <input type="password">
  <label>Password</label>
  <span class="error">The password is too show, minimum 20 characters.</span>
</div>
```
Here is the corresponding test to register the component, mount it with all properties, and verify each HTML tag and attribute:
```js
it('should render multiple props: label, type, error and round', () => {
   riot.register('c-input', cInput);
   const _props =  { value: "1234", label: "Password", type: "password", error: "The password is too show, minimum 20 characters." }
   const [component] = riot.mount(document.createElement('div'), _props, 'c-input')

   const divElement = component.root.querySelector('div')
   assert.strictEqual(divElement.className.replace(/\s+/g,' ').trim(), 'field border invalid label');

   const inputElement = component.root.querySelector('input')
   assert.strictEqual(inputElement.value, _props.value);
   assert.strictEqual(inputElement.type, _props.type);

   const labelElement = component.root.querySelector('label')
   assert.strictEqual(labelElement.textContent, _props.label);

   const spanElement = component.root.querySelector('span')
   assert.strictEqual(spanElement.textContent, _props.error);
   assert.strictEqual(spanElement.className, 'error');        
   riot.unregister('c-input');
})
```
Code Breakdown:
1. I will not describe what I mentioned on the first test above ⬆️
2. The Component logic is printing tags and attributes conditionally, and we must check every element.
3. Instead of calling multiple times `querySelector('span')`, the result is stored in a variable to re-use it for each assert expression.
4. Checking the **className** requires to remove all extra whitespace with `.replace(/\s+/g,' ').trim()`. The component has conditions to add a class; if a class does not exist, it will leave a space character. 

The test pass ✅

Find all Riot tests with Vitest and JsDom the following GitHub repository: https://github.com/steevepay/riot-beercss/blob/main/tests/c-input.jsdom.test.js

## Conclusion

Combining Riot with Vitest and JsDom is a good solution for testing the rendering of Riot Components in a Browser environment. It requires knowledge about manipulating HTML elements with Javascript and is a bit verbose. This method also allows for testing component reactivity with events, input, key type, and more.

I covered another [testing method with Riot-SSR in a Node environment](https://dev.to/steeve/riot-component-unit-test-with-vitest-node-env-hk0), to compare the two solutions:
* **For fast, no-brain but limited Riot testing:** use a Node Server Environment with Riot-SSR
* **For extensive but verbose Riot testing:** use a JsDom environment

Feel free to comment if you have questions or need help about RiotJS.

Have a great day! Cheers 🍻