Unit-testing Aurelia with Jest + JSDOM + TypeScript + Pug

All source can be found at: Aurelia-Aire

Testing UI components is one of the more difficult parts of QA. In Stratosphere (one of the key components of our cloud management platform), we’d been using Karma + Jasmine, but the browser component had been complicated by the fact that providing a DOM to tests in a fast, portable manner subject to memory and speed constraints can be pretty challenging. Sure, initially we did the PhantomJS thing, then Chrome Headless came out, but writing UI tests just never really felt natural.

Then, last week, we decided to open-source our component framework, Aire, built on top of UIKit+Aurelia, and that created an exigent need to fix some of the things we’d been limping along with, most importantly testing. The success of OSS projects depends on quite a few things, but I consider providing a simple way to get contributors up-and-running critical.

Simple set-up

Internally, Aurelia uses an abstraction layer (Aurelia PAL) instead of directly referencing the browser’s DOM. Aurelia will (in principle) run on any reasonable implementation of PAL. Aurelia provides a partial implementation OOTB, Aurelia/pal-nodejs, that will enable to (mostly) run your application inside of NodeJS.

Project Structure

Our project structure is pretty simple: we keep all our components and tests under a single directory, src:


aire
├── build
│   └── paths.js
├── gulpfile.js
├── index.html
├── jest.config.js
├── jspm.config.js
├── jspm.dev.js
├── package.json
├── package-lock.json
├── src
│   ├── main
│   │   ├── aire.ts
│   │   ├── application
│   │   ├── button
│   │   ├── card
│   │   ├── core
│   │   ├── core.ts
│   │   ├── dropdown
│   │   ├── events.ts
│   │   ├── fab
│   │   ├── form
│   │   ├── icon
│   │   ├── init
│   │   ├── init.ts
│   │   ├── loader
│   │   ├── nav
│   │   ├── navbar
│   │   ├── offcanvas
│   │   ├── page
│   │   ├── search
│   │   ├── table
│   │   ├── tabs
│   │   └── widget
│   └── test
│   │   ├── button
│   │   ├── core
│   │   ├── init
│   │   ├── render.ts
│   │   ├── setup.ts
│   │   └── tabs

...etc

At the top of the tree you’ll notice jest.config.js, the contents of which look like this:

Basically, we tell Jest to look under src for everything. ts-jest will automatically look for your Typescript compiler configuration, tsconfig.js in its current directory, so there’s no need to specify that.

Our tsconfig is pretty standard for Aurelia projects:

Test

If you just copy and paste our tsconfig.json and jest.config.js files while following the outlined directory structure, everything will Just Work (don’t forget to npm i -D the appropriate Jest and Aurelia packages.)

At this point, you can use aurelia-test to write tests a la:

hello.html

hello.ts

hello.test.ts

Now, you can run your tests with npx jest:

aire@1.0.0 test /home/josiah/dev/src/github.com/sunshower/aurelia-aire/aire
npx jest

PASS src/test/button/button.spec.ts
PASS src/test/tabs/tab-panel.spec.ts
PASS src/test/init/init.spec.ts
PASS src/test/core/dom.spec.ts

Test Suites: 4 passed, 4 total
Tests: 12 passed, 12 total
Snapshots: 0 total
Time: 3.786s
Ran all test suites.

Enabling support for complex DOM operations

That wasn’t too bad, was it? Well, the problem we encountered was that we use the excellent UIKit framework, and they obviously depend pretty heavily on the DOM. Any reference in Aire to UIKit’s Javascript would fail with a ReferenceError: is not defined error. Moreover, if we changed the Jest environment from node to jsdom, we’d encounter a variety of errors along the lines of TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node' which I suspect were caused by pal-nodejs creating DOM elements via its own jsdom dependency while Jest was performing DOM operations using its jsdom dependency. In any case, the solution turned out to be to define a single, global jsdom by importing jsdom-global. Once we discovered this, we encountered other issues with browser-environment types and operations not being defined, but this setup.js resolved them:

At this point we could successfully test Aurelia + UIKit in NodeJS.

The final piece

All of our component views are developed in Pug, and I didn’t like that we’d be developing in Pug but testing using HTML. The solution to this turned out to be pretty simple: adding Pug as a development dependency and creating a pretty simple helper function:

With that final piece in place, our test looks like:

Conclusion

The benefits of writing tests in this way became apparent the moment we cloned the project into a new environment and just ran them via npm run test or our IDE’s. They’re fast, don’t require any environmental dependencies (e.g. browsers), and allow you to run and debug them seamlessly from the comfort of your IDE. But, perhaps most importantly, these are fun to write!

4 comments

  1. I tried to follow your helpful example today and stumbled accross a JSDOM problem:

    TypeError: Cannot set property ‘_eventListeners’ of undefined

    at Window.close (node_modules/jsdom/lib/jsdom/browser/Window.js:410:51)

    Did you see that one and did you find a solution?

  2. Thanks that was really useful, just a quick note with TypeScript, I changed the require to import as it’s mentioned on the jsdom-global GitHub/NPM, read the ES2015 section. So the line I used instead is import 'jsdom-global/register' and there’s no more TSlint warnings. I’d like to ask, when we use jsdom-global, are we officially using JSDOM or only a portion of it?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d