Testing web components with jsdom - javascript

I have a web component I've built which is bundled (using Rollup) into a UMD and ESM package. The src/index.js basically looks like this.
import html from 'template.html';
import css from 'styles.css';
export class MyComponent extends window.HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
}
export default (function() {
window.customElements.define('my-component', MyComponent);
})()
I'm well aware about how weird this looks, and I'm open to suggestions on cleaning this up so testing actually works.
I'm trying to test my web component using mocha and jsdom-global. The problem is I can't get the component to mount. In other words, this fails:
import jsdomGlobal from 'jsdom-global';
import { expect } from 'chai';
import { MyComponent } from '../dist/my-component.esm';
describe('test', function() {
let jsdom;
before(function(){
jsdom = jsdomGlobal('', { runScripts: 'dangerously' });
const script = document.createElement('script');
script.textContent = `window.customElements.define('my-component', (${MyComponent}))`;
document.head.appendChild(script);
})
after(function() {
jsdom();
});
it('mounts', async function() {
await window.customElements.whenDefined('my-component');
expect(window.customElements.get('my-component')).to.exist;
})
})
I've also tried not wrapping in a new window.customElements.define call because I assumed the IIFE would just execute once the file is referenced but that doesn't seem to be the case?
As an aside, if anyone knows how to test the /src files directly instead of the /dist, that would be awesome. I assume there's some way that mocha and rollup could work together to do a better job in identifying coverage. As it stands right now, coverage just shows a single line of failures not anything against the lines in the source.

Ok, I have some success after trying all sorts of things.
import { JSDOM } from 'jsdom';
import { expect } from 'chai';
import fs from 'fs';
import { resolve } from 'path';
import { promisify } from 'util';
const readFile = promisify(fs.readFile);
async function getHTML() {
const script = await readFile(resolve(__dirname, '..', 'dist', 'my-component.umd.js'));
return `<html><head><script>${script}</script></head><body></body></html>`;
}
describe('my-component', function () {
describe('browser context', function() {
let window, document;
before(async function() {
const html = await getHTML();
const dom = new JSDOM(html, { runScripts: 'dangerously', resources: 'usable' });
window = dom.window;
document = window.document;
});
it('defines a custom element', async function() {
await window.customElements.whenDefined('my-component');
expect(window.customElements.get('my-component')).to.exist;
})
})
})
So this passes the assertion but because the test has no idea about the script, there's no coverage metrics.
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
It would be nice to be able to test and report coverage, but this'll have to do for now.

Related

Rewire private Typescript class methods

I'm trying to write unit tests for some private methods in my class using Typescript for my Node.js module.
I tried using rewire, but it's unable to access any methods (even public ones).
Here is my setup:
myclass.ts
class MyClass{
private somePrivateMethod() {
// do something
console.log(42);
}
public somePublicMethod() {
// do something
this.somePrivateMethod();
// do something
}
}
export default MyClass;
test.ts
import { expect } from 'chai';
import rewire from 'rewire';
describe('Test', function () {
describe('#somePrivateMethod()', function () {
const rewired = rewire('../src/myclass.ts');
//const rewired = rewire('../dist/myclass.js');
const method = rewired.__get__('somePrivateMethod');
});
});
I tried using rewire on the Typescript file and on the compiled JS file as well, but I get the same error:
ReferenceError: somePrivateMethod is not defined
I'm using Mocha with the following config:
'use strict';
module.exports = {
extension: ["ts"],
package: "./package.json",
reporter: "spec",
slow: 75,
timeout: 2000,
ui: "bdd",
"watch-files": ["test/**/*.ts"],
require: "ts-node/register",
};
Is there any solution for this problem?
I know is a pretty old question but if someone else find themself in the situation I've managed to find a solutions:
export class YourClass {
constructor( ) { }
private thisIsPrivate() {
//private stuff
}
otherPublicMethod() {
//public stuff
}
}
inside your test file you can import the class as usual to test public methods
import YourClass from '../src/classes/YourClass'
const testClass = new YourClass();
...
to test private methods import the class using rewire
import rewire from 'rewire'
const rewiredModule = rewire('../src/classes/YourClass')
const rewiredClass = rewiredModule.__get__('YourClass')
then inside your test
it('Test your private method', function () {
const myRewiredClassInstance = new rewiredClass()
myRewiredClassInstance.thisIsPrivate()
//do your expect here
}
The only downside is that the object returned is "any" and so no help from typescript.
you can even debug inside it if you have setup your debugger correctly
Enjoy everyone

Importing custom CommonJS module fails

I created a CommonJS module in project A in the following way:
const { WebElement } = require('selenium-webdriver');
const { By } = require('selenium-webdriver');
class VlElement extends WebElement {
constructor(driver, selector) {
...
}
async getClassList() {
...
}
}
module.exports = VlElement;
In project B I use the following code:
const VlElement = require('projectA');
class VlButton extends VlElement {
constructor(driver, selector) {
super(driver, selector);
}
...
}
module.exports = VlButton;
When running the code, VLElemlent cannot be found.
It is in my package.json and I can see VLElement under projectB > node_modules > projectA.
What am I doing wrong with my exports?
Thanks in advance.
Regards
Make sure you have a projectB/mode_modules/package.json with a main which points to the file that defines/exports VlElement, like this:
"main": "path/to/file/with/VlElement.js",
When you call require('projectA'); this has to be resolved to a file inside projectA so that it can be evaluated to (and return) the exports from that file. The main entry in the package.json allows this (but defaults to index.js, so if you are using that you don't need package.json, probably, but you should have it anyway).
You can have multiple files with various exports, but remember require('projectA'); can still only return one thing, so the way to do that is usually to have an index.js which looks something like:
module.exports = {
'something': require('./something.js'),
'otherthing': require('./otherthing.js'),
'etc': require('./etc.js'),
};

How to get localStorage working in vue testing

I am trying to test vue components.
I have a vue single file component which uses vuex. My states are stored in store.js which makes use of localStorage. However, when I run npm test I get error that reads:
WEBPACK Compiled successfully in 9416ms
MOCHA Testing...
RUNTIME EXCEPTION Exception occurred while loading your tests
ReferenceError: localStorage is not defined
Tools I am using for testing:
#vue/test-utils, expect, jsdom, jsdom-global, mocha, mocha-webpack
How I run the tests:
"test": "mocha-webpack --webpack-config node_modules/laravel-mix/setup/webpack.config.js --require tests/JavaScript/setup.js tests/JavaScript/**/*.spec.js"
A sample test, order.spec.js:
require('../../resources/assets/js/store/store');
require('../../resources/assets/js/app');
import { mount } from '#vue/test-utils';
import Order from '../../resources/assets/js/views/order/List.vue';
import expect from 'expect';
describe('Order', ()=>{
it('has alert hidden by default', () => {
let wrapper = mount(Order);
expect(wrapper.vm.alert).toBe(false);
})
})
In setup.js file I am loading jsdom like this:
require('jsdom-global')();
How do I fix this?
jsdom-global is using an old version of jsdom. jsdom has supported localStorage since 11.12.0.
To use jsdom 11.12+ with localStorage support, you can add jsdom window properties to the global scope yourself in a test setup file that runs before your tests:
/* setup.js */
const { JSDOM } = require('jsdom');
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;
function copyProps(src, target) {
Object.defineProperties(target, {
...Object.getOwnPropertyDescriptors(src),
...Object.getOwnPropertyDescriptors(target),
});
}
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: 'node.js',
};
global.requestAnimationFrame = function (callback) {
return setTimeout(callback, 0);
};
global.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
copyProps(window, global);

Python like function mock in JavaScript

With Python it's so easy to mock a function that is used in the function under test.
# my_module.py
def function_a():
return 'a'
def function_b():
return function_a() + 'b'
# tests/test_my_module.py
from unittest import TestCase
from unittest.mock import patch
from my_module import function_b
class MyModuleTestCase(TestCase):
#patch('my_module.function_a')
def test_function_b(self, mock_function_a):
mock_function_a.return_value = 'c'
self.assertEqual(function_b(), 'cb')
Is something like this possible in JavaScript using, for example, jest?
# myModule.js
function myFunctionA() {
return 'a';
}
export function myFunctionB() {
return myFunctionA() + 'b';
}
# __test__/test.myModule.js
import { myFunctionB } from '../myModule';
describe('myModule tests', () => {
test('myFunctionB works', () => {
// Mock `myFunctionA` return value here somehow.
expect(myFunctionB()).toBe('cb')
});
});
I've read https://github.com/facebook/jest/issues/936 and still have no idea how to do it as there are so many (hacky) suggestions (some of them ~2 years old).
Jest can mock an entire module using jest.mock() or mock an individual module export using jest.spyOn() in combination with functions like mockImplementation().
This makes it easy to mock a function imported from a library:
// ---- lib.js ----
export function myFunctionA() {
return 'a';
}
// ---- myModule.js ----
import { myFunctionA } from './lib';
export function myFunctionB() {
return myFunctionA() + 'b'; // call lib.myFunctionA()
}
// ---- myModule.test.js ----
import { myFunctionB } from './myModule';
import * as lib from './lib';
describe('myModule tests', () => {
test('myFunctionB works', () => {
const mock = jest.spyOn(lib, 'myFunctionA'); // create a spy on lib.myFunctionA()
mock.mockImplementation(() => 'c'); // replace the implementation
expect(myFunctionB()).toBe('cb');
mock.mockRestore(); // remove the spy and mock implementation
});
});
In the code sample from the question myModule contains two functions and one calls the other directly.
Because a mock works on either an entire module or a module export, mocking the direct call to myFunctionA() from within myFunctionB() would be very difficult the way the code is written.
The easiest way I have found to work around situations like this is to import the module into itself and use the module when calling the function. That way it is the module export that is being called and it can be mocked in the test:
// ---- myModule.js ----
import * as myModule from './myModule';
export function myFunctionA() {
return 'a';
}
export function myFunctionB() {
return myModule.myFunctionA() + 'b'; // call myModule.myFunctionA()
}
// ---- myModule.test.js ----
import * as myModule from './myModule';
describe('myModule tests', () => {
test('myFunctionB works', () => {
const mock = jest.spyOn(myModule, 'myFunctionA'); // create a spy on myModule.myFunctionA()
mock.mockImplementation(() => 'c'); // replace the implementation
expect(myModule.myFunctionB()).toBe('cb');
mock.mockRestore(); // remove the spy and mock implementation
});
});

How to integrate Electron ipcRenderer into Angular 2 app based on TypeScript?

I want to use ipcMain / ipcRenderer on my project to communicate from Angular to Electron and back.
The Electron side is pretty clear:
const
electron = require('electron'),
ipcMain = electron.ipcMain,
;
ipcMain.on('asynchronous-message', function(event, arg) {
console.debug('ipc.async', arg);
event.sender.send('asynchronous-reply', 'async-pong');
});
ipcMain.on('synchronous-message', function(event, arg) {
console.debug('ipc.sync', arg);
event.returnValue = 'sync-pong';
});
But I have no idea how to integrate that Electron module into my Angular 2 app. I use SystemJS as module loader, but I'm a rookie with it.
Any help appreciated. Thanks.
--- Mario
There is conflict, because Electron use commonjs module resolving, but your code already compiled with systemjs rules.
Two solutions:
Robust way. Register object require returned:
<script>
System.set('electron', System.newModule(require('electron')));
</script>
This is the best, because renderer/init.js script loads that module on start. SystemJS have to take it only, not loads.
Alternative way. Use dirty trick with declaration.
Get electron instance inside index.html:
<script>
var electron = require('electron');
</script>
Declare it inside your typescript file this way:
declare var electron: any;
Use it with freedom )
electron.ipcRenderer.send(...)
A recent package called ngx-electron makes this easy. Link to repo and link to article
src/app/app.module.ts
import { NgxElectronModule } from 'ngx-electron';
// other imports
#NgModule({
imports: [NgxElectronModule],
...
})
src/app/your.component.ts
import { Component, NgZone } from '#angular/core';
import { ElectronService } from 'ngx-electron';
#Component({
selector: 'app-your',
templateUrl: 'your.component.html'
})
export class YourComponent {
message: string;
constructor(private _electronService: ElectronService, private _ngZone: NgZone) {
this._electronService.ipcRenderer.on('asynchronous-reply', (event, arg) => {
this._ngZone.run(() => {
let reply = `Asynchronous message reply: ${arg}`;
this.message = reply;
});
}
}
playPingPong() {
this._electronService.ipcRenderer.send('asynchronous-message', 'ping');
}
}
Note: NgZone is used because this.message is updated asynchronously outside of Angular’s zone. article
But I have no idea how to integrate that Electron module into my Angular 2 app
You would have angular hosted within the UI rendering process in electron. The ipcMain is used to communicate to non rendering child processes.
This should just be a case of requiring the ipcRenderer module in your main html file (electron will provide this for you):
<script>
var ipc = require('electron').ipcRenderer;
var response = ipc.sendSync('getSomething');
console.log(response); // prints 'something'
</script>
and then setting up a handler in your main js file:
const ipcMain = require('electron').ipcMain;
ipcMain.on('getSomething', function(event, arg) {
event.returnValue = 'something';
});
That's all there should be to it.
My solution:
configure a baseUrl in tsconfig.json
at the root of the directory pointed by the baseUrl, create a directory "electron".
Inside this directory, a file index.ts:
const electron = (<any>window).require('electron');
export const {BrowserWindowProxy} = electron;
export const {desktopCapturer} = electron;
export const {ipcRenderer} = electron;
export const {remote} = electron;
export const {webFrame} = electron;
(ideally export default [...]require('electron'), but this is not statically analysable...)
now I can have in my renderer process:
import {remote} from 'electron';
console.log(remote);
Hope it makes sense...
with typings enabled:
///<reference path="../../typings/globals/electron/index.d.ts"/>
const electron = (<any>window).require('electron');
export const BrowserWindowProxy = <Electron.BrowserWindowProxy>electron.BrowserWindowProxy;
export const desktopCapturer = <Electron.DesktopCapturer>electron.desktopCapturer;
export const ipcRenderer = <Electron.IpcRenderer>electron.ipcRenderer;
export const remote = <Electron.Remote>electron.remote;
export const webFrame = <Electron.WebFrame>electron.webFrame;
NB: typings I got is from:
{
"globalDependencies": {
"electron": "registry:dt/electron#1.4.8+20161220141501"
}
}
Component.TS
const ipc = require('electron').ipcRenderer;
#Component({
selector: 'app-my component',.....
})
....
public testElectronIpc(): void{
ipc.send('test-alert');
}
MAIN.JS
// IPC message listeners
ipc.on('test-alert', function (event, arg) {
console.log('Test alert received from angular component');
})
config
plugins: [
new webpack.ExternalsPlugin('commonjs', [
'desktop-capturer',
'electron',
'ipc',
'ipc-renderer',
'native-image',
'remote',
'web-frame',
'clipboard',
'crash-reporter',
'screen',
'shell'
])
],

Categories