Tuesday, March 25, 2014

OK Polymer, Why 500ms?


I hate to look a gift horse in the mouth, but...

After last night, I have my <x-pizza> pizza builder element well tested in both JavaScript and now Dart. Having struggled with the Dart testing in particular, it is extremely satisfying to run my tests and always see passing tests:
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: [defaults] it has no toppings
CONSOLE MESSAGE: PASS: [adding toppings] updates the pizza state accordingly
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 2 tests passed.
CONSOLE MESSAGE: unittest-suite-success
CONSOLE WARNING: line 213: PASS
And yet…

I only have this passing because I added a delay of 500ms to my test schedule:
    test('updates the pizza state accordingly', (){
      var toppings = JSON.encode({
        'firstHalfToppings': [],
        'secondHalfToppings': [],
        'wholeToppings': ['green peppers']
      });

      schedule(()=> xPizza.addWholeTopping('green peppers'));

      schedule(()=> new Future.delayed(new Duration(milliseconds: 500)));

      schedule((){
        expect(
          xPizza.currentPizzaStateDisplay,
          equals(toppings)
        );
      });
    });
Without that delay—or even if I try to decrease that delay—the test fails.

And so that delay just sits there, slowing down my test suite. Worse, it clutters up a pretty test, which would otherwise be super easy to read thanks to Page Objects (xPizza is a Page Object describing how to interact with the element) and scheduled_test, which enables scheduling asynchronous actions to run in sequence. And I cannot explain why this is even needed.

Well, I have some idea. Choosing a topping from the <x-pizza> pizza builder requires choosing an item from a <select> menu in a child Polymer element:



The <select> is in the “private” <x-pizza-toppings> element. This element fires a custom event to notify <x-pizza> that a change occurred:
@CustomTag('x-pizza-toppings')
class XPizzaToppings extends PolymerElement {
  // ...
  XPizzaToppings.created(): super.created() {
    model.
      changes.
      listen((_)=> fire('topping-change'));
  }
}
The parent <x-pizza> element listens for that event, updating the internal state accordingly:
@CustomTag('x-pizza')
class XPizza extends PolymerElement {
  XPizza.created(): super.created();
  enteredView() {
    super.enteredView();
    on['topping-change'].listen(updatePizzaState);
  }

  updatePizzaState([_]) {
    pizzaState = JSON.encode({
      'firstHalfToppings': $['firstHalfToppings'].model,
      'secondHalfToppings': $['secondHalfToppings'].model,
      'wholeToppings': $['wholeToppings'].model
    });
  }
}
My initial suspicion is that the various events and updates are taking too darn long for some unknown reason. To get to the bottom of this, I bring to bear my wide range of very sophisticated debugging techniques… OK, OK I scatter print() statements everywhere, but I started with the Chrome debugger for Dart, so that's got to count for something, right? Ahem.

Anyway, I eventually discern that, even with no scheduled delay at all, the internal state of <x-pizza> is being updated—it is just not updating the bound variables so that my test can see it.

What I need is a way to schedule a pause until the Polymer element has redrawn itself with the appropriate data—however long that might take. But that's just what James Hurford's suggestion from last night did! And, sure enough, if I replace the arbitrary wait for 500ms with an async() redraw action:
    test('updates the pizza state accordingly', (){
      var toppings = JSON.encode({ /* ... */ });

      schedule(()=> xPizza.addWholeTopping('green peppers'));

      // schedule(()=> new Future.delayed(new Duration(milliseconds: 500)));
      schedule((){
        var _completer = new Completer();
        xPizza.el.async((_)=> _completer.complete());
        return _completer.future;
      });

      schedule((){
        // Check expectations here ...
      });
    });
Then my tests pass. Every. Damn. Time.

So it seems that telling the Polymer element to “async” solves all my ills. This async() method tells the Polymer platform to flush(), which updates all observed variables including the one on which I am setting my test expectation. In other words, it makes complete sense that I would do this. The 500ms delay was ensuring that Platform.flush() was called and that my custom element was redrawn. Now I am just doing it more effectively.

This is so effective, that I ought to pull it back into the Page Objects pattern that I am using to interact with my Polymer element. The classic Page Objects pattern is for all methods to return a reference to the Page Object instance, which is what I have been doing with methods like addWholeTopping():
class XPizzaComponent {
  // ...
  XPizzaComponent addWholeTopping(String topping) {
    // Select items, click buttons, etc...
    return this;
  }
}
There is very little point to this in Dart, however. If I needed to chain method calls, I could simply use Dart's amazing method cascades. But, if I return a Future—say a Future that completes when async() finishes—then my Page Object becomes Polymer super-charged:
class XPizzaComponent {
  // ...
  Future addWholeTopping(String topping) {
    // Select items, click buttons, etc...
    return flush();
  }

  Future flush() {
    var _completer = new Completer();
    el.async((_)=> _completer.complete());
    return _completer.future;
  }
}
Why is that super-charged? Because scheduled_test schedules will wait until returned futures complete. This lets me write the entire test as:
    test('updates the pizza state accordingly', (){
      var toppings = JSON.encode({
        'firstHalfToppings': [],
        'secondHalfToppings': [],
        'wholeToppings': ['green peppers']
      });

      schedule(()=> xPizza.addWholeTopping('green peppers'));

      schedule((){
        expect(
          xPizza.currentPizzaStateDisplay,
          equals(toppings)
        );
      });
    });
The schedule that returns xPizza.addWholeTopping() now waits for async() finish. Once it does, the expectation passes. Every time.

So yay! Between scheduled_test and Page Objects, I have a nice, clean test suite. And, as much as I originally disliked the idea of Page Objects in my tests, I have to admit that they make a huge difference here. So much so that I believe I may need a new chapter in Patterns in Polymer!


Day #14

2 comments:

  1. Wow, I think I might start using this scheduled_test now.

    ReplyDelete
  2. The new chapter will be awesome! Please do it!

    ReplyDelete