DEV Community

Matt Levy for Ficus

Posted on

Unit testing web components with ava and jsdom

Unit testing web components is tricky. The customElements API is required (or a polyfill of it) to create web component instances for testing.

Due to this friction and complication, I typically use an E2E testing framework like Cypress which runs tests against a real browser like headless Chrome.

Whilst an E2E framework is a fantastic tool for testing web components, I've wanted a suite of unit tests like I have for a back-end API - a comprehensive suite of fast tests for validating web components.

With Node v14, this can now be achieved through the native support for ES modules.

This post provides an example of how to set-up web component unit testing using ava and jsdom.

A repo containing the source code for this example is available at https://github.com/ducksoupdev/ficusjs-ava-test.

Dependencies

The following dependencies are used for this example.

Ava

Ava is a lightweight, fast test runner for NodeJS.

Jsdom

Jsdom is a pure Javascript implementation of HTML and DOM. Starting with v16, jsdom provides a customElements API implementation.

Set-up

Firstly, initialize a new NodeJS project to create a package.json file using npm init.

Copy the following properties to the package.json file.

{
  "scripts": {
    "test": "ava"
  },
  "devDependencies": {
    "@ficusjs/renderers": "^3.1.0",
    "ava": "^3.15.0",
    "ficusjs": "^3.2.3",
    "jsdom": "^16.5.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

Secondly, create an ES module (.mjs) test file at test/component.spec.mjs.

Copy the following to the file.

import { JSDOM } from 'jsdom'
import test from 'ava'

test.before(t => {
  const dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>')
  globalThis.window = dom.window
  globalThis.document = dom.window.document
  globalThis.customElements = window.customElements
  globalThis.HTMLElement = window.HTMLElement
})
Enter fullscreen mode Exit fullscreen mode

This contains the basic structure of a unit test:

  • Imports jsdom and ava
  • Initializes a new DOM and exposes global properties required during testing

Create a web component

Create an ES module file at src/component.mjs and copy the following contents into it.

import { createComponent } from 'ficusjs'
import { html, renderer } from '@ficusjs/renderers'

createComponent('basic-comp', {
  renderer,
  render () {
    return html`<p>Basic component</p>`
  }
})
Enter fullscreen mode Exit fullscreen mode

The example is a web component created using FicusJS and the uhtml renderer. It is a basic example that renders a <p> tag internally.

Writing a test

Lets test the component.

Open the ES module test file test/component.spec.mjs.

Copy the following to the file after the existing content.

test('render basic component', async t => {
  await import('../src/component.mjs')
  const document = globalThis.document
  const basicComp = document.createElement('basic-comp')
  document.body.appendChild(basicComp)
  t.is(document.querySelector('basic-comp p').textContent, 'Basic component')
})
Enter fullscreen mode Exit fullscreen mode

The following is happening in this test:

  • Import the component ready to test
  • Create a new instance of the component and append it to the <body>
  • Assert that the component renders the <p> tag internally

The component is imported within the test and not at the top of the file because the global properties must be initialized for the component to work. Importing the component during the test means that the required global properties are available for the component to register itself with the customElements API.

Run the test

To run the test, type npm test.

alt text

If all is successful, you should see the above output from ava.

Summary

The opportunity to unit test web components this way is exciting. However, there are some caveats with this approach that might make it difficult in certain scenarios.

  1. It follows NodeJS ES module guidelines that may not be compatible with the browser.
  2. Files must be named with the .mjs extension or provide a package.json top level property type of value module if the extension is .js.
  3. Importing libraries from a URL is not natively supported but can be achieved through a custom HTTPS loader.

ES module support in NodeJS is relatively new and I'm sure will get better in time.

For applications that use NodeJS build tools such as Rollup or Webpack, this could provide a unit testing approach to validate web components bundled together.

Top comments (2)

Collapse
 
ducksoupdev profile image
Matt Levy

This approach utilises native ES modules which means you can share the browser code with NodeJS without transpilation. I believe it makes it better as it reduces the complexity. Currently Jest does not support native ES modules although this will probably change in the future.

Collapse
 
sherlockcodes profile image
Lucas Miguel

How does this compare with something like Jest? Do you find it better to use?