How can I setup Jest with a transpiler so that I can have automatically mocked DOM and canvas? - dom

I have a small, browser-based game that I'm trying to get Jest up and running with.
My goal is to be able to write tests, and to have them run with Jest, and not to have any extra DOM- or browser API-related error messages.
As the game makes use of DOM and canvas, I need a solution where I can either mock those manually, or have Jest take care of it for me. At a minimum, I'd like to verify that the 'data model' and my logic is sane.
I'm also making use of ES6 modules.
Here's what I've tried so far:
Tried running jest:
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
I understood here that I can experimentally enable ES module support, or use a transpiler to output ES5 that Jest can recognize and run.
So my options are:
Enable experimental ES module support
Transpile using Babel
Transpile using Parcel
Transpile using Webpack
I decided to try Babel and looked here for instructions: https://jestjs.io/docs/en/getting-started#using-babel
I created a babel.config.js file in the root directory.
After installing babel and creating a config file, here's an 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"
}
}
Now when I try running Jest again, I get:
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)
...and now, I'm not sure what to do. If document is not being recognized, then I suspect Jest is not making use of jsdom properly. Am I supposed to configure anything else?

Investigation:
Jest runs with jsdom by default.
document actually exists:
However, since it's mocked, getElementById() just returns null.
In this situation, it's not possible to return an existing canvas defined in the HTML document. Rather, one can create the canvas programmatically:
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() will, however, still return null, so this call must be mocked:
game.spec.js
import { Game } from './game';
test('instantiates Game', () => {
jest.spyOn(document, 'getElementById').mockReturnValue({})
expect(new Game()).toBeDefined();
});
The test still fails to run:
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)
This is because Game is instantiating itself as soon as Jest imports it due to the new Game() call on the last line. Once rid of that:
game.js
export class Game {
constructor() {
const canvas = document.createElement('canvas');
canvas.setAttribute('id', 'gameCanvas');
document.getElementById('gameContainer').append(canvas);
canvas.width = 600;
}
}
We get:
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)
One step closer, but the append() call must also be mocked out:
game.spec.js
import { Game } from './game';
test('instantiates Game', () => {
jest.spyOn(document, 'getElementById').mockReturnValue({
append: jest.fn().mockReturnValue({})
});
expect(new Game()).toBeDefined();
});
...and now the test passes:
PASS ./game.spec.js
✓ instantiates Game (9 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
It's interesting that jsdom returns an HTMLCanvasElement when created programmatically and mocked:
However, it can't really be used for anything:
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, 0)';
ctx.fillRect(10, 10, 50, 50);
ctx.fillStyle = 'rgba(0, 0, 200, 0.5)';
ctx.fillRect(30, 30, 50, 50);
}
}
As shown by the failing test:
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, 0)';
| ^
13 | ctx.fillRect(10, 10, 50, 50);
14 |
15 | ctx.fillStyle = 'rgba(0, 0, 200, 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, 0)';
13 | ctx.fillRect(10, 10, 50, 50);
To be able to test further, either of the following two conditions must be fulfilled:
canvas has to be installed as a peer dependency of jsdom,
jest-canvas-mock has to be installed.

Related

How to check for level in react-testing-library?

I have a heading <h4>Big offer!</h4> on the page, when I first ran my tests I got:
Expected: "Big offer!"
Received: <h4>Big offer!</h4>
35 | const switchToggle = screen.getByRole('checkbox');
36 | expect(switchToggle.checked).toEqual(true);
> 37 | expect(titleEl).toEqual(title);
Ok, it get's correct file, so I changed my code to actually check heading level and text, but it failed:
Expected: "Big offer!"
Received: undefined
35 | const switchToggle = screen.getByRole('checkbox');
36 | expect(switchToggle.checked).toEqual(true);
> 37 | expect(titleEl.name).toEqual(title);
| ^
38 | expect(titleEl.level).toEqual(4);
I always thought that .name is equal to getByText(). I commented out a line and tried checking for level, and it failed again:
Expected: 4
Received: undefined
36 | expect(switchToggle.checked).toEqual(true);
37 | //expect(titleEl.name).toEqual(title);
> 38 | expect(titleEl.level).toEqual(4);
I don't understand why my test cases failed. Code for test was:
const title = 'Big offer!';
render(<Component title={title}/>);
const titleEl = screen.getByRole('heading');
expect(titleEl).toEqual(title);
expect(titleEl.level).toEqual(4);
It wont work because screen.getByRole (or others querys) returns an HTMLElement (in your case returns a HTMLHeadingElement that inherits HTMLElement). And it doesnt have name or level properties. You can check more here.
To check the text rendered you should use:
expect(titleEl).toHaveTextContent(title);
And for h4 type check you just need to filter it on query:
const titleEl = screen.getByRole('heading', { level: 4 });
The full test:
it('test search input', async () => {
const title = 'Big offer!';
render(<SearchBox title={title} />);
const titleEl = screen.getByRole('heading', { level: 4 });
expect(titleEl).toBeInTheDocument();
expect(titleEl).toHaveTextContent(title);
});

how to set babel plugins and presets - react-app-rewired

i am new to babel and testing stuff in react so it would be really great if you can provide a little more context for the approaches you can suggest.
Thank you
The app was created using react-app-rewired everything works fine but when i run my test script react-app-rewired test it throws this error
SyntaxError: C:\Users\kishan\Documents\GitHub\kp\client\src\components\DateTest\index.js: Support for the experimental syntax 'jsx' isn't currently enabled (219:5):
217 |
218 | return (
> 219 | <div className="date-test-container">
| ^
220 | <input
221 | ref={inputRef}
222 | type="text"
Add #babel/preset-react (https://git.io/JfeDR) to the 'presets' section of your Babel config to enable transformation.
If you want to leave it as-is, add #babel/plugin-syntax-jsx (https://git.io/vb4yA) to the 'plugins' section to enable parsing.
Test file:
const React = require('react');
const {shallow} = require('enzyme')
const DateTest = require('./') // component needs to be tested
it('should render date test component and check current month',()=>{
// const wrapper = shallow(<DateTest />);
// const month = wrapper.find('span.monthName').text();
// expect(text).toEqual("March");
})
what i tried:
useBabelRc() from customize-cra can see the documentation here
Causes your .babelrc (or .babelrc.js) file to be used, this is especially useful if you'd rather override the CRA babel configuration and make sure it is consumed both by yarn start and yarn test (along with yarn build).
//inside my config-overrides.js
module.exports = override(
useBabelRc()
);
// inside my .babelrc
{
"presets": [ "#babel/preset-react"]
}
But getting the same error as above
please let me know if more information required

Please tell me how to write nuxt plugins 'printd'

During development, we implemented the page to print using Printd.
create plugins / printd.ts
import Vue from "vue";
import { Printd } from "printd";
Vue.use(Printd);
The 'plugins' part of nuxt.conifg.ts.
plugins: [
... ,
{ src: "~/plugins/printd", ssr: false }]
but the error is shown below
10:9 No overload matches this call.
Overload 1 of 2, '(plugin: PluginObject<unknown> | PluginFunction<unknown>, options?: unknown): VueConstructor<Vue>', gave the following error.
Argument of type 'typeof Printd' is not assignable to parameter of type 'PluginObject<unknown> | PluginFunction<unknown>'.
Property 'install' is missing in type 'typeof Printd' but required in type 'PluginObject<unknown>'.
Overload 2 of 2, '(plugin: PluginObject<any> | PluginFunction<any>, ...options: any[]): VueConstructor<Vue>', gave the following error.
Argument of type 'typeof Printd' is not assignable to parameter of type 'PluginObject<any> | PluginFunction<any>'.
Property 'install' is missing in type 'typeof Printd' but required in type 'PluginObject<any>'.
8 |
9 |
> 10 | Vue.use(Printd);
| ^
11 |
help me!!
This worked for me :
import Vue from "vue"
import { Printd } from "printd";
Vue.prototype.$Printd = new Printd();
Then you can access this.$Printd in your whole app.
Try adding: as any just like in the following.
import Vue from "vue";
import { Printd } from "printd";
Vue.use(Printd as any);
This might be your answer. But if that does not work, try this.
import Vue from "vue";
const Printd = require("printd").Printd;
Vue.use(Printd);
... And if that still does not work, try this.
file: ~/types/index.d.ts
declare module "printd"

Ionic runtime error cannot read property "toLowerCase" of null

I am trying to view an app via "ionic serve" but I get this error. I have looked at other subjects but have never seen a runtime error. I tried all of their solutions but I would still have the same error page. Here is what I see:
And to be honest, I am not the coder of this app, but I have bought it from a store. But, I know some here and there. If there is a simple solution to fix this, that would be amazing!
The first error link is pointing to here:
_this.initialData();
var operating_system = '';
var admob = {};
if (_this.device.platform.toLowerCase() == 'android') {
operating_system = 'android';
admob = {
banner: settings['admob_android_banner'],
interstitial: settings['admob_android_interstitial']
};
}
The second error link is pointing me to here:
SafeSubscriber.prototype.__tryOrUnsub = function (fn, value) {
try {
fn.call(this._context, value);
}
catch (err) {
this.unsubscribe();
throw err;
}
};
The third error is pointing to here:
SafeSubscriber.prototype.next = function (value) {
if (!this.isStopped && this._next) {
var _parentSubscriber = this._parentSubscriber;
if (!_parentSubscriber.syncErrorThrowable) {
this.__tryOrUnsub(this._next, value);
}
else if (this.__tryOrSetError(_parentSubscriber, this._next, value)) {
this.unsubscribe();
}
}
The fourth error is pointing to here:
Subscriber.prototype._next = function (value) {
this.destination.next(value);
};
The fifth error is pointing to here:
var /** #type {?} */ response = new Response(responseOptions);
response.ok = isSuccess(status);
if (response.ok) {
responseObserver.next(response);
// TODO(gdi2290): defer complete if array buffer until done
responseObserver.complete();
return;
}
Here is the code I get in cmd:
C:\Users\xx-nj\Desktop\DukhanApp\DukhanColor>ionic serve
Starting app-scripts server: --address 0.0.0.0 --port 8100 --livereload-port 35729 --dev-logger-port 53703 --nobrowser -
Ctrl+C to cancel
[20:02:52] watch started ...
[20:02:52] build dev started ...
[20:02:53] clean started ...
[20:02:53] clean finished in 3 ms
[20:02:53] copy started ...
[20:02:53] deeplinks started ...
[20:02:53] deeplinks finished in 573 ms
[20:02:53] transpile started ...
[20:03:01] transpile finished in 7.70 s
[20:03:01] preprocess started ...
[20:03:01] preprocess finished in 1 ms
[20:03:01] webpack started ...
[20:03:01] copy finished in 8.80 s
[20:03:09] webpack finished in 8.01 s
[20:03:09] sass started ...
Without `from` option PostCSS could generate wrong source map and will not find Browserslist config. Set it to CSS file path or to `undefined` to prevent this warning.
[20:03:13] sass finished in 3.39 s
[20:03:13] postprocess started ...
[20:03:13] postprocess finished in 14 ms
[20:03:13] lint started ...
[20:03:13] build dev finished in 20.14 s
[20:03:13] watch ready in 20.44 s
[20:03:13] dev server running: http://localhost:8100/
[OK] Development server running!
Local: http://localhost:8100
External: http://192.168.8.111:8100
DevApp: celltore_ionic3#8100 on NajmLaptop
[20:03:23] tslint: C:/Users/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/contactus/contactus.ts, line: 124
'marker' is declared but never used.
L123: let map = new google.maps.Map(element, mapOptions);
L124: let marker = new google.maps.Marker({
L125: title: this.textStatic['cellstore_contact_us_title'],
[20:03:23] tslint: ...s/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/detailcategory/detailcategory.ts, line: 138
Duplicate variable: 'filter'
L138: for (var filter in this.filter['valueCustom']) {
L139: let attr = this.filter['value'][filter];
[20:03:23] tslint: ...s/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/detailcategory/detailcategory.ts, line: 140
Duplicate variable: 'option'
L139: let attr = this.filter['value'][filter];
L140: if (attr && Object.keys(attr).length > 0) for (var option in attr) {
L141: if(option != 'select' && attr[option]) {
[20:03:23] tslint: C:/Users/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/brand/brand.ts, line: 121
Duplicate variable: 'filter'
L121: for (var filter in this.filter['valueCustom']) {
L122: let attr = this.filter['value'][filter];
[20:03:23] tslint: C:/Users/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/brand/brand.ts, line: 123
Duplicate variable: 'option'
L122: let attr = this.filter['value'][filter];
L123: if (attr && Object.keys(attr).length > 0) for (var option in attr) {
L124: if(option != 'select' && attr[option]) {
[20:03:23] tslint: C:/Users/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/home/home.ts, line: 300
Duplicate variable: 'diff'
L299: element[i]["due_date"] = false;
L300: var diff = (start.getTime() - today.getTime()) / 1000;
L301: element[i]['time_diff'] = Math.floor(diff);
[20:03:23] tslint: C:/Users/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/home/home.ts, line: 165
'remaining_time' is declared but never used.
L164: ngOnInit() {
L165: var remaining_time = setInterval(() => {
L166: if (this.navCtrl.getActive().component.name == "HomePage") {
[20:03:23] tslint: C:/Users/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/search/search.ts, line: 213
Duplicate variable: 'filter'
L213: for (var filter in this.filter['valueCustom']) {
L214: let attr = this.filter['value'][filter];
[20:03:23] tslint: C:/Users/xx-nj/Desktop/DukhanApp/DukhanColor/src/pages/search/search.ts, line: 215
Duplicate variable: 'option'
L214: let attr = this.filter['value'][filter];
L215: if (attr && Object.keys(attr).length > 0) for (var option in attr) {
L216: if(option != 'select' && attr[option]) {
[20:03:23] lint finished in 10.05 s
It seems that platform is not returned properly and that's why it's null.
You didn't include your constructor so i have no idea how your constructor looks like, but the following code snippet is returning platform name properly using the Device plugin you're using in your code.
import { Device } from '#ionic-native/device';
constructor(private device: Device) {}
......
if(this.device.platform.toLowerCase()=='android'){
// Your logic here
}
This happens because you're running ionic serve on web and the cordova platform isn't available there. Ionic Native returns {} when a property doesn't exist (when cordova isn't available), and it throws a console warning for developer assistance.
When one try to retrieve data from {} object then it will return null.
Please try adding below code:
import { Platform } from 'ionic-angular';
//add it in constructor as follows:
constructor( public platform: Platform){
}
//add platform check
if (this.platform.is('cordova')) {
//add your code here
}else{
console.log("Plugin not supported on web");
}

Broccoli-compass and ember-cli 0.39

I recently upgraded ember-cli to .39, and something changed to cause my broccoli-compass code to break.
Here's the code:
app.styles = function() {
return compileCompass(this.appAndDependencies(), this.name + '/styles/app.scss', {
compassCommand: 'bundle exec compass',
outputStyle: 'expanded',
sassDir: this.name + '/styles',
imagesDir: 'public/images',
cssDir: '/assets'
});
};
I get this error:
[broccoli-compass] Error: Command failed: Errno::ENOENT on line ["155"] of ~/.rvm/gems/ruby-2.1.1/gems/compass-0.12.6/lib/compass/compiler.rb: No such file or directory # rb_sysopen - ~/campaign-designer/ember/tmp/tree_merger-tmp_dest_dir-pSk32Zuy.tmp/campaign-designer/styles/app.scss
Run with --trace to see the full backtrace
arguments: `bundle exec compass compile campaign-designer/styles/app.scss --relative-assets --sass-dir campaign-designer/styles --output-style expanded --images-dir public/images --css-dir "../compass_compiler-tmp_cache_dir-8Yu97OaF.tmp/assets"`
Has app.styles or this.appAndDependencies() changed? I've tried many variants of this config to no avail.
There's a similar question here, but I still couldn't get things working.
For what it's worth, something like this ended up helping me:
// Compass config in Brocfile.js
app.registry.add('css', 'broccoli-compass', 'scss', {
toTree: function(tree, inputPath, outputPath, options) {
// broccoli-compass doesn't like leading slashes
if (inputPath[0] === '/') { inputPath = inputPath.slice(1); }
// tree = mergeTrees([
// tree,
// 'public'
// ], {
// description: 'TreeMerger (stylesAndVendorAndPublic)'
// });
return compileCompass(tree, inputPath + '/app.scss', {
outputStyle: 'expanded',
// require: 'sass-css-importer', // Allows us to import CSS files with #import("CSS:path")
sassDir: inputPath,
imagesDir: 'images',
//fontsDir: 'fonts',
cssDir: outputPath
});
}
});
Ultimately I removed compass from my project (I just had to write a few SASS mixins myself) to avoid the troubles with the config + attempt to get faster build speeds.
Update: You may now want to check out the ember-cli-compass-compiler ember-cli addon, which makes it easier to get started with Compass in your ember-cli project.