Monday, March 24, 2014

Worky Scheduled Polymer Page Objects


Testing semi-complex Polymer.dart elements certainly has proved troublesome.

I am still working on my <x-pizza> pizza builder. The UI is not terribly complex:



The semi-complexity comes from the makeup of the Polymer element's <template>, which pulls in locally defined <x-pizza-toppings> sub-components:
<link rel="import" href="x-pizza-toppings.html">
<polymer-element name="x-pizza">
  <template>
    <h2>Build Your Pizza</h2>
    <pre id="state">{{pizzaState}}</pre>
    <x-pizza-toppings id="firstHalfToppings"
                      name="First Half Toppings"
                      ingredients="{{ingredients}}"></x-pizza-toppings>
    <x-pizza-toppings id="secondHalfToppings"
                      name="Second Half Toppings"
                      ingredients="{{ingredients}}"></x-pizza-toppings>
    <x-pizza-toppings id="wholeToppings"
                      name="Whole Toppings"
                      ingredients="{{ingredients}}"></x-pizza-toppings>
    <p class=help-block>
      Build Your Pizza!
    </p>
  </template>
  <script type="application/dart" src="x_pizza.dart"></script>
</polymer-element>
This is not too complex—just complicated enough to warrant the “semi-complex” label. And to give me fits testing.

Most of my fits are due to the asynchronous nature of Polymer elements—especially when sub-components come into play. I had hoped that relying on scheduled_test, which is a unittest replacement with nice asynchronous support, might solve my problems. But no.

James Hurford was kind enough to give me a hint about readying Polymer elements for testing. He suggested that, after the element was created:
  setUp((){
    schedule((){
      _el = createElement('<x-pizza></x-pizza>');
      document.body.append(_el);

      xPizza = new XPizzaComponent(_el);
    });
    // ...
  });
That I could schedule an async call which would fire after updating the UI. That seems like it might be right up my alley:
  setUp((){
    schedule((){ /* Create element... */ });

    schedule((){
      var _completer = new Completer();
      _el.async((_)=> _completer.complete());
      return _completer.future;
    });
  });
That actually helps quite a bit, but it does not solve all of my problems. It really seems as though Polymer is extremely sensitive to timing. I had accidentally left a delay in the my setup:
  setUp((){
    schedule((){ /* Create element... */ });

    schedule((){
      var _completer = new Completer();
      _el.async((_)=> _completer.complete());
      return _completer.future;
    });

    schedule(()=> new Future.delayed(new Duration(milliseconds: 750)));
  });
When removed, everything started working. I have no idea why that delay would prevent my tests from seeing changes in the Polymer, but that is how the failures would manifest—the original values would show rather than updates in response to page updates.

I also performed a bit of code cleanup in the Polymer code itself. Specifically, I found it best to listen for events in the enteredView() (will eventually be renamed attached()) callback:
@CustomTag('x-pizza')
class XPizza extends PolymerElement {
  // ...
  XPizza.created(): super.created();
  enteredView() {
    super.enteredView();

    on['topping-change'].listen((_){
      updatePizzaState();
    });
    updatePizzaState();
  }
  // ...
}
That code had been in the created() constructor, but it seemed more robust in enteredView(). Specifically, it worked with multiple elements and tests without random failures.

So it took a bit (and much thanks to James for the tip), but I finally have a robust test suite for this Polymer element.


Day #13

2 comments:

  1. It helps, but It's not perfect. I actually tried using it on a composite component, and found it was not working for me for testing sub polymer components, so I ended up adding another async() call for that sub polymer element.

    el.async((_) {
    el.shadowRoot.querySelector("resident-view").async((_) {
    completer.complete();
    });
    });

    This is actually a component with a list of subcomponents, but I find only the first one needs the async() call, as I think the rest seem to be synced when the first is finished, I think, maybe. Only time will tell.

    So don't create complex components, with many subcomponents to test if you wish to avoid this :-(

    ReplyDelete
    Replies
    1. Yah, I'm still concerned about that 750ms delay, which really needs to be there. Without it, or with a smaller delay (e.g. 250ms), the tests fail regularly. With that delay, the tests are 100% rock solid. It is wonderful to have the tests passing, but not if something in there makes no sense to me. I think I need to explore those 750ms tonight.

      Delete