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