Friday, October 4, 2013

Testing Futures in Polymer.dart Elements


Getting multiple instance of my Polymer.dart custom element turned out to be fairly easy. So easy, that it only took a four line hack. Tonight, I remove the hack so that I can do it for real. Even though the hack turned out to be small and self-contained, I originally suspected it to be a more significant problem which led me to explore the fix via a code spike. Now that I understand the problem, I have enough information to fix it the right way: by testing.

The hack had nothing to do with Polymer.dart, but with the manner in which I am loading JavaScript libraries for use via js-interop. To keep my code a black-box I do not load any JavaScript libraries with explicit <script> tags. Rather, I have my library dynamically append the necessary <script> tags to the document. Once the load event for the script fires, an instance of my ICE Code Editor object sees a Future completed so that the remainder of the initialization can occur.

Last night's hack was to wait for a constant amount of time so that multiple instances would delay long enough for the JavaScript to be ready. That is a hack rather than a long-term solution because the delay needs to vary in time depending on latency in the network. Instead of a time-based delay, what I really need is a way to expose the when-javascript-ready Future to all instances of my <ice-code-editor> element.

I suspect that there may be a more general solution that I need to extract for this, but I start with my specific problem by adding a second <ice-code-editor> custom element to my testing context:
<head>
  <link rel="import" href="packages/ice_code_editor/polymer/ice_code_editor.html">
  <script src="packages/polymer/boot.js"></script>
  <script src="packages/unittest/test_controller.js"></script>
  <script type="application/dart" src="test.dart"></script>
</head>
<body>
  <ice-code-editor src="embed_foo.html" line_number="42"></ice-code-editor>
  <ice-code-editor src="embed_bar.html" line_number="0"></ice-code-editor>
  <script src="packages/browser/interop.js"></script>
</body>
My existing tests that still set their expectations on the first element continue to pass:
PASS: [polymer] can embed code 
PASS: [polymer] can set line number 
PASS: [polymer] creates a shadow preview 
PASS: [polymer] creates an editor

All 4 tests passed. 
The tests pass, but there is a mess of error output:



The errors are coming from no-longer hacked code. Rather than try to address the errors, I start with a test setting an expectation that the last <ice-code-editor> element on my test pages is using the embed_bar.html source (instead of embed_foo.html in the first element):
    group("multiple elements", (){
      var it;
      setUp((){
        it = queryAll('ice-code-editor').last;
      });

      test("can embed code", (){
        expect(
          it.shadowRoot.query('h1').text,
          contains('embed_bar.html')
        );
      });
    });
That fails because there is only one functioning <ice-code-editor> element on the page—the first one with embed_foo.html as the source:
PASS: [polymer] can embed code 
PASS: [polymer] can set line number 
PASS: [polymer] creates a shadow preview 
PASS: [polymer] creates an editor 
FAIL: [polymer] multiple elements can embed code
  Expected: contains 'embed_bar.html'
    Actual: 'The src is "embed_foo.html" (42)'
To get this passing, I focus on the same area that got hacked up last night—the _startAce() method. This method is responsible for instantiating the JavaScript code that builds the editor part of ICE's editor/preview combo. Instead of adding a delay timer before instantiating the JavaScript objects, I wait for a soon-to-be-created Future, jsReady to complete:
  _startAce() {
    // ...
    jsReady.then((_)=> _startJsAce());
    // ...
  }
Once the JavaScript is ready, then I start the JavaScript Ace objects.

This _startAce() method is an instance method. I might have a hundred instances of the editor—each one of them needs its own Ace instance. But each and every instance of the editor needs to check for the same JavaScript ready. Enter a static getter:
class Editor {
  // ...
  static Future get jsReady { /* ... */ }

  // instance method
  _startAce() {
    jsReady.then((_)=> _startJsAce());
  }
  // ...
}
This is probably one of those cases in which a static method ought to be refactored into a separate singleton class. I'll suffer with the static method for now.

In Dart, if an instance method cannot be found, a class method of the same name will be searched. This is how the call to the static jsReady getter method works from the _startAce() instance method.

For the jsReady function to return a Future, I need a Completer object, which has both a future property and a complete() method which will fire any then() callbacks attached to the Future:
  static Completer _waitForJS;
  static Future get jsReady {
    if (!_isAceJsAttached) {
      _waitForJS = new Completer();
      _attachScripts().
        first.
        onLoad.
        listen((_)=> _waitForJS.complete());
    }

    return _waitForJS.future;
  }
I already have a _isAceJsAttached getter that will return true if a <script> tag exists for the Ace source. If the Ace JavaScript files have not been attached to the document yet, I do so with _attachScripts() and then listen for the load event on the first of those <script> tags. When loaded, I complete my new _waitForJs completer, firing the then() callbacks.

And that does the trick:
unittest-suite-wait-for-done
PASS: [polymer] can embed code
PASS: [polymer] can set line number
PASS: [polymer] creates a shadow preview
PASS: [polymer] creates an editor
PASS: [polymer] multiple elements can embed code

All 5 tests passed.
unittest-suite-success 
So, in the end, a little future-based code goes a long way. Futures can be a little hard to explain (or maybe my brain is just fried), but they make for some easy-to-read, working code.

I think tomorrow that I will pick back up by taking this a step further. I would like to see if I can dynamically create and add my custom element to the document and test that it behaves as desired.


Day #894

No comments:

Post a Comment