问题描述
我有一个基于浏览器的小型游戏,我正在尝试启动并运行 Jest。
我的目标是能够编写测试,并让它们与 Jest 一起运行,并且没有任何额外的 DOM 或浏览器 API 相关的错误消息。
由于游戏使用了 DOM 和画布,我需要一个解决方案,我可以在其中手动模拟它们,或者让 Jest 为我处理。至少,我想验证“数据模型”和我的逻辑是否正常。
我也在使用 ES6 模块。
这是我迄今为止尝试过的:
- 尝试运行笑话:
Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse,e.g. it's not plain JavaScript.
By default,if Jest sees a Babel config,it will use that to transform your files,ignoring "node_modules".
Here's what you can do:
• If you are trying to use ECMAScript Modules,see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
• To have some of your "node_modules" files transformed,you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html
Details:
/home/dingo/code/game-sscce/game.spec.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import { Game } from './game';
^^^^^^
SyntaxError: Cannot use import statement outside a module
我在这里了解到,我可以通过实验启用 ES 模块支持,或者使用转译器输出 Jest 可以识别和运行的 ES5。
所以我的选择是:
- 启用实验性 ES 模块支持
- 使用 Babel 进行转译
- 使用 Parcel 进行转译
- 使用 Webpack 进行转译
我决定尝试使用 Babel 并查看此处的说明:https://jestjs.io/docs/en/getting-started#using-babel
- 我在根目录中创建了一个 babel.config.js 文件。
安装 babel 并创建配置文件后,这是一个 SSCCE:
babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env'
]
],};
game.js
export class Game {
constructor() {
document.getElementById('gameCanvas').width = 600;
}
}
new Game();
game.spec.js
import { Game } from './game';
test('instantiates Game',() => {
expect(new Game()).toBeDefined();
});
index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<script type="module" src="game.js" defer></script>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" />
</div>
</body>
</html>
package.json
{
"name": "game-sscce","version": "1.0.0","scripts": {
"test": "jest"
},"devDependencies": {
"@babel/core": "^7.12.13","@babel/preset-env": "^7.12.13","babel-jest": "^26.6.3","jest": "^26.6.3"
}
}
现在当我再次尝试运行 Jest 时,我得到:
FAIL ./game.spec.js
● Test suite failed to run
TypeError: Cannot set property 'width' of null
1 | export class Game {
2 | constructor() {
> 3 | document.getElementById('gameCanvas').width = 600;
| ^
4 | }
5 | }
6 |
at new Game (game.js:3:5)
at Object.<anonymous> (game.js:7:1)
at Object.<anonymous> (game.spec.js:1:1)
...现在,我不知道该怎么做。如果文档没有被识别,那么我怀疑 Jest 没有正确使用 jsdom。我应该配置其他东西吗?
解决方法
调查:
Jest runs with jsdom by default。
document
确实存在:
然而,由于它被模拟,getElementById()
只返回 null
。
在这种情况下,无法返回 HTML 文档中定义的现有画布。相反,可以通过编程方式创建画布:
game.js
export class Game {
constructor() {
const canvas = document.createElement('canvas');
canvas.setAttribute('id','gameCanvas');
document.getElementById('gameContainer').append(canvas);
canvas.width = 600;
}
}
new Game();
但是,getElementById()
仍会返回 null
,因此必须模拟此调用:
game.spec.js
import { Game } from './game';
test('instantiates Game',() => {
jest.spyOn(document,'getElementById').mockReturnValue({})
expect(new Game()).toBeDefined();
});
测试仍然无法运行:
FAIL ./game.spec.js
● Test suite failed to run
TypeError: Cannot read property 'append' of null
3 | const canvas = document.createElement('canvas');
4 | canvas.setAttribute('id','gameCanvas');
> 5 | document.getElementById('gameContainer').append(canvas);
| ^
6 |
7 | canvas.width = 600;
8 |
at new Game (game.js:5:5)
at Object.<anonymous> (game.js:16:1)
at Object.<anonymous> (game.spec.js:1:1)
这是因为 Game
在 Jest 导入后立即实例化自身,这是由于最后一行的 new Game()
调用。一旦摆脱它:
game.js
export class Game {
constructor() {
const canvas = document.createElement('canvas');
canvas.setAttribute('id','gameCanvas');
document.getElementById('gameContainer').append(canvas);
canvas.width = 600;
}
}
我们得到:
FAIL ./game.spec.js
✕ instantiates Game (7 ms)
● instantiates Game
TypeError: document.getElementById(...).append is not a function
3 | const canvas = document.createElement('canvas');
4 | canvas.setAttribute('id','gameCanvas');
> 5 | document.getElementById('gameContainer').append(canvas);
| ^
6 |
7 | canvas.width = 600;
8 |
at new Game (game.js:5:46)
at Object.<anonymous> (game.spec.js:5:10)
更近一步,但还必须模拟 append()
调用:
game.spec.js
import { Game } from './game';
test('instantiates Game','getElementById').mockReturnValue({
append: jest.fn().mockReturnValue({})
});
expect(new Game()).toBeDefined();
});
...现在测试通过了:
PASS ./game.spec.js
✓ instantiates Game (9 ms)
Test Suites: 1 passed,1 total
Tests: 1 passed,1 total
有趣的是,jsdom 在以编程方式创建和模拟时返回一个 HTMLCanvasElement:
然而,它不能真正用于任何事情:
game.js
export class Game {
constructor() {
const canvas = document.createElement('canvas');
canvas.setAttribute('id','gameCanvas');
document.getElementById('gameContainer').append(canvas);
canvas.width = 600;
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(200,0)';
ctx.fillRect(10,10,50,50);
ctx.fillStyle = 'rgba(0,200,0.5)';
ctx.fillRect(30,30,50);
}
}
如失败的测试所示:
FAIL ./game.spec.js
✕ instantiates Game (43 ms)
● instantiates Game
TypeError: Cannot set property 'fillStyle' of null
10 | var ctx = canvas.getContext('2d');
11 |
> 12 | ctx.fillStyle = 'rgb(200,0)';
| ^
13 | ctx.fillRect(10,50);
14 |
15 | ctx.fillStyle = 'rgba(0,0.5)';
at new Game (game.js:12:5)
at Object.<anonymous> (game.spec.js:7:10)
console.error
Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)
at module.exports (/home/dingo/code/game-sscce/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17)
at HTMLCanvasElementImpl.getContext (/home/dingo/code/game-sscce/node_modules/jsdom/lib/jsdom/living/nodes/HTMLCanvasElement-impl.js:42:5)
at HTMLCanvasElement.getContext (/home/dingo/code/game-sscce/node_modules/jsdom/lib/jsdom/living/generated/HTMLCanvasElement.js:130:58)
at new Game (/home/dingo/code/game-sscce/game.js:10:22)
at Object.<anonymous> (/home/dingo/code/game-sscce/game.spec.js:7:10)
at Object.asyncJestTest (/home/dingo/code/game-sscce/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:106:37)
at /home/dingo/code/game-sscce/node_modules/jest-jasmine2/build/queueRunner.js:45:12
at new Promise (<anonymous>)
at mapper (/home/dingo/code/game-sscce/node_modules/jest-jasmine2/build/queueRunner.js:28:19)
at /home/dingo/code/game-sscce/node_modules/jest-jasmine2/build/queueRunner.js:75:41 undefined
8 | canvas.width = 600;
9 |
> 10 | var ctx = canvas.getContext('2d');
| ^
11 |
12 | ctx.fillStyle = 'rgb(200,0)';
13 | ctx.fillRect(10,50);
为了能够进一步测试,必须满足以下两个条件之一:
- canvas 必须安装为 jsdom 的对等依赖项,
- jest-canvas-mock 必须安装。