Tuesday, December 16, 2014

Improving Protractor Testing of Polymer Elements


I can barely test Polymer elements with Protractor. And yet, I still rather like it.

I may be misplacing my extreme gratitude over getting a working Polymer test. I had previously tried and failed to get the Intern, another WebDriver-based testing framework, to test Polymer elements. Still, it seems that at least one intrepid soul was able to make it work, so my gratitude may be personal rather than based on legitimate criteria. That said, my Protractor test is rather nice:
describe('<x-pizza>', function(){
  describe('element content', function(){
    beforeEach(function(){
      browser.ignoreSynchronization = true;
      browser.get('http://localhost:8000');
    });
    it('has a shadow DOM', function(){
      expect($('[unresolved]').waitAbsent()).toBeTruthy();

      var shadowRootHtml = browser.executeScript(
        'return document.querySelector("x-pizza").shadowRoot.innerHTML'
      );

      expect(shadowRootHtml).toMatch("<h2>Build Your Pizza</h2>");
    });
  });
});
The beforeEach() setup block disables Protractor's AngularJS-specific synchronization code and requests my test content (being served separately by a Python simple HTTP server):
    beforeEach(function(){
      browser.ignoreSynchronization = true;
      browser.get('http://localhost:8000');
    });
The actual test involves a little hackery due to WebDriver's lack of shadow DOM support. Still, it remains fairly readable:
    it('has a shadow DOM', function(){
      expect($('[unresolved]').waitAbsent()).toBeTruthy();

      var shadowRootHtml = browser.executeScript(
        'return document.querySelector("x-pizza").shadowRoot.innerHTML'
      );

      expect(shadowRootHtml).toMatch("<h2>Build Your Pizza</h2>");
    });
The expectation on the [unresolved] attribute (square brackets being the CSS attribute matcher) waits for the Polymer library to handle FOUC, thus proving that it is operating properly on the page. Additional hackery grabs the string representation of my element's shadow root for actually assertion.

Despite Protactor's claims to the contrary, it will not resolve standard HTML elements inside a shadow DOM to WebDriver WebElement objects. This assertion on an <h2> element inside my element's shadow DOM fails:
      // Fails with Error: Expected undefined to match '<h2>Build Your Pizza</h2>'.
      var shadowRoot = browser.executeScript(
        'return document.querySelector("x-pizza").shadowRoot.children[0]'
      );
      expect(shadowRoot.outerHTML).toMatch("<h2>Build Your Pizza</h2>");
But if I return the outerHTML string from executeScript(), then it passes:
      // This passes...
      var shadowRoot = browser.executeScript(
        'return document.querySelector("x-pizza").shadowRoot.children[0].outerHTML'
      );
      expect(shadowRoot).toMatch("<h2>Build Your Pizza</h2>");
So it seems that I am restricted to working with strings from executeScript(). That is a big pile of meh.

Instead, I would like to write that test as something along the lines of:
    it('has a shadow DOM', function(){
      expect($('[unresolved]').waitAbsent()).toBeTruthy();
      expect($('x-pizza').getShadowRootHtml()).
        toMatch("<h2>Build Your Pizza</h2>");
    });
I can get that with the following in my Protractor conf.js:
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['tests/XPizzaSpec.js'],
  onPrepare: function() {
    var ElementFinder = $('').constructor;

    ElementFinder.prototype.getShadowRootHtml = function() {
      var locator = this.locator();
      if (locator.using != 'css selector') return '';

      var selector = locator.value;
      return browser.executeScript(
        'return document.querySelector("' + selector + '").shadowRoot.innerHTML'
      );
    };

    require('./tests/waitAbsent.js');
  }
};
Some of this is based on the waitAbsent.js code required at the bottom of the onPrepare() block. I borrowed this code yesterday, which is how I know how to access the ElementFinder prototype. The rest of this code assumes that a CSS selector is being used. If that is the case, then I work through the same executeScript() hackery.

The difference here is that the hackery is now encapsulated outside of my test, which reads better and, more importantly, continues to pass:
$ protractor conf.js
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
.

Finished in 0.581 seconds
1 test, 2 assertions, 0 failures
That makes me happy. At least happier. I may continue this effort tomorrow to see if true testing happiness with Polymer and Protactor is possible.



Day #26

1 comment:

  1. Polymer team has been promoting their own testing tool > https://github.com/Polymer/web-component-tester
    I've been using it but I havent done anything really complex.
    Have you tried it? What are your thoughts?

    ReplyDelete