Friday, May 2, 2014

Successfully Using Polymer Elements in Angular Tests


I have a reliable Karma / Jasmine test for my AngularJS that plays with Polymer. The only problem is that I don't understand it.

The test itself is… somewhat OK-ish as Angular directive tests go. I am attempting to verify the behavior in angular-bind-polymer. This directive should tell Angular that any Polymer element with the bind-polymer directive attribute should be observed for attribute changes. With that in place, angular-bind-polymer can double bind Polymer attributes to values in its own scope.

As I found last night, this still works in regular pages:
<pre ng-bind="answer"></pre>
<x-double bind-polymer in="2" out="{{answer}}"></x-double>
In there, the bind-polymer attribute on the <x-double> Polymer element signals to Angular that it should watch attributes with the mustache operator in it—in this case, Angular will bind the value of <x-double>'s out attribute to the local scope variable answer. Thus, the <pre> tag that is bound to that same scope variable will contain the answer from <x-double>

For the longest time, I have been unable to get this to work under test. I surmise that this is mostly because the test needs to wait for both Polymer and Angular to update their respective values, but I am having the darndest time just getting the test to work, let alone understand everything. Regardless, here is the working test in all of its glory:
describe('Double binding', function(){
  // Build in setup, check expectations in tests
  var ngElement;

  // Load the angular-bind-polymer directive
  beforeEach(module('eee-c.angularBindPolymer'));

  beforeEach(inject(function($compile, $rootScope, $timeout) {
    // Container to hold angular and polymer elements
    var container = document.createElement('div');
    container.innerHTML =
      '<pre ng-bind="answer"></pre>' +
      '<x-double bind-polymer in="2" out="{{answer}}"></x-double>';
    document.body.appendChild(container);

    // The angular element is the first child (the <pre> tag)
    ngElement = container.children[0];

    // Compile the document as an angular view
    $compile(document.body)($rootScope);
    $rootScope.$digest();

    // Must wait one event loop for ??? to do its thing
    var done = false;
    setTimeout(function(){ done = true; }, 0);
    waitsFor(function(){ return done; });

    // ??? has done its thing, so flush the current timeout
    runs(function(){ $timeout.flush(); });
  }));

  // The actual test
  it('sees values from polymer', function(){
    expect(ngElement.innerHTML).toEqual('4');
  });
});
Most of that is fairly typical of an Angular directive or view test. The need to inject $timeout and the further need to flush it after an event loop are atypical. Atypical and not well understood by me. Both were added as I was flailing about for a working solution and remain in place after I painstakingly removed cruft that was programming by coincidence. The $timeout flush and the need to wait one event loop are both required as the test fails without them.

My best (and really only) guess is that the $timeout flush is required because of a $timeout-based promise inside of the directive. I wait for Polymer to be ready with a timeout:
      // Helper to wait for Polymer to expose the observe property
      function onPolymerReady() {
        var deferred = $q.defer();

        function _checkForObserve() {
          polymer().observe ?
            deferred.resolve() :
            $timeout(_checkForObserve, 10);
        }
        _checkForObserve();

        return deferred.promise;
      }
I think I can live with that explanation. I would have preferred not to have needed the $timeout flush. I still have no explanation for why I have to wait a browser event loop before the flush, which is troubling.

I am also forced to modify the code solely so that it will work under test. When the directive looks up the Polymer, I am forced to include a second conditional check to determine if I have the right Polymer element:
      function polymer() {
        var all = document.querySelectorAll(element[0].nodeName);
        for (var i=0; i<all.length; i++) {
          if (all[i] == element[0]) return all[i];
          if (all[i].impl == element[0]) return all[i];
        }
      }
In real web pages, the load and evaluation is such that the first conditional finds the proper element. But, under test, the element is already decorated with Polymer attributes, necessitating the check against the original element instead of the Polymer-wrapped version.

I still have the nagging feeling that I am overlooking something. If nothing else, I seem to have a reasonable test coupled with an OK explanation for why that test works. Tomorrow I will push both by using this test to refactor my directive. Perhaps that will have the added benefit of exposing my remaining gaps in understanding what is really happening here.



Day #52

No comments:

Post a Comment