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!
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?
Hi Eiswind,
Sorry, you’re totally correct, it’s missing some things from its setup (full config at: https://github.com/sunshower-io/aurelia-aire/blob/master/aire/src/test/setup.ts). I’ll update the Gist. Thanks for pointing that out.
Thanks that was really useful, just a quick note with TypeScript, I changed the
require
toimport
as it’s mentioned on thejsdom-global
GitHub/NPM, read the ES2015 section. So the line I used instead isimport 'jsdom-global/register'
and there’s no more TSlint warnings. I’d like to ask, when we usejsdom-global
, are we officially using JSDOM or only a portion of it?Hi Ghislain,
Sorry, this post needs to be updated since I’ve found a much better way. Please feel free to copy the configurations in: https://github.com/sunshower-io/aire/tree/v2/src/test. I’ll get around to updating this post as soon as I can, but please don’t hesitate to ping me with any questions