Running PixiJS tests in Node

Automated tests are a pretty cool invention even though a lot of people seem to think there is no place for those in video game development. I disagree and I'll tell you how to run tests for your Pixi game in Node.

#Gamedev #Programming
Running PixiJS tests in Node

Running PixiJS tests in Node

Quick disclaimer: This article expects you to have basic understanding of how Node, NPM and Mocha work. Otherwise why would you be looking for how to run tests in Node?

Somehow you got yourself into this situation: you have a project with Pixi, and you need to add tests to it. You also refuse to run them in the browser, it has to be Node. But when you add the first test it fails immediately on the import of Pixi, you find yourself in this issue thread on GitHub and you leave despondently, convinced there is no easy way to make it work.

Let me be your easy way.

The short version

If you don’t care about the whys, here are the three steps to make it work:

  1. Run npm install --save-dev jsdom canvas
  2. Add this file to your project
  3. To the command you use to run Mocha tests add -r <path>/hookJsdom.js

This will allow you import and use classes exposed by Pixi but beware: I have only tested this with display objects and subclasses, it’s more than likely that this is not enough to properly test rendering in full scope.

The long version

Why do you need jsdom and canvas

PixiJS is a rendering library that requires a graphical environment to work. Node is run in the console with neither visuals nor DOM (which is essentially the support for HTML structures). Now there are a couple of caveats to that:

  1. If you’re not planning to instantiate Pixi it shouldn’t matter if you reference or instantiate some of its class, except that it does. It’s a design choice, there is little point in supporting things almost no one requires, especially if you can fairly easily polyfill the support into the library.
  2. Even if you wanted to render something you don’t necessarily require a graphical interface, because you may only want to write to file. For example previews of user-made levels. Unfortunately, this still fits under the 0.01% use cases and for the most part, the polyfills we use in this post should carry you through. I say should because I haven’t tried them myself.

But here I am talking about graphical interface while the question is about jsdom and canvas and you may not know why. Pixi uses WebGL for rendering, WebGL requires <canvas> HTML tag to work, <canvas> tag requires the DOM to be there. And even if you use the canvas-rendering, removing the first step from this list, you still end up requiring both the tag and DOM.

So that’s where the two libraries come in, jsdom provides you with the context of a virtual browser page with window, document, etc, while canvas makes the rendering possible. Plain and simple!

Photo of a coffee, in brown colors
How about a coffee break before a wall of code?

How do we give Pixi access to the libs?

Due to the tight coupling with the browser, Pixi accesses certain objects globally: window, navigator, document and probably more; things which are not provided at all in Node. In order to get it to work we need to somehow bind certain things from jsdom into the global context, and it is achieved with this code. Below is the same code but with comments that explain in detail what is happening and why:

// Just importing the constructor from `jsdom` library
const JSDOM = require( 'jsdom' ).JSDOM;

// `url` has to be provided, otherwise `jsdom` will throw an exception on `localStorage` not being accessible,
// which would be triggered by the code below that assigns properties to the global context
const jsdomOptions = {
url: 'http://localhost/'

// Just creating the JSDOM and extracting `window`
const jsdomInstance = new JSDOM( '', jsdomOptions );
const { window } = jsdomInstance;

// This takes all non-private properties of `window` and assigns them to the global context, to replicate
// the behavior you have in the browser, where accessing a variable that was not declared explicitly
// it looks for it on the global `window` object

// Get all the properties in `window`
Object.getOwnPropertyNames( window )
// Remove the ones which start with underscore
.filter( property =&gt; !property.startsWith( '_' ) )
// Add the remaining ones to the global context
.forEach( key =&gt; global[key] = window[key] );

// Finally add the window object itself to the global context and add console to the window
global.window = window;
window.console = global.console;

You may have noticed the curious lack of canvas anywhere in this code despite my earlier mention that it is required. jsdom is smart enough to import canvas library automatically, provided you have included it in your package.json.

Getting the hook to run with Mocha

The final step is to get the above JS to run before any of the defined tests, and all it takes is to use Mocha’s --require CLI option. If your typical invocation of Mocha is something like this: node_modules/.bin/mocha tests/**/*.test.* all you have to do is add -r <path>/hookJsdom.js between the path to the binary and definition of the test files.

For example: node_modules/.bin/mocha -r tests/hookJsdom.js tests/**/*.test.* if you put the file under tests/hookJsdom.js.

And that’s about it. If your initial impression was that the problem is going to be arduous to solve, don’t fret, it was the same for me.