Unit testing web components with ava and jsdom
Tags: webdev, testing, webcomponents
Published: 2021-04-11 17:02:06

Unit testing web components is tricky. The [`customElements`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry) 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](https://www.cypress.io/) 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](https://nodejs.org/api/packages.html#packages_determining_module_system).

This post provides an example of how to set-up web component unit testing using [ava](https://github.com/avajs/ava) and [jsdom](https://github.com/jsdom/jsdom).

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

## Dependencies

The following dependencies are used for this example.

### Ava

[Ava](https://github.com/avajs/ava) is a lightweight, fast test runner for NodeJS.

### Jsdom

[Jsdom](https://github.com/jsdom/jsdom) is a pure Javascript implementation of HTML and DOM. Starting with v16, jsdom provides a [`customElements`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry) 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.

```json
{
  "scripts": {
    "test": "ava"
  },
  "devDependencies": {
    "@ficusjs/renderers": "^3.1.0",
    "ava": "^3.15.0",
    "ficusjs": "^3.2.3",
    "jsdom": "^16.5.2"
  }
}
```

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

Copy the following to the file.

```js
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
})
```

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.

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

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

The example is a web component created using [FicusJS](https://www.npmjs.com/package/ficusjs) and the [uhtml](https://www.npmjs.com/package/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.

```js
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')
})
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62rt8r5z9qn680j82yr5.png)

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](https://nodejs.org/api/packages.html#packages_determining_module_system) 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](https://nodejs.org/api/esm.html#esm_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.