Tuesday, March 18, 2014

WaitsFor Polymer-Ready… FOREVER!


Tonight, I need to figure out how to test my <x-pizza> Polymer element. Really, I should have done this last night before refactoring, but sometimes the siren song of shiny new refactoring is too strong to resist. Thankfully there is always tomorrow. And by tomorrow, I mean today.

This version of the element is only a few select lists and and a JSON representation of the current pizza state:



Later on in Patterns in Polymer I give this an SVG makeover. For tonight, I only need test adding pizza toppings and checking expectations on the pizza state.

I start with the default value of the pizza state, which should be a JSON representation of a pizza with no toppings. In Jasmine-ese:
  describe('defaults', function(){
    it('has no toppings anywhere', function() {
      var el = document.querySelector('x-pizza');
      var pre = el.shadowRoot.querySelector('pre');

      var no_toppings = JSON.stringify({
        firstHalfToppings: [],
        secondHalfToppings: [],
        wholeToppings: []
      });

      expect(pre.textContent).toEqual(no_toppings);
    });
  });
I am fairly sure that this is the right test, but… it always fails due to the pizza state <pre> element being blank:
Chrome 33.0.1750 (Linux) <x-pizza> defaults has no toppings anywhere FAILED
        Expected '' to equal '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":[]}'.
        ...
It seems that my test setup is not robust enough to handle a Polymer that requires child elements to load. I had been using a beforeEach() that waited for a single JavaScript event loop to allow the tested Polymer to ready itself for testing:
  beforeEach(function(){
    container = document.createElement("div");
    container.innerHTML = '';
    document.body.appendChild(container);
    waits(0); // One event loop for elements to register in Polymer
  });
If I futz with the waits() amount, I eventually find that 250ms is sufficient time to get this working. Sticking with a hard-coded value like that would be silly. It is clearly dependent on my machine and the current complexity of my <x-pizza> Polymer element. If I tried running the test on a slower machine or if I ever add more complexity, this waits() amount would no longer be sufficient and I would have a falsely failing test on my hands.

To get this to work, I first try to waitsFor() a polymer-ready event:
  beforeEach(function(){
    var _isPolymerReady = false;
    window.addEventListener('polymer-ready', function(e) {
      _isPolymerReady = true;
    });

    container = document.createElement("div");
    container.innerHTML = '';
    document.body.appendChild(container);

    waitsFor(
      function() {return _isPolymerReady;},
      "polymer-ready",
      5000
    );
  });
The trouble is that this event never fires:
timeout: timed out after 5000 msec waiting for polymer-ready
More accurately, it has already fired by the time my test setup is run. In reality, I do not want my test to wait for polymer-ready, I want my test to wait until this particular Polymer element is ready. And really, even that is not quite right. What I really need is to wait until the Polymer element is ready, the child elements are all ready, the internal pizza state has been updated, and the bound variable in the template has been updated.

Ugh. There is not exactly a lifecycle callback or event for that. In the end, I find that I have to craft a waitsFor() that blocks for two things: the Polymer $ property to be present and for the <pre> element to have content. The beforeEach() setup block for that is:
  beforeEach(function(){
    container = document.createElement("div");
    el = document.createElement("x-pizza");
    container.appendChild(el);
    document.body.appendChild(container);

    waitsFor(
      function() {
        return el.$ !== undefined &&
               el.$.state.textContent != '';
      },
      "element upgraded",
      5000
    );
  });
I am not particularly thrilled to be waiting for the value being tested, el.$.state.textContent, to be present.

It gets even worse when I try to select an option from the child element's select list:
  describe('adding a whole topping', function(){
    it('updates the pizza state accordingly', function(){
      var toppings = el.$.wholeToppings,
          select = toppings.$.ingredients,
          button = toppings.$.add;

      // Polymer won't see a select list change without a change event...
      select.selectedIndex = 3;
      var event = document.createEvent('Event');
      event.initEvent('change', true, true);
      select.dispatchEvent(event);

      button.click();

      var old = el.$.state.textContent;
      waitsFor(function(){return el.$.state.textContent != old;});
      runs(function(){
        var with_toppings = JSON.stringify({
          firstHalfToppings: [],
          secondHalfToppings: [],
          wholeToppings: ['green peppers']
        });

        expect(el.$.state.textContent).toEqual(with_toppings);
      });
    });
  });
Here, I change the value of one of the <select> menus, click the “Add Topping” button and expect that the chosen topping has been added to the current state of the pizza. In addition to again being required to waitsFor() the <pre> element to update, I find that I also have to manually trigger a change event in the <select> list. Without this event, Polymer does not update the bound variable associated with the <select> list, which prevents the topping from being added to the pizza.

All of this feels much harder than it ought to be. I am satisfied that I have crafted relatively robust tests, but they feel like more work that should be necessary. I will likely take another run at these two tests in another day or so. I like the acceptance test feel of querying the resulting <pre> tag, but some of this would go away if I queried the pizzaState property directly rather than setting the expectation on an element that is data bound to pizzaState. I may also pull in jQuery—at least in my tests—to make some of the event code a little easier. Grist for exploration tomorrow...


Day #7

7 comments:

  1. Why not recursively check for the presence of the parts you need or even unresolved className on the body.

    ReplyDelete
  2. https://gist.github.com/Nevraeka/754ced49e50e14cb4bc2

    ReplyDelete
    Replies
    1. That would work and reads well, but I'm unsure if this is tangibly better than using the waitsFor() function built into Jasmine. I actually like the procedural nature of waits(), waitsFor(), and run() in Jasmine, but that's just a personal preference.

      My real problem is what goes inside the waitsFor() / _polymerReady() functions. For an end-to-end / acceptance test, I want to set the expectation on the bound variable (inside the pre#state element in this case), but Polymer seems to take its sweet time to update those. My only choice then seems to be wait for the element on which I set my expectation to change (which is what I wound up doing) or to add an arbitrary delay. Neither is particularly palatable :-\

      Delete
    2. Yeah..I see your points here - essentially it is the same hack. I hate delayed checks for the DOM in general because of the varied time browsers take to perform the same functionality. Since Jasmine has a waits solution baked in I guess it makes sense to use it rather than a custom recursive function that new devs on the project may not see why you implemented it.....+1 to you and the icky waitsFor. :)

      Delete
  3. Prob need a different conditional but seems like it would work

    ReplyDelete
  4. I'm going to try adding mutation observers in a Polymer code base I'm working on now to help offset the need for this. Not sure if it will help but seems less icky

    ReplyDelete