Monday, November 24, 2014

Testing Polymer.dart Synchronization with Form Input Fields


Surprisingly, none of my Dart tests are failing. Or have failed. It's not that I expect Dart tests to fail with any regularity—far from it. It is just that the tests for the JavaScript version of Patterns in Polymer seem to fail every other week or so.

It probably helps that Polymer.dart has remained at 0.15 for a while now. I could also use some better tests. And since I went to all that trouble last night to write a test describing the JavaScript code from the chapter on synchronizing the plain-old form <input> value with Polymer, it seems only fair that I write the same thing for Dart.

The JavaScript test wound up looking like:
  describe('syncing <input> values', function(){
    var input;

    beforeEach(function(done){
      input = document.createElement('input');
      container.appendChild(input);

      syncInputFromAttr(input, el, 'state');

      el.model.firstHalfToppings.push('pepperoni');
      el.async(done);
    });

    it('updates the input', function(){
      expect(input.value).toEqual('pepperoni');
    });
  });
Cerate an <input>, sync it with the <x-pizza> Polymer element, add a pepperoni to the Polymer element and expect that the <input> now contains pepperoni as well.

As with last night's JavaScript solution, I am using MutationObserver objects to synchronize my elements in Dart:
syncInputFromAttr(input, el, attr) {
   var observer = new MutationObserver((changes, _) {
    changes.forEach((change) {
      if (change.attributeName == attr) {
        input.value = change.target.attributes[change.attributeName];
      }
    });
  });
  observer.observe(el, attributes: true);
}
This creates an observer with the supplied callback that is invoked when changes are seen. It then observes the supplied el, which is my <x-pizza> Polymer element in this case. Not a particularly Darty looking thing, but the callback updates the input when the desired changes are seen.

It works in my smoke tests, but I still need a repeatable unit test. Well, maybe not need, but if this approach failed in JavaScript, then it can also fail in Dart at some point. So it would be good to have such a test.

The test setup is the same as with the JavaScript test—create an <input> which is added to the container from previous setup. Then synchronize the Polymer and input elements. Lastly, add pepperoni as the first half topping:
  group('syncing <input> values', (){
    var input;

    setUp((){
      input = createElement('<input>');
      _container.append(input);

      syncInputFromAttr(input, _el, 'state');

      _el.model.firstHalfToppings.add('pepperoni');
    });

    test('updates the input', (){
      // Test will go here...
    });
  });
The test then needs to check that the input value is now set to indicate that the first half of the pizza includes pepperoni like the Polymer element:
  group('syncing <input> values', (){
    var input;

    setUp((){ /* ... /* });

    test('updates the input', (){
      expect(
        input.value,
        startsWith('First Half: [pepperoni]')
      );
    });
  });
Except that does not work:
FAIL: <x-pizza> syncing <input> values updates the input
  Expected: a string starting with 'First Half: [pepperoni]'
    Actual: ''
Fortunately, I have gotten to the point in my Polymer testing (JavaScript and Dart) at which I know that I need to wait for Polymer to update all internal and external values before testing. Furthermore, I know that I can supply a callback to Polymer's async() method such that Polymer will invoke this callback when it has updated bound variables and observables alike.

To adapt this into vanilla Dart unittest, I need to wrap the whole thing in an expectAsync() call:
group('syncing <input> values', (){
    var input;

    setUp((){ /* ... /* });

    test('updates the input', (){
      _el.async(expectAsync((_){
        expect(
          input.value,
          startsWith('First Half: [pepperoni]')
        );
      }));
    });
  });
And that actually works. I now have my very basic test working and a test of the actual functionality from this chapter both passing:
PASS: <x-pizza> has a shadowRoot
PASS: <x-pizza> syncing <input> values updates the input
All 2 tests passed.
I must admit that I begin to dislike the callback love in Polymer.dart. Both the MutationObserver and the async() methods require callbacks, making this feel very much like JavaScript code. I can only hope that this changes at some point so that I can use a Future or two. In the meantime, I will likely convert this test into a scheduled_test test. I may have to build my own Completer to make it work, but the end result ought to be a bit easier on the eye.



Day #4

No comments:

Post a Comment