DailyJS

Let's Make a Framework: jsdom

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks lmaf npm

Let's Make a Framework: jsdom

Posted by Alex R. Young on .
Featured

tutorials frameworks lmaf npm

Let's Make a Framework: jsdom

Posted by Alex R. Young on .
*Let's Make a Framework* is an ongoing series about building a JavaScript framework from the ground up. These articles are tagged with [lmaf](http://dailyjs.com/tags.html#lmaf). The project we're creating is called [Turing](http://github.com/alexyoung/turing.js). Documentation is available at [turingjs.com](http://turingjs.com/).

Last week we adapted Turing to load appropriate modules using npm. It's also possible to adapt the
DOM-related code to work using jsdom.

jsdom

jsdom by Elijah Insua is a CommonJS implementation of the DOM. This isn't a trivial module, and thankfully
is extremely easy to use thanks to the recently introduced
jsdom.env() method.

There are several different ways to invoke env, but I like
passing an object of options:

jsdom.env({
  html: 'Some HTML, it could be read from a file.',
  src: [ fileName1, fileName2 ],
  done: function(err, window) {
    if (err) throw(err);
    fn(window.turing);
  }
});

The done callback will be run when the source files have
been read. The callback takes two arguments, errors and
window. The window object is what we're
looking for.

Adding the Module

The package.json file must be edited to include the new
dependency:

// ...
, "dependencies": {
    "jsdom": "0.2.x"
  }

I've set it to 0.2.x because I expect the API won't be too
volatile within this version range.

Using jsdom

I'd have liked to be able to use turing('

html

'), but
I reasoned that this was confusing given that this function is
effectively overloaded. I decided using a separate function would make
things a lot clearer; allowing the reader to distinguish between a
typical framework invocation and the case where jsdom has
been used.

I've named this function browser -- I think this makes the
intent fairly clear. I wrote a short test to get a feel for a potential
API:

turing.browser('JSDOM\'s Homepage', function(turing) {
  assert.equal(1, turing('p').length);
});

As this is a framework-provided method, I decided to wrap
jsdom.env's callback and pass in the turing
function, rather than errors and window.

I found that done seemed to fire multiple times unless I
read all the JavaScript first:

module.exports.browser = function(html, fn) {
  var js = '',
      files = ('turing.core.js turing.oo.js turing.enumerable.js turing.promise.js '
              + 'turing.functional.js turing.dom.js turing.plugins.js turing.events.js '
              + 'turing.net.js turing.touch.js turing.anim.js').split(' ');

  files.forEach(function(file) {
    js += fs.readFileSync(__dirname + '/../' + file);
  });

  // The JavaScript in as an array seems to make `done` fire twice
  jsdom.env({
    html: html,
    src: [ js ],
    done: function(err, window) {
      if (err) throw(err);
      fn(window.turing);
    }
  });
};

In the end I decided to make it read all of the files in the framework.

Events

When I first wrote the previous test I only loaded the DOM and core
modules. Then I realised jsdom should also be able to cope
with events. To test events work, I tried triggering a simple click
handler:

turing.browser('JSDOM\'s Homepage', function(turing) {
  var triggered = 0;
  assert.equal(1, turing('p').length);
  turing('a').bind('click', function() {
    triggered++;
  });
  turing.events.fire(turing('a')[0], 'click');
  assert.equal(1, triggered);
});

This actually passes and demonstrates that jsdom.env is
fine for both querying the DOM and simulating events.

Conclusion

If you've got a cool client-side script and want to distribute it with
npm, wrapping certain functionality with jsdom might provide people with
new ways to use your code. The most obvious example is screen scraping,
but it would also be convenient for writing tests that run in the
console, or maybe for indexing documents in a search engine.

I've committed these changes in
98ec3af and published the npm module.