Saturday, August 9, 2014

Testing Paper (JavaScript Polymer)


My tests fail. That is hardly cause for concern—I did just switch my <x-pizza> Polymer element from a single display of a bunch of drop-down elements to a Paper-based tabs approach. And, since I am using Page Objects to wrap interaction with my custom element, this should be an easy fix.

“Should.”

Tests fail with messages like:
Expected '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":[]}'
to equal '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":["green peppers"]}'.
That hardly seems a problem—my page object no longer knows how to add toppings. It had been taught to add toppings with the old drop down lists:



Now I need to teach the page object how to choose the appropriate tab and choose the corresponding ingredient from last night's Paper interface:



To fix this, I should only have to change the addWholeTopping() call in the Jasmine test setup:
  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);
    });
  });
The end result should be the same: the JSON representation should have green peppers on the entire pizza.

When interacting with this on the page, I have to click the tab, then click the green peppers from the pizza ingredients list. In Jasmine + Polymer + Page Objects parlance, that should look something like:
XPizzaComponent.prototype = {
  // ...
  addWholeTopping: function(v, cb) {
    // Click the tab:
    this.el.$.whole_tab.click();

    // Click green peppers in the active pizza toppings element:
    this.el.shadowRoot.
      querySelector('x-pizza-toppings[active]').
      $.green_peppers.
      click();

    return this.flush(cb);
  }
  // ...
};
Except this still fails. It fails, but I have succeeded in changing the error message. So, I am probably on the right track. The failure now includes green peppers in the result, but on the first half, not the entire pizza:
Chrome 36.0.1985 (Linux) <x-pizza> adding a whole topping updates the pizza state accordingly FAILED
        Expected '{"firstHalfToppings":["green peppers"],"secondHalfToppings":[],"wholeToppings":[]}'
        to equal '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":["green peppers"]}'.
My first thought here is that I am not giving the tab click time enough to complete such that the whole toppings element is active. If that is the case, then clicking green peppers on the currently active tab would add from the first half toppings since it is active by default. That seems a perfectly reasonable explanation. So of course it is wrong.

Even if I add absurd amounts of delay before clicking green peppers in the active toppings selector, the error message does not change. So the problem must lie elsewhere. After some trial and error, I eventually find the problem: clicking the whole toppings tab is broken.

Well, not so much broken as incorrect. It turns out that Paper / Core Polymer elements tend to use mouse up/down events rather than click events. So I have to replace the nice, clean click() call with messy initEvents calls:
  addWholeTopping: function(v, cb) {
    // Click the tab:
    var evt1 = document.createEvent("MouseEvents");
    evt1.initEvent("mousedown", true, true);
    this.el.$.whole_tab.dispatchEvent(evt1);

    var evt2 = document.createEvent("MouseEvents");
    evt2.initEvent("mouseup", true, true);
    this.el.$.whole_tab.dispatchEvent(evt2);

    // Click green peppers in the active pizza toppings element:
    this.el.shadowRoot.
      querySelector('x-pizza-toppings[active]').
      $.green_peppers.
      click();

    return this.flush(cb);
  }
With that, I… still get the same exact failure. The green peppers are still being added to the first half. It turns out that a slight delay is needed after all to allow the selected tag to change the current page. This only requires a single event loop, so I setTimeout() for zero milliseconds:
  addWholeTopping: function(v, cb) {
    // Click the tab:
    var evt1 = document.createEvent("MouseEvents");
    evt1.initEvent("mousedown", true, true);
    this.el.$.whole_tab.dispatchEvent(evt1);

    var evt2 = document.createEvent("MouseEvents");
    evt2.initEvent("mouseup", true, true);
    this.el.$.whole_tab.dispatchEvent(evt2);

    // Click green peppers in the active pizza toppings element:
    var that = this;
    setTimeout(
      function(){
        that.el.shadowRoot.
          querySelector('x-pizza-toppings[active]').
          $.green_peppers.
          click();

        return that.flush(cb);
      },
      0
    );
  },
With that, I have my test passing.

(The flush() call at the end of that method works with Polymer's async() and Jasmine's done() to ensure proper timing of tests with this element.)

I greatly appreciate making no changes to the actual test code. It still reads exactly as it did when I started. I only needed to change the addWholeTopping() method to accommodate changes to that area of my Polymer element. This feels like a nice win. I would much rather send a simple click message to my Polymer elements, but I can live with the mouse events now that I know about them.


Day #147

No comments:

Post a Comment