Riot Component Unit Test with Vitest (JsDom env)

Steeve - Mar 22 - - Dev Community

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.

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.

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:

npm install -D vitest jsdom
Enter fullscreen mode Exit fullscreen mode

To execute the test, add the following section to your package.json:

{
  "scripts": {
    "test": "vitest"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
 */
Enter fullscreen mode Exit fullscreen mode

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:

<c-input/>
Enter fullscreen mode Exit fullscreen mode

The generated HTML for the component is:

<div class="field border">
  <input type="text">
</div>
Enter fullscreen mode Exit fullscreen mode

The test for the Input component:

/**
 * @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');

   })
})
Enter fullscreen mode Exit fullscreen mode

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(). 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(). 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.
  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
  7. Verify all expected results with the expression assert.strictEqual(result, expected).
  8. Finally, unregister the input tag c-input with riot.unregister(). 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:

npm run test
Enter fullscreen mode Exit fullscreen mode

The result on the console:

 ✓ 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
Enter fullscreen mode Exit fullscreen mode

✅ 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:

<c-input label="Passport" type="passport" value="1234" error="The password is too show, minimum 20 characters." />
Enter fullscreen mode Exit fullscreen mode

The generated HTML for the component is:

<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>
Enter fullscreen mode Exit fullscreen mode

Here is the corresponding test to register the component, mount it with all properties, and verify each HTML tag and attribute:

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');
})
Enter fullscreen mode Exit fullscreen mode

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, 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 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .