Reputation: 12252
I would like to test some custom web components and use jest.js as test runner (due to its support for ES6).
Chromium supports commands like
window.customElements.define('my-custom-element', MyCustomElementClass);
to register a custom web component.
However, window.customElements
does not seem to be known in the context of jest tests.
As a work around I tried to use jest in combination with puppeteer and express to run the customElements
part in Chromium.
However, I have difficulties to inject the custom element class TreezElement
in the evaluated code:
treezElement.js:
class TreezElement extends HTMLElement {
connectedCallback () {
this.innerHTML = 'Hello, World!';
}
}
treezElement.test.js:
import TreezElement from '../../src/components/treezElement.js';
import puppeteer from 'puppeteer';
import express from 'express';
describe('Construction', ()=>{
let port = 3000;
let browser;
let page;
let element;
const width = 800;
const height = 800;
beforeAll(async () => {
const app = await express()
.use((req, res) => {
res.send(
`<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
</html>`
)
})
.listen(port);
browser = await puppeteer.launch({
headless: false,
slowMo: 80,
args: [`--window-size=${width},${height}`]
});
var pages = await browser.pages();
page = pages[0];
await page.setViewport({ width, height });
await page.goto('http://localhost:3000');
element = await page.evaluate(({TreezElement}) => {
console.log('TreezElement:')
console.log(TreezElement);
window.customElements.define('treez-element', TreezElement);
var element = document.create('treez-element');
document.body.appendChild(element);
return element;
}, {TreezElement});
});
it('TreezElement', ()=>{
});
afterAll(() => {
browser.close();
});
});
Maybe TreezElement
is not serializable and therefore undefined
is passed to the function.
If I try to import the custom element class TreezElement
directly from within the evaluated code ...
element = await page.evaluate(() => {
import TreezElement from '../../src/components/treezElement.js';
console.log('TreezElement:')
console.log(TreezElement);
window.customElements.define('treez-element', TreezElement);
var element = document.create('treez-element');
document.body.appendChild(element);
return element;
});
... I get the error
'import' and 'export' may only appear at the top level
=> What is the recommended way to test custom web components with jest?
Some related stuff:
Upvotes: 12
Views: 23569
Reputation: 23544
JSDOM 16.2 includes basic support for custom elements and is available in Jest 26.5 and above. Here's a simple Jest test that shows it working:
customElements.define('test-component', class extends HTMLElement {
constructor() {
super();
const p = document.createElement('p')
p.textContent = 'It works!'
this.appendChild(p)
}
})
test('custom elements in JSDOM', () => {
document.body.innerHTML = `<h1>Custom element test</h1> <test-component></test-component>`
expect(document.body.innerHTML).toContain('It works!')
})
Output:
$ jest
PASS ./test.js
✓ custom elements in JSDOM (11 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.409 s
Ran all test suites.
Note not all features are supported yet, notably shadow DOM does not work.
Upvotes: 6
Reputation: 41
I have created a DOM that supports server side rendering of web components. It also supports testing web components with Jest.
DOM:
https://www.npmjs.com/package/happy-dom
Jest environment:
https://www.npmjs.com/package/jest-environment-happy-dom
To install it
npm install jest-environment-happy-dom --save-dev
To use it:
Edit your package.json to include the Jest environment:
{
"scripts": {
"test": "jest --env=jest-environment-happy-dom"
}
}
Edit:
The name of package has been changed to "@happy-dom/jest-environent"
Upvotes: 4
Reputation: 407
use electron runner can include all node and chrome env, use this to replace jsdom
https://github.com/facebook-atom/jest-electron-runner
Upvotes: 2
Reputation: 12252
Another (limited) approach is to use Object.create
as a work around to create an instance of the custom web component, without using window.customElements.define
and document.createElement(..)
:
import TreezElement from '../../src/components/treezElement.js';
var customElement = Object.create(TreezElement.prototype);
This way the instance methods can be tested directly in jest and the tests are included in the code coverage, too. (Also coverage issues of my other answer regarding puppeteer.)
A major disadvantage is that only the methods can be accessed, not the properties. If I try to use customElement.customProperty I get : TypeError: Illegal invocation
.
This is caused by the check !module.exports.is(this)
in Element.js:
Element.prototype.getAttribute = function getAttribute(qualifiedName) {
if (!this || !module.exports.is(this)) {
throw new TypeError("Illegal invocation");
}
...
Element.prototype.setAttribute = function setAttribute(qualifiedName,value){
if (!this || !module.exports.is(this)) {
throw new TypeError("Illegal invocation");
}
Another disadvantage of Object.create
is that the constructor code is not called and not included in the test coverage.
If the command window.customElements.define(..)
is directly included in a class file that we would like to import (e.g. treezElement.js) ... the customElements
property needs to be mocked before including the import:
customElementsMock.js:
export default class CustomElementsMock{} //dummy export
//following command mocks the customElements feature to be able
//to import custom elements in jest tests
window.customElements = {
define: (elementName, elementClass)=>{
console.log('Mocked customElements.define(..) for custom element "' + elementName + '"');
}
};
usage in treezElement.test.js:
import CustomElementsMock from '../customElementsMock.js';
import TreezElement from '../../src/components/treezElement.js';
var customElement = Object.create(TreezElement.prototype);
//...
(I also tried to put the mock code directly at the beginning of treezElement.test.js
but all the imports are executed prior to the script doing the import. That is why I had to put the mocking code in an extra file.)
Upvotes: 0
Reputation: 12252
Here is an ugly version that kind of works. Some further notes on this:
express.js is configured to work as file server. Otherwise there would be issues with mime type or cross origin checking for the imported ES6 modules.
The class TreezElement
is not directly imported but using the work around of creating an extra script tag
There are issues with instance methods regarding the code coverage. It does not seem to be possible to directly call the constructor of TreezElement
(inherits from HTMLElement, => illegal constructor
). An instance of the element class can only be created with document.createElement(...)
in puppeteer. Therefore, all the instance methods cannot be tested in Jest but only static methods. The instance methods and properties can be tested in puppeteer. However, the code coverage of jest does not consider the code coverage of puppeteer.
The created element of type TreezElement
can be returned in form of an ElementHandle. Accessing the properties and methods of the element instance is quite cumbersome (see example below). As an alternative to the handle approach, the page.$eval
method can be applied:
var id = await page.$eval('#custom-element', element=> element.id);
index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="root"></div>
</body>
</html>
treezElement.test.js
import TreezElement from '../../src/components/treezElement.js';
import puppeteer from 'puppeteer';
import express from 'express';
describe('Construction', ()=>{
let port = 4444;
const index = Math.max(process.argv.indexOf('--port'), process.argv.indexOf('-p'))
if (index !== -1) {
port = +process.argv[index + 1] || port;
}
var elementHandle;
beforeAll(async () => {
const fileServer = await express()
.use(express.static('.'))
.listen(port);
var browser = await puppeteer.launch({
headless: false,
slowMo: 80,
userDataDir: '.chrome',
args: ['--auto-open-devtools-for-tabs']
});
var pages = await browser.pages();
var page = pages[0];
await page.goto('http://localhost:'+port + '/test/index.html');
await page.evaluate(() => {
var script = document.createElement('script');
script.type='module';
script.innerHTML="import TreezElement from '../src/components/treezElement.js';\n" +
"window.customElements.define('treez-element', TreezElement);\n" +
"var element = document.createElement('treez-element');\n" +
"element.id='custom-element';\n" +
"document.body.appendChild(element);";
document.head.appendChild(script);
});
elementHandle = await page.evaluateHandle(() => {
return document.getElementById('custom-element');
});
});
it('id', async ()=>{
var idHandle = await elementHandle.getProperty('id');
var id = await idHandle.jsonValue();
expect(id).toBe('custom-element');
});
afterAll(() => {
browser.close();
});
});
Upvotes: 0