Monday, December 15, 2014

Protractor Can Test Polymer (Sort of)


I was unable to test Polymer elements with Protractor last night. But, thanks to some helpful suggestions this morning, I think it just might be possible after all.

My barrier last night was the seeming hard coupling between Protractor and AngularJS, which resulted in testing errors that indicated that Angular was not loaded:
  1) <x-pizza> element content has a shadow DOM
   Message:
     Error: Angular could not be found on the page http://localhost:8000/ : retries looking for angular exceeded
The solution suggested by various people is the undocumented ignoreSynchronization property on the Protractor browser object.

This property is meant to be set in the Protractor configuration or a beforeEach() block. I opt for the latter here:
describe('<x-pizza>', function(){
  var el;

  describe('element content', function(){
    beforeEach(function(){
      browser.ignoreSynchronization = true;
      browser.get('http://localhost:8000');
    });
    it('has a shadow DOM', function(){
      // Tests here...
    });
  });
Unfortunately, that does not solve all of my problems. I am still using last night's simple test—that the <x-pizza> Polymer element, being served by a Python simple HTTP server on port 8000, has a shadow root:
    it('has a shadow DOM', function(){
      el = $('x-pizza');
      expect(el.shadowRoot).toBeDefined();
    });
This simple test still fails:
Failures:

  1) <x-pizza> element content has a shadow DOM
   Message:
     Expected undefined to be defined.
The problem here is twofold. First, I have to wait for Polymer to have upgraded the <x-pizza> element on the page being served. Second, that el reference is not a regular DOM element—it is a WebDriver WebElement wrapper. Neither problem seems to be easily solved, though I am able to come up with a solution for each.

To wait for Polymer to be ready, I need to pull in an external library. Protractor has great support for waiting for Angular operations, but no support for waiting for other operations. Fortunately for me, others have had similar problems. I swipe one of those solutions, waitAbsent.js, from a Gist and save it in tests/waitAbsent.js. I then use it in my current setup by adding to the onPrepare section of my Protractor configuration:
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['tests/XPizzaSpec.js'],
  onPrepare: function() {
    require('./tests/waitAbsent.js');
  }
};
There is also a waitReady.js, but I specifically want to use a wait-until-absent operation for Polymer so that I can wait until Polymer has loaded and removed the unresolved attribute from the document:
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();
      el = $('x-pizza');
      expect(el.shadowRoot).toBeDefined();
  });
});
That will block the remainder of the test from being evaluated until the unresolved attribute is removed from the document. Polymer does this to deal with FOUC, so once that attribute is removed I know that Polymer is active on the page (this does assume that the author has added unresolved somewhere in the page).

Even waiting for Polymer to be active is not sufficient, however, because the el in that test is a WebElement wrapper. And to my intense consternation, there is no way to access the underlying element from the wrapper. This would be fine… if WebDriver had some way to access the shadow DOM for testing, but it does not. All of this leaves me resigned to hackery.

The hackery in this case comes in the form of executeScript(), which I use to execute a simple query of the element's shadow DOM, returning its the innerHTML:
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 shadowRoot = browser.executeScript(
        'return document.querySelector("x-pizza").shadowRoot.innerHTML'
      );
      expect(shadowRoot).toMatch("<h2>Build Your Pizza</h2>");
  });
});
And that actually works:
$ protractor conf.js
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
.

Finished in 0.621 seconds
1 test, 2 assertions, 0 failures

[launcher] 0 instance(s) of WebDriver still running
[launcher] chrome #1 passed
I know that is a legitimate test because I can make it fail by changing the expectation:
      expect(shadowRoot).toMatch("<h2>Build a Pizza</h2>");
Which results in:
Failures:

  1) <x-pizza> element content has a shadow DOM
   Message:
     Expected '
    <h2>Build Your Pizza</h2>
    <! ... -->
  ' to match '<h2>Build a Pizza</h2>'.
   Stacktrace:
     Error: Failed expectation
    at [object Object].<anonymous> (/home/chris/repos/polymer-book/play/protractor/tests/XPizzaSpec.js:27:26)

Finished in 0.608 seconds
1 test, 2 assertions, 1 failure
I should probably convert the working executeScript into another onPrepare addition to Protractor, but I will leave that to another day. For now, I suffer with functional hackery.

One note about that functional hackery is that I have to be extremely careful with the return value from executeScript() and the matcher used. I had originally tried to return a typeof() for it:
      var shadowRoot = browser.executeScript(
        'return typeof(document.querySelector("x-pizza").shadowRoot);'
      );
The result of that is "object", which seems OK, until there is a problem. If there is no shadow root for any reason, the shadowRoot local variable is assigned to a promise that does not resolve to unresolved. In other words, I might not have a shadowRoot, but the following will still pass:
    it('has a shadow DOM', function(){
      expect($('[unresolved]').waitAbsent()).toBeTruthy();
      var shadowRoot = browser.executeScript(
        'return typeof(document.querySelector("h1").shadowRoot);'
      );
      expect(shadowRoot).toBeDefined();
    });
That already bit me during my investigation, so it is sure to cause problems down the line—all the more reason to want shadow DOM support of some kind in WebDriver. But, at least for one night, I was able to write a successful Protractor test for a Polymer element.


Day #25

2 comments:

  1. Testing non angular sites with Protractor http://ng-learn.org/2014/02/Protractor_Testing_With_Angular_And_Non_Angular_Sites/

    ReplyDelete
    Replies
    1. Thanks! I read that article (well skimmed it) and did use some of the techniques listed in there, but I don't think anything in there will yield access to the shadow DOM, which is really holding me back at this point. I'll go back and try to read in detail tonight or tomorrow -- I definitely could have missed something...

      Delete