Wednesday, December 24, 2014

TDDing New Polymer Element Features with Protractor


Even after a week or so of working with Protractor as testing solution for Polymer, I still do not know if I have a good feel for recommending it as a solution. I begin to understand its strengths and weaknesses as a Polymer testing tool—its async support works brilliantly with Polymer while its complete lack of shadow DOM support is nearly a show stopper.

Nearly a show stopper, but when I can write tests like this:
describe('<x-pizza>', function(){
  beforeEach(function(){
    browser.get('http://localhost:8000');
  });
  it('updates value when internal state changes', function() {
    new XPizzaComponent().
      addFirstHalfTopping('pepperoni');

    expect($('x-pizza').getAttribute('value')).
      toMatch('pepperoni');
  });
});
How can I dismiss it? To be sure, the XPizzaComponent Page Object helps to mask some ugliness—but the same test in Karma has just as much shadow DOM ugliness, plus async ugliness.

I have already tried TDDing a bug fix with Protractor, which went reasonably well. So tonight I wonder what it is like TDDing a new feature? I have been playing with an early version of the <x-pizza> pizza building Polymer element as I explore these questions:



This version of the element has the exploration advantage of including some old-timey HTML form elements with which to play. The advantage here is that I made old-timey mistakes in the backing code. The bug that I TDD'd the other day resulted from adding unknown toppings to the pizza. I eliminated the bug, but it is still possible to click the “Add First Half Topping” button without choosing a topping. I would like to change that. So I write a Protractor test:
describe('<x-pizza>', function(){
  beforeEach(function(){
    browser.get('http://localhost:8000');
  });
  // ...
  it('does nothing when adding a topping w/o choosing first', function(){
    new XPizzaComponent().
      clickAddFirstHalfToppingButton();

    expect($('x-pizza').getAttribute('value')).
      toEqual('\n\n');
  });
});
I need to write the clickAddFirstHalfToppingButton() method for my page object. This is already done as part of the addFirstHalfTopping() method, but now I need it separate:
function XPizzaComponent() {
  this.selector = 'x-pizza';
}

XPizzaComponent.prototype = {
  // ...
  clickAddFirstHalfToppingButton: function() {
    browser.executeScript(function(selector){
      var el = document.querySelector(selector),
          button = el.$.firstHalf.querySelector("button");
      button.click();
    }, this.selector);
  }
};
The executeScript() method is hackery to work around Protractor's lack of shadow DOM support. It is not too horrible, but I am extremely tempted to factor the el assignment out into its own method, especially since both addFirstHalfTopping() and clickAddFirstHalfToppingButton() both assign it in the exact same way:
XPizzaComponent.prototype = {
  addFirstHalfTopping: function(topping) {
    browser.
      executeScript(function(selector, v){
        var el = document.querySelector(selector),
            select = el.$.firstHalf.querySelector("select");
      }, this.selector, topping);
  },

  clickAddFirstHalfToppingButton: function() {
    browser.
      executeScript(function(selector){
        var el = document.querySelector(selector),
            button = el.$.firstHalf.querySelector("button");
        // ...
      }, this.selector);
  }
};
The problem with trying to factor this out is that I am obtaining the element inside an anonymous function that is called by browser.executeScript() and the reason that I am doing this is because regular Protractor locators cannot find Polymer elements. The best that I can come up with is:
XPizzaComponent.prototype = {
  el: function(){
    return browser.executeScript(function(selector){
      return document.querySelector(selector);
    }, this.selector);
  },
  // ...
  clickAddFirstHalfToppingButton: function() {
    browser.
      executeScript(function(el){
        var button = el.$.firstHalf.querySelector("button");
        button.click();
      }, this.el());
  }
});
This is not too horrible, I suppose. But I am writing as much test code (and working as hard to write it) as I am with the actual code.

Regardless, I have a failing test:
$ protractor --verbose
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
<x-pizza>
  has a shadow DOM - pass
  updates value when internal state changes - pass
  syncs <input> values - pass
  does nothing when adding a topping w/o choosing first - fail

Failures:

  1) <x-pizza> does nothing when adding a topping w/o choosing first
   Message:
     Expected 'unknown

' to equal '

'.
   Stacktrace:
     Error: Failed expectation
    at [object Object].<anonymous> (/home/chris/repos/polymer-book/play/protractor/tests/XPizzaSpec.js:30:7)

Finished in 2.032 seconds
4 tests, 4 assertions, 1 failure
I can make that pass easily enough. My Polymer element's addFirstHalf() method simply needs a guard clause:
Polymer('x-pizza', {
  // ..
  addFirstHalf: function() {
    if (this.currentFirstHalf == '') return;
    this.model.firstHalfToppings.push(this.currentFirstHalf);
  },
  // ...
});
With that, I have my passing test:
$ protractor --verbose
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
<x-pizza>
  has a shadow DOM - pass
  updates value when internal state changes - pass
  syncs <input> values - pass
  does nothing when adding a topping w/o choosing first - pass

Finished in 2.012 seconds
4 tests, 4 assertions, 0 failures
And a nice new feature.

I normally do not care for refactoring tests, so the difficulty experienced tonight really should not bother me. But it does. The lack of shadow DOM support makes me think that I will need to write many helpers—if not an entire library—to make Protractor and Polymer really play nicely. If I struggle with the simple stuff like getting the Polymer element itself, this does not bode well for more in-depth testing adventures. I may play with this some more over the next few days, but I am leaning toward not using Protractor at this point.


Day #34

1 comment: