Testing Web Components
A write-up on testing web components.
by Lea Rosema
With the help of the customElements
API, developers for the web platform can now build their own custom html elements, bringing a zero-footprint approach to a component-based archtitecture to the web.
But what about testing?
anchorTesting might still be one of the reasons why people choose a framework like React rather than working with the native web components.
But it turns out testing web components is not too complex. All you need is a test runner of your choice and a DOM API. There you have several options:
- run the tests in a real browser, or a headless one
- use a console-based test runner with a (fake) DOM library
Using the node.js test runner
anchorAs I learned via the talk by Sebastian Schürmann, node.js comes with its own integrated test runner. By default, node --test
runs all files in the project which are suffixed with .test.js
, .test.cjs
and .test.mjs
.
For this article, I've created a test project with a password-input web component as a demo: wctest.
The test is using the describe
and it
-syntax which can be imported from "node:test"
.
In order to be able to run DOM APIs inside my test, I have to emulate it for my test environment. I'm using linkedom which is a fast implementation based on JSDON and linked lists.
It works like this:
import { parseHTML } from 'linkedom';
const {
window, document, customElements,
HTMLElement, Event, CustomEvent
} = parseHTML(`
<!doctype html>
<html lang="en">
<body>
</body>
</html>
`);
It even comes with the customElements
API so this works nearly out of the box.
The only tricky thing might be the fact I won't get global namespace pollution, which is normally a good thing, but my modules to be tested need them in the global namespace.
So, the workaround I'm using is to put the APIs consumed into the global namespace and then do a dynamic import of my module in the test setup:
describe('password-input component', ()=> {
let window, document, customElements, HTMLElement, DocumentFragment, PasswordInput, Event;
before(async () => {
window = global.window = parseHTML(`<!DOCTYPE html><html><head></head><body></body></html>`);
DocumentFragment = global.DocumentFragment = window.DocumentFragment;
document = global.document = window.document;
customElements = global.customElements = window.customElements;
Event = global.Event = window.Event;
HTMLElement = global.HTMLElement = window.HTMLElement;
const module = await import('./password-input.js');
PasswordInput = module.PasswordInput;
PasswordInput.register();
});
});
Alternative: jest
anchorA more common choice to test web components is to use jest. I haven't prepared a special demo for this article, but I did this in the past with my shader-art web component, which is for a lightweight embedding of shader animations into websites while respecting "reduced motion" settings of your operating system.
You can configure jest to use JSDOM which allows you to use DOM-APIs. One essential configuration option you have to specify in your jest.config.js
is testEnvironment: 'jsdom'
.
Additionally, you can also specify how jest would handle further things, which could also impact your test execution time. For testing canvas2d graphics, you can npm install canvas
and jsdom will use that to emulate the canvas API. That would even allow you to do some visual regression testing.
When emulating the canvas API in your test, you may also want to work with images. So, another setting that comes into play is resources: 'usable'
, which is configured in the testEnvironmentOptions
inside the jest.config.js
.
module.exports = {
testEnvironment: 'jsdom',
testEnvironmentOptions: {
resources: 'usable',
},
}
For my shader-art component, I chose to use a webgl mock for jest which you can install via NPM and specify it via setupFiles: ['jest-webgl-canvas-mock']
inside the jest.config.js
.
Further approaches?
anchorFor now, I decided for me that those above two approaches are enough for me for now. I'm would be excited to explore other approaches which may involve using a headless browser and/or cypress. If you have a demo project or want to talk about it with me, feel free to reach out. 💖