Tuesday, March 4, 2014

Karma/Jasmine Testing of Polymer Ready


Time to get serious about testing Polymer. The esteemed Rob Dodson was kind enough to point out a mistake in Patterns in Polymer. I am going to fix it—with a test.

The problem was reported in the Theming chapter of the book, though I suspect that I introduced it elsewhere. In the <pricing-plans> element, I do a bit of Bootstrap calculation when the Polymer is ready:
Polymer('pricing-plan', {
  // ...
  ready: function() {
    this.size = 12 / this.parentElement.childElementCount;
  },
  // ...
});
The problem is that I am making this calculation when the <pricing-plan> Polymer is ready—not when it has been added to the parent element. It makes all kinds of sense for the Polymer to wait until it has been added to a parent before performing calculations dependent on that parent.

For what it's worth, I got away with this in the book for two reasons: first I defined the elements in the parent template HTML so, even before the Polymer is ready, there were elements there. Second, I skip over this method in the book. Still, it is poor form at best and buggy at worst so I ought to fix it with a test.

I postulate that I can trigger a bug if I dynamically create this Polymer and add it to the parent (which a perfectly reasonable thing to do in Polymer). I already have Karma / Jasmine test setup that adds one element to a container/parent element:
describe('<pricing-plan>', function(){
  var container;
  beforeEach(function(){
    container = document.createElement("div");
    container.innerHTML = __html__['test/pricing-plan-fixture.html'];
    document.body.appendChild(container);
    waits(0); // One event loop for elements to register in Polymer
  });

  afterEach(function(){
    document.body.removeChild(container);
  });

  // tests here...
});
Let's see how hard it is to get a test that proves my mistake. First a test that confirms that adding the Polymer as HTML actually works—either directly in the document or, as I do in the setup, by setting the innerHTML. I query for the innerHTML added Polymer and check that its size is 12:
  describe('size', function(){
    var el;
    beforeEach(function(){
      el = document.querySelector('pricing-plan');
    });

    it('is 12 when then only sibling', function(){
      expect(el.size).toEqual(12);
    });
  });
That passes. So what about dynamically creating the element to be added to the document? I enter that as:
  describe('size', function(){
    var el;
    beforeEach(function(){
      el = document.querySelector('pricing-plan');
    });
    // ....
    it('is 6 when one of two siblings', function(){
      var el_2 = document.createElement('pricing-plan');
      container.appendChild(el_2);
      expect(el_2.size).toEqual(6);
    });
  });
And, indeed, that fails just as Rob's comment suggested that it would:
Chrome 33.0.1750 (Linux) <pricing-plan> size is 6 when one of two siblings FAILED
        TypeError: Cannot read property 'childElementCount' of null
            at Polymer.ready (/home/chris/repos/polymer-book/book/code-js/theming/elements/pricing_plan.js:9:40)
            at e.prepareElement (/home/chris/repos/polymer-book/book/code-js/theming/bower_components/polymer/polymer.js:29:9906)
            at e.createdCallback (/home/chris/repos/polymer-book/book/code-js/theming/bower_components/polymer/polymer.js:29:9684)
            at j (/home/chris/repos/polymer-book/book/code-js/theming/bower_components/platform/platform.js:32:8557)
            at g (/home/chris/repos/polymer-book/book/code-js/theming/bower_components/platform/platform.js:32:8165)
            at f (/home/chris/repos/polymer-book/book/code-js/theming/bower_components/platform/platform.js:32:8019)
            at new <anonymous> (/home/chris/repos/polymer-book/book/code-js/theming/bower_components/platform/platform.js:32:9081)
            at HTMLDocument.p [as createElement] (/home/chris/repos/polymer-book/book/code-js/theming/bower_components/platform/platform.js:32:9131)
            at null.<anonymous> (/home/chris/repos/polymer-book/book/code-js/theming/test/PricingPlanSpec.js:50:27)
Cool beans. So I just need to add the parent-dependent size calculation to the enteredView() lifecycle method:
Polymer('pricing-plan', {
  size: 1,
  // ....
  enteredView: function() {
    this.size = 12 / this.parentElement.childElementCount;
    // ...
  },
  // ....
});
And now everything passes:
FAILED <pricing-plan> size is 6 when one of two siblings debug.html:28
Expected 1 to equal 6.
Error: Expected 1 to equal 6.
    at new jasmine.ExpectationResult (http://localhost:9876/absolute/home/chris/local/node-v0.10.20/lib/node_modules/karma-jasmine/lib/jasmine.js:114:32)
D'oh!

It takes me a while to realize that the number 1 was coming from the default instead of some weird calculation based on parentElement.childElementCount. Some quick debugging reveals that the size is being calculated correctly, but it is happening in the browser event loop after I dynamically add my Polymer to the test page. To wait for a single event loop, I make use of Jasmine's wait() and run(). These two need to be used together—the wait() adds a delay before running the next synchronous code block specified by run().

All I need do here is wait for zero milliseconds—enough time for the next event loop to run the enteredView() code:
    it('is 6 when one of two siblings', function(){
      var el_2 = document.createElement('pricing-plan');
      container.appendChild(el_2);
      waits(0);
      runs(function(){
        expect(el_2.size).toEqual(6);
      });
    });
With that, I finally have my passing test:
SUCCESS <pricing-plan> size is 6 when one of two siblings
The last thing that I need to do is restore my original code. This test is worse than useless if it does not fail with the original code. It would be very easy to assume that my test accurately describes the problem even though I had to change the test to see the desired messages. But it is entirely possible that, during the course of getting the right test messages I inadvertently lost something fundamental in the test. If that happened, I would have a test purporting to describe a feature when the feature could very well be broken.

Happily that is not the case tonight. This test fails when the code is reverted. The test fails with the updated code. So I am good to go. Now I just need to fix this in a couple of other places in the book...


Day #1,044

5 comments:

  1. Thanks for posting this! I should also point out that enteredView is deprecated in favor of attached().

    Living on the bleeding edge :D

    ReplyDelete
  2. If enteredView() has been deprecated in favour of attached(), then it hasn't made it's way into the Dart version yet. Even when running the very latest using >=0.10.0-pre.0 <0.11.0 as the version constraints for the polymer library, which will give you the latest if you put this in the pubspec.yaml file.

    ReplyDelete
    Replies
    1. Hm, I'll ping the dart peeps to see what's up

      Delete
    2. http://dartbug.com/17385
      we were kind of waiting for it to settle down :). For a while there it seemed like another rename might happen.

      also FYI -- https://api.dartlang.org/apidocs/channels/be/#dart-dom-html.Element@id_enteredView comes from dart:html not the Dart polymer package.

      Delete
  3. Hi, Chris!
    Did you know about the "karma-polymer" npm module? I just found it when I was reading your book (I had some problems trying to run the tests from chapter 11).

    ReplyDelete