jest


React, Jest, and CustomEvent testing

Simple problem: I need my React component to communicate an event to a JQuery plugin in Rails.

Super complicated solution: I have the component emit an event so that a JQuery event listener can know to do its thing.

Easy enough. I set to work writing tests first, but Jest keeps spitting out an error:

1
ReferenceError: CustomEvent is not defined

React and Jest don’t know about the CustomEvent constructor for some reason. I need the assurance that an event will be fired every time an agent clicks the clear button on an input box.

The component looks a little like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Tagger = React.createClass({ displayName: 'Tagger',
/**
* Clear the tagBox input
*/
clear: function(evt) {
this.refs.tagBox.getDOMNode().value = '';
// I can't get jquery-tokeninput to play nicely. This
// event signals the listner defined in queries.coffee
window.dispatchEvent(new CustomEvent('clear-tags'));
},
// ...
render: function() {
// ...
}
});
module.exports = Tagger;

In the corresponding test file, I provide my own CustomeEvent and mock the window.dispatchEvent function:

1
2
3
4
if (!window.CustomEvent) {
CustomEvent = function(name, params){ return params;};
}
window.dispatchEvent = jest.genMockFunction();

By doing all that, I can perform meaningful tests like this:

1
2
3
4
5
6
7
8
9
10
11
12
it('should emit a clear-tags event', function() {
var tagger = TestUtils.renderIntoDocument(<Tagger model="pick" />);
expect(window.dispatchEvent.mock.calls.length).toEqual(0);
// Enter some tags
tagger.refs.tagBox.getDOMNode().value = 'bill murray, comedy, movies';
// Clear
TestUtils.Simulate.click(tagger.refs.clearButton.getDOMNode());
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
});

React Test Utilities do not Simulate 'message' events

React’s Test Utilities have a method for every event that React understands. At this point, React doesn’t understand the message event emitted by the window object. Should it? I’m not sure. I’ll try to make a long story short…

I have a project scaffold that makes it easy to set up a web application interface that does the whole OAuth2 thing through a popup window. When the server is provided with valid login credentials, it sends an authentication token back to the web application through a React component that provides the callback functionalilty.

The relevant parts look like this:

1
2
3
4
5
6
7
8
9
10
11
var Oauth2Callback = React.createClass({
componentDidMount: function() {
var queryString = window.location.hash.split('#')[1];
var params = this.parseKeyValue(queryString);
window.opener.postMessage(params, '*');
window.close();
},
// ...
});

The server sends the authentication token embedded in a query string. The authentication token is isolated and then passed back to the application window that opened the OAuth2 popup through a call to that object’s postMessage function. The postMessage function is what emits the message event not understood by react. The listener for the postMessage-produced message event looks a little like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var AuthenticateMenu = React.createClass({
// ...
componentDidMount: function() {
// ...
window.addEventListener('message', function(event) {
if (event.data.access_token) {
verifyToken(event.data.access_token, function(err, data) {
// ...
localStorage.setItem('archiver-hai-key', event.data.access_token);
// ...
});
}
});
// ...
},
// ...
});

So how do you test all this if React’s Test Utilities don’t understand the message event?

It’s no big deal. Just find and call the function assigned to the event in the window object itself. Here’s a sample test for Jest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe('window message event', function() {
// ...
it('should store the token received from the popup authentication dialog', function() {
var authenticate = TestUtils.renderIntoDocument(<AuthenticateMenu />);
expect(localStorage.getItem('my-authentication-key')).toBe(undefined);
// Simulate a message being sent
var event = { data : {
access_token: 'SomePseudoRandomAuthenticationKey', type: 'bearer' }
};
window._listeners.message.false[0](event);
expect(localStorage.getItem('my-authentication-key')).
toEqual('SomePseudoRandomAuthenticationKey');
});
// ...
});

This part

1
window._listeners.message.false[0](event);

calls the function listening for the message event. You can see all of window‘s listeners in window._listeners.

Incidentally, localStorage was mocked in the Jest tests thusly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var mock = (function() {
var store = {};
return {
getItem: function(key) {
return store[key];
},
setItem: function(key, value) {
store[key] = value.toString();
},
clear: function() {
store = {};
}
};
})();
Object.defineProperty(window, 'localStorage', { value: mock });

This code is courtesy of Martin Danielson.


grunt-init and the Jest preprocessor

I like developing with React. I find myself using it more and more, so today I decided to create a project scaffold to automate project creation with grunt-init.

Being obsessive about testing (and conformity), I adopted Jest to test my interfaces, simply because that’s what the good folk over at Facebook use too. As such, unit testing was to be built into my project template right from the beginning. This is where I ran into a problem…

grunt-init calls on a file called template.js. The code in this file typically prompts the user for project-specific details (name, description, homepage, etc.) and writes all the information to the project’s package.json file. You can see my full template here. The following shows the project.json properties that are not configurable by the user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ...
props.scripts = {
"test": "jest"
};
props.keywords = [];
props.dependencies = {};
props.devDependencies = {
"grunt": "~0.4.x",
"grunt-contrib-clean": "~0.6.x",
"grunt-contrib-concat": "~0.5.x",
"grunt-contrib-connect": "~0.8.x",
"grunt-contrib-copy": "~0.6.x",
"grunt-contrib-livereload": "~0.1.x",
"grunt-contrib-uglify": "~0.6.x",
"grunt-contrib-watch": "^0.6.x",
"grunt-open": "^0.2.x",
"grunt-react": "^0.9.x",
"grunt-regarde": "~0.1.x",
"grunt-usemin": "^2.4.x",
"jest-cli": "^0.1.x",
"jquery": "^2.1.x",
"matchdep": "~0.3.x",
"react": "^0.11.x",
"react-tools": "^0.11.x"
};
// ...
// Generate package.json file.
init.writePackageJSON('package.json', props);

These props primarily consist of the development dependencies I always uses when developing with React. They are written to the file by the init.writePackageJSON function. For those familiar with Jest, you know that package.json needs a little Jest-specific addition for the the script preprocessor, which looks like this:

1
2
3
4
5
6
"jest": {
"scriptPreprocessor": "<rootDir>/__tests__/preprocessor.js",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react"
]
},

Being somewhat naive, I assumed that I could simply add the jest property to the props object like this:

1
2
3
4
5
6
props.jest = {
"scriptPreprocessor": "<rootDir>/__tests__/preprocessor.js",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react"
]
};

This does not work. init.writePackageJSON only sets the typical package.json options. The jest property is not typical, and the official documentation is not especially helpful. Here’s the relevant part pertaining to the init.writePackageJSON function:

The callback can be used to post-process properties to add/remove/whatever.

So yes, it is possible. You just have to add/remove/whatever the jest property. After poking around the code a little bit, I figured out that this is how you do it:

1
2
3
4
5
6
7
8
9
10
// Generate package.json file.
init.writePackageJSON('package.json', props, function(pkg, props) {
pkg.jest = {
"scriptPreprocessor": "<rootDir>/__tests__/preprocessor.js",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react"
]
};
return pkg;
});

See my ever-changing React project template here: https://github.com/RaphaelDeLaGhetto/grunt-init-gebo-react-hai.