react


A non-intrusive behavioural testing approach to bootstrapped React in Typescript

I’m on a team that loves Typescript and React. Their code is manually tested. I prefer to write my tests first.

The following process addresses the problem of respecting an existing approach to app development, while respecting my own standard of professional practice.

This is how I introduced my behavioural testing approach to a project bootstrapped with create-react-app. It equips me to write automated tests while allowing my teamates their own approach.

Generate a React Typescript app

As per the docs, the following produces the base application:

1
2
npx create-react-app my-app --typescript
cd my-app

create-react-app comes with a few baked-in npm scripts. There are two existing build scripts: npm run build and npm start. The first only produces production builds. The latter only produces transient development builds (i.e., you can’t save them for later).

I need to produce test builds, so I add this to this to the scripts section in package.json:

1
2
3
4
5
6
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build:test": "npx env-path -p .env.test react-scripts build"
...
}

create-react-app does a lot of crazy-cool stuff with .env files. I don’t think what follows will respect the dotenv-flow and dotenv-expand features that allows you to cascade build configurations according to environment.

With that it mind, install env-path:

1
npm install --save-dev env-path

I want a test build, so I need a .env.test file. Configurations, of course, are app dependent. The following is one likely example:

1
2
REACT_APP_REDIRECT_URI=http://localhost:3000
REACT_APP_CLIENT_ID=SomeExampleToken123

To create your test build, execute:

1
npm run build:test

You’ll see a message that says:

1
Creating an optimized production build...

Uh oh! react-scripts build only produces a production build. Is it still creating a production build, or is it reading my .env.test file?

This is the perfect opportunity to test if this test build configuration is working…

jasmine and zombie.js

None of this requires testing with jasmine and zombie, but I like testing with this pair because I’m old-fashioned and lazy. Install and initialize:

1
2
npm install --save-dev jasmine zombie 
npx jasmine init

NOTE: I’m purposefully skipping adding typescript support to jasmine for the moment. I may revisit this in the future.

Testing the build

I need to determine if the values I set in .env.test are being baked into the test build configured above.

Here’s my first test, which I place in spec/indexSpec.js:

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
const path = require('path');
require('dotenv').config({ path: path.resolve(process.cwd(), '.env.test') });
require('../support/server');

const Browser = require('zombie');
const PORT = 3000;

Browser.localhost('example.com', PORT);

describe('landing page', () => {

let browser, document;
beforeEach(done => {
browser = new Browser({ waitDuration: '30s', loadCss: false });

// Wait for React to execute and render
browser.on('loading', (doc) => {
document = doc;
document.addEventListener("DOMContentLoaded", (event) => {
done();
});
});

browser.visit('/', (err) => {
if (err) return done.fail(err);
});
});

it('displays the .env.test config variables', () => {
browser.assert.link('a', 'Redirect', 'http://localhost:3000/&id=SomeExampleToken123');
});
});

If you look closely at the above file, you’ll see this test needs its own server to run (npm start only does a development build). Paste the following into spec/support/server.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express');
const path = require('path');
const app = express();
const logger = require('morgan');

app.use(logger('dev'));

app.use(express.static(path.join(__dirname, '../../build')));

app.get('/*', function(req, res) {
res.sendFile(path.join(__dirname, '../../build', 'index.html'));
});

const port = 3000;
app.listen(port, '0.0.0.0', function() {
console.log(`auth-account listening on port ${port}!`);
});

You need morgan to see the server output:

1
npm install --save-dev morgan

Configure jasmine script

Add another line in the package.json (i.e., the e2e line right below the build:test line already configured):

1
2
3
4
5
6
7
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build:test": "npx env-path -p .env.test react-scripts build",
"e2e": "npm run build:test && npx jasmine"
...
}

The tests should now execute with one failing test:

1
npm run e2e

Make the test pass

The test defined above is simply checking to see if a link is created from the values stored in the .env.test file. Add that link to src/App.tsx:

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
// ...

const App: React.FC = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>

<!-- Add this link! Right here!!! -->
<a href={`${process.env.REACT_APP_REDIRECT_URI}/&id=${process.env.REACT_APP_CLIENT_ID}`}>Redirect</a>
</div>
);
}

Execute the test:

1
npm run e2e

It passes!

More work…

This configuration doesn’t currently respect the neat dotenv-flow and dotenv-expand features that come baked into create-react-app.

As noted above, jasmine is not currently configured to support typescript.


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);
});

Deploy a gebo human-agent interface

This document provides step-by-step instructions on how to deploy a gebo human-agent interface (HAI). It is comprehensive so that you may copy, paste, and execute the commands and deploy the configurations provided, from beginning to end.

Set up grunt

Some of these instructions may have already been executed in previous gebo tutorials.

1
2
sudo npm install grunt-cli -g
sudo npm install grunt-init -g

The first command enables you to run the locally installed grunt, automatically. The second allows you to call grunt-init on the gebo-react-hai template.

Get the gebo-react-hai template

This is going in your ~/.grunt-init/ directory:

1
git clone https://github.com/RaphaelDeLaGhetto/grunt-init-gebo-react-hai.git ~/.grunt-init/gebo-react-hai

Create a new project

1
2
3
mkdir myfirsthai
cd myfirsthai
grunt-init gebo-react-hai

Upon execution the gebo-react-hai will prompt you for some input, with some fields prefilled:

1
2
3
4
5
6
7
8
9
10
11
12
13
Please answer the following:
[?] Project name (myfirsthai)
[?] Description (The best project ever.) My first gebo HAI
[?] Version (0.0.0)
[?] Project git repository (none) https://github.com/RaphaelDeLaGhetto/myfirsthai.git
[?] Project homepage (none)
[?] Project issues tracker (none)
[?] Licenses (MIT)
[?] Author name (RaphaelDeLaGhetto)
[?] Author email (daniel@capitolhill.ca)
[?] Author url (none)
[?] Main module/entry point (index.html)
[?] Do you need to make any changes to the above before continuing? (y/N)

Once everything is configured to your liking, execute:

1
sudo npm install

Test

If everything has been initialized correctly, the tests should pass:

1
npm test

Run the development server

1
grunt server

By default, your HAI will be running at http://localhost:9000. There you will see a vanilla gebo HAI interface.

Development

Obviously, a simple Hello, world! HAI isn’t going to suffice. You’re going to want to implement your own functionality…

Project directory

The gebo-react-hai template produces this directory structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.. (up a dir)                  
/home/daniel/workspace/myfirsthai/
▸ __tests__/
▸ assets/
▸ node_modules/
▾ scripts/
▸ gebo/
config.js
HelloWorld.js
Interface.js
Gruntfile.js
index.html
oauth2callback.html
package.json
README.md
robots.txt

Everything in scripts/gebo directory is critical to interfacing with the gebo-server. Monkey with it at your own risk. I like to place project-specific React components in a scripts/lib directory, which I create myself. Use scripts/HelloWorld.js and scripts/Interface.js as launch points for your own gebo HAI.

Out of the box, the HelloWorld.js interface is intended as a welcome to unauthenticated clients. The functionality defined in Interface.js is only for friendos of the corresponding gebo agent.

Configure

Your HAI needs to know what gebo agent it’s talking to. This is specified in config.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...

/**
* This HAI's name and client ID
*/
var name = 'myfirsthai';
var clientId = 'myfirsthai@example.com';

/**
* The agent with which this HAI interacts
*/
var gebo = 'https://localhost:3443';

// ...

Setting root React component props through container attributes

Disclaimer: for this particular problem, there is a much simpler solution than that oulined here (see way below). Regardless, I’m documenting the complicated solution, because it will certainly become useful again in the future.

I’m working on a web application that archives typeset documents and images. Once a document is archived, users are able to view an HTML conversion of the document through the browser without having to copy the original file to their local machines. The main application window has a paginated list of archived documents. The user selects the document he wants to view and it opens in a new browser tab.

The root component on the main application tab opens a new tab in a manner similar to this:

1
2
3
4
5
6
7
<button type='button' className='btn btn-default btn-sm'
onClick={function(){
var w = window.open('page.html', '_blank');
w.addEventListener('load', function() {
parent.preview(doc._id, this);
});
}}>

page.html provides the scaffolding for the HTML representation of a PDF, DOC, or some other typeset document. In conventional React style, the root component is rendered to an element of your choosing. In this case, the element looks like this:

1
2
3
4
5
<div class="container">
<div class="starter-template">
<div id="page"></div>
</div>
</div>

Here’s the problem: the main application window either needs to

  1. download the HTML representation of the requested archived document, or
  2. obtain the mongo ID of the archived document so that it can be downloaded when page.html renders the root component

Either way, the problem is the same. I couldn’t set the page content or document ID until after page.html was loaded. For illustrative simplicity, suppose I proceed by obtaining the mongo ID. In this case, I need page.html to look like this after it’s loaded:

1
2
3
4
5
<div class="container">
<div class="starter-template">
<div id="page" docId="SomeMongoObjectID"></div>
</div>
</div>

Once page.html is fully loaded, a gebo-server-style request is sent to the archiver-agent:

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
33
var DocumentList = React.createClass({

// ...

preview: function(id, windoe) {
this.refs.error.getDOMNode().style.visibility = 'hidden';
var previewMsg = {
sender: this.props.clientId,
performative: 'request',
action: 'archive.view',
content: {
resource: 'pages',
id: id,
},
access_token: localStorage.getItem(name + '-key'),
};

var parent = this;
var request = perform(previewMsg, function(err, data) {
if (err) {
parent.refs.error.getDOMNode().style.visibility = 'visible';
parent.setState({ errorMessage: 'Cannot preview that document', errorDetails: err })
return;
}

// !!!
var element = windoe.document.getElementById('page');
element.setAttribute('docId', data._id);
});
},

// ...
});

These lines set #page‘s attribute in page.html:

1
2
var element = windoe.document.getElementById('page');
element.setAttribute('docId', data._id);

But wait! That’s great and all, but how do I tell the root React component to wait until the docId attribute has been set?

Behold the MutationObserver!

This code was appended to the root component’s definition, which is rendered in page.html. It’s adapted from the example provided on the Mozilla page.

1
2
3
4
5
6
7
8
9
10
11
12
var target = document.querySelector('#page');

var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var container = document.getElementById('page');
React.renderComponent(
<Page startPage={container.getAttribute('startPage')} email={container.getAttribute('email')} />, document.getElementById('page')
);
});
});

observer.observe(target, { attributes: true });

This code simply waits until the #page element’s attributes change before rendering the root component in page.html.

Epilogue

You’ll have to take my word for it, but there were very good reasons why I initially pursued this complex solution. The way things worked out, however, allowed me to simply render the root component in page.html from the application loaded in the main window like this:

1
2
3
4
5
preview: function(id, windoe) {
// ...
React.renderComponent(<Page docId={data._id} />, windoe.document.getElementById('page'));
// ...
},

Obviously, this is a much cleaner solution than the one outlined above. Nonetheless, I know that which has been documented will become useful to me again in the future.


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.


Global events and communication between React components

Motivation: I was working on a React component called AuthenticateMenu. Basically, it gives you a Login or Logout option depending on your OAuth2 authentication status with the backend. The problem I faced was that I didn’t want to make AuthenticateMenu a child of another component. It seemed perfectly reasonable that I should want this kind of organizational structure (simplified):

1
2
3
4
5
6
7
8
9
10
11
12
...
<body>
<div class="navbar" role="navigation">
...
<div id="authenticateMenu"></div> <!-- menu rendered here -->
...
</div>
...
<div id="mainApp"></div> <!-- main application rendered here -->
...
</body>
...

Reasonable. But, how does the rendered authenticateMenu tell the mainApp that the user has been authenticated? How do these, and other similarly arranged React components components talk amongst each other?

The official documentation says this:

For communication between two components that don’t have a parent-child relationship, you can set up your own global event system. Subscribe to events in componentDidMount(), unsubscribe in componentWillUnmount(), and when you receive an event, call setState().

I dare say, that is precisely what I did…

All this effort culminated in three components:

  1. AuthenticateMenu
  2. Authenticate
  3. HelloWorld

Simply put, HelloWorld shouts out to the whole world until a user authenticates through AuthenticateMenu. At this point, Authenticate, the global event listener, tells HelloWorld to greet the user instead.

Without delving into the intricacies of OAuth2, AuthenticateMenu calls on the server to authenticate through a popup menu. On success, the server returns an authentication token, which then needs to be verified. Once verified, AuthenticateMenu needs to notify HelloWorld.

Here are the relevant parts of AuthenticateMenu:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var AuthenticateMenu = React.createClass({

//
// ...
//

/**
* Emit a 'verified' event each time verifyToken
* returns, whether on success or error
*/
dispatchEvent: function() {
var verifiedEvent = new CustomEvent('verified', { detail: this.state });
window.dispatchEvent(verifiedEvent);
},

/**
* Remember, the token was received through a popup window. When the user
* receives a token, the popup window emits a 'message' event, which
* carries the token to the AuthenticateMenu component.
*/
componentDidMount: function() {

// Listen for when the authentication dialog returns
// a token
var parent = this;
window.addEventListener('message', function(event) {
if (event.data.access_token) {
verifyToken(event.data.access_token, function(err, data) {
if (err) {
localStorage.clear(name + '-key');
parent.setState(parent.getInitialState(), parent.dispatchEvent);
}
else {
localStorage.setItem(name + '-key', event.data.access_token);
parent.setState({ accessToken: event.data.access_token,
name: data.name,
admin: data.admin,
verified: true }, parent.dispatchEvent);
}
});
}
else {
localStorage.clear(name + '-key');
parent.setState(parent.getInitialState(), parent.dispatchEvent);
}
});
},

//
// ...
//
});

If nothing else, note the parent.dispatchEvent callbacks. The HelloWorld component needs to know when the server approves or denies user access. The parent.dispatchEvent function accomplishes this by emitting a verified event, which is then picked up by the Authenticate component. Authenticate then executes the callback provided by the HelloWorld component.

As you can see, Authenticate is very simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Authenticate = React.createClass({

/**
* componentDidMount
*/
componentDidMount: function() {
var update = this.props.update;
window.addEventListener('verified', function(event) {
update(event.detail);
});
},

/**
* Render the component
*/
render: function() {
return(
<div className='authenticate'></div>
);
}
});

As per the official documentation’s recommendation, I attached the listener in the componentDidMount() function. When a verified event is emitted by the AuthenticateMenu, the Authenticate component calls its parent’s callback (stored in this.props.update).

This arrangment allowed me to stick an Authenticate component inside of HelloWorld:

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
33
34
var HelloWorld = React.createClass({

//
// ...
//

/**
* Called whenever the user's authentication status changes
*
* @param object
*/
authenticate: function(user) {
if (user.verified) {
this.setState({
friendo: user.name,
});
}
else {
this.setState(this.getInitialState());
}
},

/**
* Render the component
*/
render: function() {
return(
<div className='helloWorld' ref='helloWorld'>
<Authenticate ref='authenticated' update={this.authenticate} />
<h1>Hello, {this.state.friendo}!</h1>
</div>
);
}
});

This also allows me to stick an Authenticate component anywhere application functionality needs to be restricted to authorized users. Given this particular authentication-focussed example, the Authenticate intermediary makes the whole mishmash a lot more manageable.


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.