I'm testing a couple of components that reach outside of their DOM structure when mounting and unmounting to provide specific interaction capability that wouldn't be possible otherwise.
I'm using Jest and the default JSDOM initialization to create a browser-like environment within node. I couldn't find anything in the documentation to suggest that Jest reset JSDOM after every test execution, and there's no explicit documentation on how to do that manually if that is not the case.
My question is, does Jest reset the JSDOM instance after every test, suite or does it keep a single instance of JSDOM across all test runs? If so, how can I control it?
To correct the (misleading) accepted answer and explicitly underline that very important bit of information from one of the previous comments:
No. Jest does not clean the JSDOM document after each test run! It only clears the DOM after all tests inside an entire file are completed.
That means that you have to manually cleanup your resources created during a test, after every single test run. Otherwise it will cause shared state, which results in very subtle errors that can be incredibly hard to track.
The following is a very simple, yet effective method to cleanup the JSDOM after each single test inside a jest test suite:
describe('my test suite', () => {
afterEach(() => {
document.getElementsByTagName('html')[0].innerHTML = '';
});
// your tests here ...
});
Rico Pfaus is right, though I found that resetting the innerHTML the way he suggests was too destructive and caused tests to fail. Instead, I found selecting a specific element (by class or id) I want to remove from the document more effective.
describe('my test suite', () => {
afterEach(() => {
document.querySelector(SOME CLASS OR ID).innerHTML = ''
})
})
This is still an issue for many people — and it's the top answer in Google — so I wanted to provide some context from the future ;)
does it keep a single instance of JSDOM across all test runs
Yes, the jsdom instance remains the same across all test runs within the same file
If so, how can I control it?
Long story short: you'll need to manage DOM cleanup yourself.
There is a helpful Github issue on facebook/jest that provides more context and solutions. Here's a summary:
if you want a new jsdom instance then separate your tests into separate files. This is not ideal for obvious reasons...
you can set .innerHTML = '' on the HTML element as mentioned in the accepted answer. That will resolve most issues but the window object will remain the same. Window properties (like event listeners) can persist in subsequent tests and cause unexpected errors.
cleanup the jsdom instance between tests. The jsdom cleanup function doesn't do anything magic — it's basically resetting global properties. Here's an example directly from the Github issue:
const sideEffects = {
document: {
addEventListener: {
fn: document.addEventListener,
refs: [],
},
keys: Object.keys(document),
},
window: {
addEventListener: {
fn: window.addEventListener,
refs: [],
},
keys: Object.keys(window),
},
};
// Lifecycle Hooks
// -----------------------------------------------------------------------------
beforeAll(async () => {
// Spy addEventListener
['document', 'window'].forEach(obj => {
const fn = sideEffects[obj].addEventListener.fn;
const refs = sideEffects[obj].addEventListener.refs;
function addEventListenerSpy(type, listener, options) {
// Store listener reference so it can be removed during reset
refs.push({ type, listener, options });
// Call original window.addEventListener
fn(type, listener, options);
}
// Add to default key array to prevent removal during reset
sideEffects[obj].keys.push('addEventListener');
// Replace addEventListener with mock
global[obj].addEventListener = addEventListenerSpy;
});
});
// Reset JSDOM. This attempts to remove side effects from tests, however it does
// not reset all changes made to globals like the window and document
// objects. Tests requiring a full JSDOM reset should be stored in separate
// files, which is only way to do a complete JSDOM reset with Jest.
beforeEach(async () => {
const rootElm = document.documentElement;
// Remove attributes on root element
[...rootElm.attributes].forEach(attr => rootElm.removeAttribute(attr.name));
// Remove elements (faster than setting innerHTML)
while (rootElm.firstChild) {
rootElm.removeChild(rootElm.firstChild);
}
// Remove global listeners and keys
['document', 'window'].forEach(obj => {
const refs = sideEffects[obj].addEventListener.refs;
// Listeners
while (refs.length) {
const { type, listener, options } = refs.pop();
global[obj].removeEventListener(type, listener, options);
}
// Keys
Object.keys(global[obj])
.filter(key => !sideEffects[obj].keys.includes(key))
.forEach(key => {
delete global[obj][key];
});
});
// Restore base elements
rootElm.innerHTML = '<head></head><body></body>';
});
For those interested, this is the soft-reset I'm using in "jest.setup-tests.js" which does the following:
Removes event listeners added to document and window during tests
Removes keys added to document and window object during tests
Remove attributes on <html> element
Removes all DOM elements
Resets document.documentElement HTML to <head></head><body></body>
— #jhildenbiddle
Related
I am new to jest with selenium automation test. I am trying to add a beforeAll() and afterAll() functions to open and close the browsers once and run all the tests across multiple files, instead of calling it individually in all the files and opening multiple browsers and loading the website everytime.
Here is my test:
enter image description here
Output
This is beforeAll() and afterAll() methods sitting in a separate file
Actual Test
beforeAll is different from it or describe. The first argument can only be empty or with done if you using callback:
// works
beforeEach((done) => {
...
done()
});
// works
beforeEach(() => {
return asyncSomething()
});
// Doesn't work.
beforeEach(("Don't add string here") => {..});
I set the aurelia-store up as per the docs; in the main.ts at the bottom of all the plugins (from the skeleton app with dotnet core) I have as the last plugin defined:
aurelia.use.standardConfiguration()
.plugin(PLATFORM.moduleName('aurelia-store'), { initialState })
Then my app needs to login the user and save their bearer token.
await aurelia.start();
await aurelia.setRoot(PLATFORM.moduleName("modules/login/login.vm"));
In the login class I am trying to use the #connectTo decorator. However it never sets the dependency property. So I am stuck on this simple part at the very start of the app and my work already suggested not to use Aurelia but I said I wanted to for fast POC.
I've copied the docs exactly and still have the issue. Notably, I had to turn off strictNullCheck in the tsconfig to make the doc code parse.
Login.ts
#connectTo({
target: 'state',
selector: {
userToken: (store) => store.state.pipe(pluck('userToken')),
loginRedirected: (store) => store.state.pipe(pluck('loginRedirected'))
}
})
export class Login {
static inject = [Aurelia, Store]
public state: State;
app: Aurelia;
constructor(Aurelia, private store: Store<State>) {
this.app = Aurelia
store.registerAction('ChangeUserToken', this.changeUserToken)
store.registerAction('LoginRedirected', this.loginRedirect)
}
activate() {
... this.state is always undefined.
if (!this.state.loginRedirected) { //error
}
}
}
I expect the this.state property to have a state object populated from the global state store with the initialState values.
e.g.
{ userToken: "", loginRedirected: false }
I just need to set the userToken in login and retrieve it in app.js. This is not possible; what could be missing to make this basic function actually work?
ConnectTo is a helper decorator to avoid manual state subscriptions since the Stream of states is a vanilla rxjs observable. If you take a closer look at the official plugin documentation you will notice that it sets up the subscription in a different lifecycle hook.
That said connectTo cant solve everything and with manual subscription you have the most flexibility.
Dont give up with your quest you just had bad luck of falling into a more complicated scenario of startup timing right at the begin which easy enough might bite you with lots of other Frameworks/Libraries as well. Also make sure to visit the official discourse.aurelia.io forum and post back solutions to SO.
I'm currently building an application using Electron which is fantastic so far.
I'm using Vue.js and Vuex to manage the main state of my app, mainly user state (profile and is authenticated etc...)
I'm wondering if it's possible to open a new window, and have the same Vuex state as the main window e.g.
I currently show a login window on app launch if the user is not authenticated which works fine.
function createLoginWindow() {
loginWindow = new BrowserWindow({ width: 600, height: 300, frame: false, show: false });
loginWindow.loadURL(`file://${__dirname}/app/index.html`);
loginWindow.on('closed', () => { loginWindow = null; });
loginWindow.once('ready-to-show', () => {
loginWindow.show();
})
}
User does the login form, if successful then fires this function:
function showMainWindow() {
loginWindow.close(); // Also sets to null in `close` event
mainWindow = new BrowserWindow({width: 1280, height: 1024, show: false});
mainWindow.loadURL(`file://${__dirname}/app/index.html?loadMainView=true`);
mainWindow.once('resize', () => {
mainWindow.show();
})
}
This all works and all, the only problem is, the mainWindow doesn't share the same this.$store as its loginWindow that was .close()'d
Is there any way to pass the Vuex this.$store to my new window so I don't have to cram everything into mainWindow with constantly having to hide it, change its view, plus I want to be able to have other windows (friends list etc) that would rely on the Vuex state.
Hope this isn't too confusing if you need clarification just ask. Thanks.
Although I can potentially see how you may do this I would add the disclaimer that as you are using Vue you shouldn't. Instead I would use vue components to build these seperate views and then you can achieve your goals in an SPA. Components can also be dynamic which would likely help with the issue you have of hiding them in your mainWindow, i.e.
<component v-bind:is="currentView"></component>
Then you would simply set currentView to the component name and it would have full access to your Vuex store, whilst only mounting / showing the view you want.
However as you are looking into it I believe it should be possible to pass the values of the store within loginWindow to mainWindow but it wouldn't be a pure Vue solution.
Rather you create a method within loginWindows Vue instance that outputs a plain Object containing all the key: value states you want to pass. Then you set the loginWindows variable to a global variable within mainWindow, this would allow it to update these values within its store. i.e.
# loginWindow Vue model
window.vuexValuesToPass = this.outputVuexStore()
# mainWindow
var valuesToUpdate = window.opener.vuexValuesToPass
then within mainWindows Vue instance you can set up an action to update the store with all the values you passed it
Giving the fact that you are using electron's BrowserWindow for each interaction, i'd go with ipc channel communication.
This is for the main process
import { ipcMain } from 'electron'
let mainState = null
ipcMain.on('vuex-connect', (event) => {
event.sender.send('vuex-connected', mainState)
})
ipcMain.on('window-closed', (event, state) => {
mainState = state
})
Then, we need to create a plugin for Vuex store. Let's call it ipc. There's some helpful info here
import { ipcRenderer } from 'electron'
import * as types from '../../../store/mutation-types'
export default store => {
ipcRenderer.send('vuex-connect')
ipcRenderer.on('vuex-connected', (event, state) => {
store.commit(types.UPDATE_STATE, state)
})
}
After this, use the store.commit to update the entire store state.
import ipc from './plugins/ipc'
var cloneDeep = require('lodash.clonedeep')
export default new Vuex.Store({
modules,
actions,
plugins: [ipc],
strict: process.env.NODE_ENV !== 'production',
mutations: {
[types.UPDATE_STATE] (state, payload) {
// here we update current store state with the one
// set at window open from main renderer process
this.replaceState(cloneDeep(payload))
}
}
})
Now it remains to send the vuex state when window closing is fired, or any other event you'd like. Put this in renderer process where you have access to store state.
ipcRenderer.send('window-closed', store.state)
Keep in mind that i've not specifically tested the above scenario. It's something i'm using in an application that spawns new BrowserWindow instances and syncs the Vuex store between them.
Regards
GuyC's suggestion on making the app totally single-page makes sense. Try vue-router to manage navigation between routes in your SPA.
And I have a rough solution to do what you want, it saves the effort to import something like vue-router but replacing components in the page by configured routes is always smoother than loading a new page: when open a new window, we have its window object, we can set the shared states to the window's session storage (or some global object), then let vuex in the new window to retrieve it, like created() {if(UIDNotInVuex) tryGetItFromSessionStorage();}. The created is some component's created hook.
Since start(), stop() will be removed in Qunit 2.0, what is the alternative for async setups and teardowns via the beforeEach, afterEach methods? For instance, if I want the beforeEach to wait for a promise to be finished?
QUnit basically wants people to stop using the global methods (not just start() and stop(), but also test(), expect(), etc). So, as of version 1.16.0, you should always use either the global namespace (QUnit) or the assert API argument passed into the test() functions. This includes the new async control:
QUnit.test( "testing async action", function( assert ) { // <-- note the `assert` argument here
var done = assert.async(); // tell QUnit we're doing async actions and
// hold onto the function it returns for later
setTimeout(function() { // do some async stuff
assert.ok( true, "This happened 100 ms later!" );
done(); // using the function returned from `assert.async()` we
// tell QUnit we're don with async actions
}, 100);
});
If you are familiar with the old start() and stop() way of doing things, you should see that this is extremely similar, but more compartmentalized and extensible.
Because the async() method call is on the assert argument into the test, it cannot be used in the beforeEach() function. If you have an example of how you were doing that before, please post it and we can try to figure out how to git it into the new way.
UPDATE
My mistake previously, the assert object is being passed into the beforeEach and afterEach callbacks on modules, so you should be able to do the same logic that you would do for a test:
QUnit.module('set of tests', {
beforeEach: function(assert) {
var done = assert.async();
doSomethingAsync(function() {
done(); // tell QUnit you're good to go.
});
}
});
(tested in QUnit 1.17.1)
Seeing that nobody has answered the beforeEach/afterEach part: a test suite is supposed to run as soon as the page loads. When that is not immediately possible, then resort to configuring QUnit:
QUnit.config.autostart = false;
and continue with setting up your test suite (initializing tests, feeding them to QUnit, asynchronously waiting for some components to load, be it AJAX or anything else), your site, and finally, when it's ready, just run:
QUnit.start();
QUnit's docsite has it covered.
Ember Qunit, has once exists beforeEach/setup, afterEach/teardown co-exist for a little while.
See PR: https://github.com/emberjs/ember-qunit/pull/125
I try to create e2e tests with karma and jasmine with yeoman. In my karma-e2e.conf.js I add jasmine:
files = [
JASMINE,
JASMINE_ADAPTER,
ANGULAR_SCENARIO,
ANGULAR_SCENARIO_ADAPTER,
'test/e2e/**/*.js'
];
A need async testing so I need to use runs, waits, waitsFor (https://github.com/pivotal/jasmine/wiki/Asynchronous-specs)
But if I try to use it:
it('test', function () {
runs(function () {
...
});
});
Scenatio test runner returns this:
TypeError: Cannot call method 'runs' of null
at runs (http://localhost:8080/adapter/lib/jasmine.js:562:32)
at Object.<anonymous> (http://localhost:8080/base/test/e2e/eduUser.js:42:3)
at Object.angular.scenario.SpecRunner.run (http://localhost:8080/adapter/lib/angular-scenario.js:27057:15)
at Object.run (http://localhost:8080/adapter/lib/angular-scenario.js:10169:18)
I don't know where the problem is. Can you help me please?
Angular e2e tests with Karma don't and can't use the JASMINE adapter. Instead you have the ANGULAR_SCENARIO_ADAPTER which has a similar feel to writing Jasmine tests.
All commands in the adapter's API are asynchronous anyway. For example element('#nav-items').count() doesn't return a number, it returns a Future object. Future objects are placed in a queue and executed asynchronously as the runner progresses. To quote the API docs:
expect(future).{matcher}:
[...] All API statements return a future object, which get a value assigned after they are executed.
If you need to run your own asynchronous test code, you can extend the adapter's DSL, this is easier than it might sound. The idea is that you return your own Future which can be evaluated by a matcher such as toBe(). There are some examples on how to do this in the e2e-tests.js Gist from Vojta. Just remember to call done(null, myRetrunValue); when your test code is successful (myRetrunValue is the value evaluated by your matcher). Or done('Your own error message'); if you want the test to fail.
UPDATE: In response to question below. To simulate a login, first add a function called login to the dsl:
angular.scenario.dsl('login', function() {
return function(selector) {
// #param {DOMWindow} appWindow The window object of the iframe (the application)
// #param {jQuery} $document jQuery wrapped document of the application
// #param {function(error, value)} done Callback that should be called when done
// (will basically call the next item in the queuue)
return this.addFutureAction('Logging in', function(appWindow, $document, done) {
// You can do normal jQuery/jqLite stuff here on $document, just call done() when your asynchronous tasks have completed
// Create some kind of listener to handle when your login is complete
$document.one('loginComplete', function(e){
done(null, true);
}).one('loginError', function(e){
done('Login error', false);
});
// Simulate the button click
var loginButton = $document.find(selector || 'button.login');
loginButton.click();
})
};
});
And then call:
beforeEach( function()
{
expect( login('button.login') ).toBeTruthy();
});