我想测试一些自定义 Web 组件并使用 jest.js 作为测试运行器(因为它支持 ES6)。
Chromium 支持像这样的命令
window.customElements.define('my-custom-element', MyCustomElementClass);
注册自定义 Web 组件。
然而,
window.customElements
在笑话测试的上下文中似乎并不为人所知。
作为解决方法,我尝试将 jest 与 puppeteer 和 express 结合使用,以在 Chromium 中运行
customElements
部分。
但是,我很难在评估代码中注入自定义元素类
TreezElement
:
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();
});
});
也许
TreezElement
不可序列化,因此 undefined
被传递给函数。
如果我尝试直接从评估代码中导入自定义元素类
TreezElement
...
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;
});
...我得到错误
'import' 和 'export' 只能出现在顶层
=> 使用 jest 测试自定义 Web 组件的推荐方法是什么?
一些相关的东西:
JSDOM 16.2 包括对自定义元素的基本支持,并且在 Jest 26.5 及更高版本中可用。这是一个简单的 Jest 测试,表明它有效:
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!')
})
输出:
$ 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.
注意并非所有功能都受支持,特别是shadow DOM 不工作.
我创建了一个支持 Web 组件服务器端渲染的 DOM。它还支持使用 Jest 测试 Web 组件。
DOM:
https://www.npmjs.com/package/happy-dom
开玩笑的环境:
https://www.npmjs.com/package/jest-environment-happy-dom
安装它
npm 安装 jest-environment-happy-dom --save-dev
使用方法:
编辑您的 package.json 以包含 Jest 环境:
{
"scripts": {
"test": "jest --env=jest-environment-happy-dom"
}
}
编辑:
包的名称已更改为“@happy-dom/jest-environent”
使用 electron runner 可以包含所有节点和 chrome 环境, 用它来代替 jsdom
这是一个丑陋的版本。关于此的一些进一步说明:
express.js 配置为 文件服务器。否则,导入的 ES6 模块的 MIME 类型或跨源检查会出现问题。
类
TreezElement
不是直接导入的,而是使用创建额外脚本标签关于代码覆盖率的实例方法存在问题。好像不能直接调用
TreezElement
的构造函数(继承自HTMLElement,=>illegal constructor
)。元素类的实例只能在puppeteer中用document.createElement(...)
创建。因此,在 Jest 中不能测试所有实例方法,只能测试静态方法。可以在 puppeteer 中测试实例方法和属性。但是,jest的代码覆盖率没有考虑puppeteer的代码覆盖率。 创建的
TreezElement
类型的元素可以以ElementHandle的形式返回。 访问元素实例的属性和方法非常麻烦(见下面的例子)。作为手柄方法的替代方法,可以应用page.$eval
方法:
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();
});
});
另一种(有限的)方法是使用
Object.create
作为变通方法来创建自定义 Web 组件的实例,而不使用 window.customElements.define
和 document.createElement(..)
:
import TreezElement from '../../src/components/treezElement.js';
var customElement = Object.create(TreezElement.prototype);
这样实例方法可以直接测试开玩笑,测试也包含在代码覆盖率中。 (还有我关于木偶师的其他答案的报道问题。)
一个主要缺点是只能访问方法,不能访问属性。如果我尝试使用 customElement.customProperty 我得到:
TypeError: Illegal invocation
.
这是Element.js中的检查
!module.exports.is(this)
导致的:
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");
}
Object.create
的另一个缺点是构造函数代码不被调用,不包含在测试覆盖率中。
如果命令
window.customElements.define(..)
直接包含在我们要导入的类文件中(例如 treezElement.js)...在包含导入之前需要模拟 customElements
属性:
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 + '"');
}
};
在 treezElement.test.js 中的用法:
import CustomElementsMock from '../customElementsMock.js';
import TreezElement from '../../src/components/treezElement.js';
var customElement = Object.create(TreezElement.prototype);
//...
(我也尝试将模拟代码直接放在
treezElement.test.js
的开头,但是 所有导入都在执行导入的脚本之前执行。这就是为什么我不得不将模拟代码放在一个额外的文件中。)