Friday, May 3, 2013

BDD and the Future in Dart

‹prev | My Chain | next›

After a pair programming session with Santiago Arias, the ICE Code Editor no longer exposes any JavaScript to the outside world. Instead of sourcing the ACE code editor JavaScript files from the web page as:
<head>
  <script src="packages/ice_code_editor/ace/ace.js" type="text/javascript" charset="utf-8"></script>
  <script src="packages/browser/dart.js"></script>
  <script src="main.dart" type="application/dart"></script>
</head>
I can now eliminate the ace.js <script> tag so that all remains is the Dart engine kicker and the Dart code:
<head>
  <script src="packages/browser/dart.js"></script>
  <script src="main.dart" type="application/dart"></script>
</head>
The goal is not to hide the fact that I am using ACE—I love ACE, it's awesome. Neither is the goal to eliminate a single <script> tag. The goal is to eliminate bunches of <script> tags that would otherwise build up in this application.

The solution was, naturally enough, to build up the <script> tag dynamically. It works, but it breaks my tests:
FAIL: defaults starts an ACE instance
  Expected: not null
       but: was <null>.
The failing test is simple enough. It creates and ICE Code Editor instance (Dart), which in turn creates an instance of ACE (JavaScript). I do not test that the ACE object exists, I test for a side-effect—that the appropriate HTML elements are added to the DOM:
    test("starts an ACE instance", (){
      var it = new Editor('ice');
      expect(document.query('.ace_content'), isNotNull);
    });
The problem is that the ACE libraries are not loaded until the Editor instance is created. And before the libraries even have a chance to load, I am checking my expectations.

Given this asynchronous interaction, it would be nice to be able to write the test in a way that says “once the editor is ready, then check the expectation.” Well, this is just what Dart Futures are for. So I change the expectation to:
    test("starts an ACE instance", (){
      var it = new Editor('ice');
      it.editorReady.then(
        expectAsync0(() {
          expect(document.query('.ace_content'), isNotNull);
        })
      );
    });
The expectAsync0() call tells the test that I expect the then() call to invoke a callback with zero arguments. That is, when the editor is ready, then call a function that accepts no arguments.

In the Editor class, I define a Completer object to coordinate this information. I call it _waitForAce. Once ACE is ready, I complete it, which tells the future that it can proceed:
class Editor {
  // ...
  Completer _waitForAce;

  Editor(id, {this.edit_only:false, this.autoupdate:true, this.title}) {
    var script = new ScriptElement();
    script.src = "packages/ice_code_editor/ace/ace.js";
    document.head.nodes.add(script);

    this._waitForAce = new Completer();

    script.onLoad.listen((event) {
      this._ace = js.context.ace.edit(id);
      js.retain(this._ace);
      this._waitForAce.complete();
    });
  }
  // ...
  Future get editorReady => _waitForAce.future;
}
Only this doesn't work. When I run the test, I get:
NoSuchMethodError : method not found: 'call'
Receiver: Closure: (dynamic) => dynamic from Function 'invoke0':.
Arguments: [null] 
Stack Trace:
#0      Object.noSuchMethod (dart:core-patch:1907:25)
#1      _SpreadArgsHelper.invoke0.invoke0 (file:///home/chris/repos/ice-code-editor/test/packages/unittest/unittest.dart:-1:-1)
#2      _ThenFuture._sendValue (dart:async:397:24)
#3      _FutureImpl._setValue (dart:async:294:26)
#4      _CompleterImpl.complete (dart:async:129:21)
#5      Editor.Editor.<anonymous closure> (file:///home/chris/repos/ice-code-editor/test/packages/ice_code_editor/editor.dart:23:32)
It takes me a long time to figure out that my expectation is the cause of this. The problem is that the then() method is invoking a callback with a single parameter. In other words, my expectAsync0() needs to be an expectAsync1():
    test("starts an ACE instance", (){
      var it = new Editor('ice');
      it.editorReady.then(
        expectAsync1((_) {
          expect(document.query('.ace_content'), isNotNull);
        })
      );
    });
I really do not understand this behavior. I am completing the completer with no arguments so why is the future's then() sending a parameter to the callback? If I inspect the parameter, it is null, which seems useless. Meh. I am probably doing something wrong somewhere.

I will investigate further later, but, with my test passing again, I call it a night.


Day #740

No comments:

Post a Comment