Wednesday, March 26, 2014

Polymer, Page Objects, and Jasmine 2.0


I am officially a Page Objects convert—especially when it comes to Polymer testing. After a bit of worry, I have Page Objects working in Dart even better that in JavaScript. This begs the question, can I apply some of the lessons learned from Dart back into JavaScript testing?

To answer that question, I am going to first make a slight change to my current Polymer JavaScript test setup. I have been using the Karma test runner, sticking with the default Jasmine test framework. Instead of the default Jasmine framework, I am going to switch to Jasmine 2.0, which (I think) has better support for asynchronous testing.

I start by adding the 2.0 Jasmine dependency to my package.json (the 2.0 dependency is karma-jasmine 0.2.0):
{
  "name": "model_example",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-watch": "~0.5.3",
    "karma-jasmine": "~0.2.0",
    "karma-chrome-launcher": ">0.0"
  }
}
After a quick npm install, I am ready to go.

First up, I need to fix my existing tests because waits(), waitsFor() and run() are gone in Jasmine 2.0:
Chrome 33.0.1750 (Linux) <x-pizza> adding a whole topping updates the pizza state accordingly FAILED
        ReferenceError: waitsFor is not defined
            at Object.<anonymous> (/home/chris/repos/polymer-book/play/model/js/test/PolymerSetup.js:14:3)
        ReferenceError: waitsFor is not defined
            at Object.<anonymous> (/home/chris/repos/polymer-book/play/model/js/test/XPizzaSpec.js:41:5)
        TypeError: Cannot read property 'wholeToppings' of undefined
            at Object.XPizzaComponent.addWholeTopping (/home/chris/repos/polymer-book/play/model/js/test/XPizzaSpec.js:11:29)
            at Object.<anonymous> (/home/chris/repos/polymer-book/play/model/js/test/XPizzaSpec.js:67:14)
That is fairly straight-forward. I need to replace waitsFor() with some equivalent that invokes Jasmine's done() callback. In this case, it is a nice improvement as this:
// Delay Jasmine specs until WebComponentsReady
beforeEach(function(){
  waitsFor(function(){
    if (typeof(CustomElements) == 'undefined') return false;
    return CustomElements.ready;
  });
});
Becomes simply:
beforeEach(function(done) {
  window.addEventListener('polymer-ready', done);
});
Learning a lesson from my Dart experience, I replace some ugly waitsFor() code with an async() call:
describe('<x-pizza>', function(){
  var container, xPizza;

  beforeEach(function(done){
    container = document.createElement("div");
    var el = document.createElement("x-pizza");
    container.appendChild(el);
    document.body.appendChild(container);

    xPizza = new XPizzaComponent(el);

    xPizza.el.async(done);
    // waitsFor(
    //   function() { return xPizza.currentPizzaStateDisplay() != ''; },
    //   "element upgraded",
    //   5000
    // );
  });
  // Tests here...
});
Just as with the Dart code, I realize that something is a little off here—I should not be directly accessing the el property of the XPizzaComponent Page Object. All interaction should go through the Page Object itself. The question is, how do I accomplish this without my beloved Futures?

Well, it ain't pretty, but enter callback hell:
describe('', function(){
  var container, xPizza;

  beforeEach(function(done){
    container = document.createElement("div");
    var el = document.createElement("x-pizza");
    container.appendChild(el);
    document.body.appendChild(container);

    xPizza = new XPizzaComponent(el);
    xPizza.flush(done);
  });
  // Tests go here...
});
As with the Dart code, the flush() method on the Page Object can invoke async() to get the Polymer platform to flush changes to observables, redraw things and then invoke the callback:
XPizzaComponent.prototype = {
  // ...
  flush: function(cb) {
    if (cb === undefined) cb = function(){};
    this.el.async(cb);
  }
};
Furthermore, I can invoke supplied callbacks via async() when supplied to Page Object interaction methods:
XPizzaComponent.prototype = {
  // ...
  addWholeTopping: function(v, cb) {
    // Choose options from select lists, click buttons, etc...
    return this.flush(cb);
  },

  flush: function(cb) {
    if (cb === undefined) cb = function(){};
    this.el.async(cb);
  }
};
So tests that use these page interaction methods can then be simplified to:
  describe('adding a whole topping', function(){
    beforeEach(function(done){
      xPizza.addWholeTopping('green peppers', done);
    });

    it('updates the pizza state accordingly', function(){
      var with_toppings = JSON.stringify({
        firstHalfToppings: [],
        secondHalfToppings: [],
        wholeToppings: ['green peppers']
      });

      expect(xPizza.currentPizzaStateDisplay()).
        toEqual(with_toppings);
    });
  });
To be sure, this is not as nice as the last night's Dart and Future based Page Object solution. Still, it is vastly superior (faster, more robust) than my previous take. Until Jasmine or some other testing framework supports promises, this will likely have to suffice. And for non-Dart code, it is not too bad.


Day #15

2 comments:

  1. Try out thus testing framework with Promise integration :)

    http://theintern.io/

    ReplyDelete
    Replies
    1. Check out this feature comparison ;) Jasmine is sooo 2011

      http://theintern.io/#compare

      Delete