Sunday, June 30, 2013

Dart Testing with Lots of Events

‹prev | My Chain | next›

The ICE Code Editor boasts a minimal set of controls:



The subject of tonight's exploration is the Update button—more specifically the checkbox in the Update button which governs auto-update. If checked, code changes are reflected automatically in the preview layer. If unchecked, then the only way to force an update is to click the Update button. That all works fine and is well tested, thanks to Dart's lovely unittest library.

What could work a little better is checking the auto-update checkbox. A programmer clicking that checkbox wants auto-update re-enabled—and that works. But it is also almost certainly true that she wants any code changes since auto-update was disabled to be reflected immediately. And that does not work.

I think I have a fairly good idea how to implement that. More perplexing is how to test that. Thanks to Dart's iframe security restrictions, I cannot (easily) query the contents of the preview iframe to verify that the update occurred correctly. Instead I have to rely on secondary indicators like the onPreviewChanged event stream. This is how existing tests verify the update feature:
    test("updates the preview layer", (){
      helpers.createProject("My Project");
      document.activeElement.
        dispatchEvent(new TextEvent('textInput', data: '<h1>Hello</h1>'));

      editor.onPreviewChange.listen(expectAsync1((_)=> true));

      helpers.click('button', text: " Update");
    });
Here, I am using unittest's expectAsync1(), which will poll until a callback is invoked with a single argument, to test my expectation. As long as the onPreviewChange event occurs, this test passes.

For my code tonight, I need to first uncheck the checkbox (by clicking it), then sending a text event with new content, and finally re-checking the auto-update checkbox and verifying that a change event occurs. Something like this ought to do the trick:
    test("checking the checkbox updates the preview layer", (){
      var button = helpers.queryWithContent("button","Update");
      var checkbox = button.query("input[type=checkbox]");
      checkbox.click();

      document.activeElement.
        dispatchEvent(new TextEvent('textInput', data: '<h1>Hello</h1>'));

      editor.onPreviewChange.listen(expectAsync1((_)=> true));

      checkbox.click();
    });
That ought to work, except it passes. That is, even though I know that the feature is not working, my test does not capture this.

After messing around a bit, I find that… this really ought to work. I confirm that the checkbox is unchecked and rechecked. I confirm that the event does not propagate (clicking the Update button to manually update the preview). I confirm that the editor is, indeed, disabled from auto-updating—at least for a little while.

And that little while turns out to be the key. I am dispatching a text event and then immediately unchecking the auto-update box. This immediate unchecking winds update re-enabling auto-update before the text event propagates through the editor. So that, by the time the editor is aware that an update has occurred, it is already in auto-update mode.

To fix, I need a little delay:
    test("checking the checkbox updates the preview layer", (){
      var button = helpers.queryWithContent("button","Update");
      var checkbox = button.query("input[type=checkbox]");
      checkbox.click();

      document.activeElement.
        dispatchEvent(new TextEvent('textInput', data: '<h1>Hello</h1>'));

      editor.onPreviewChange.listen(expectAsync1((_)=> true));

      var wait = new Duration(milliseconds: 100);
      new Timer(wait, (){
        checkbox.click();
      });
    });
That does the trick. I now have a failing test that accurately captures my buggy behavior. When I fix it, I know that I will have a test that can catch regressions.

And as expected, fixing the bug turns out to be fairly easy. I make use of the iterable nature of stream by adding a where() clause to the checkbox's onClick stream. I only want to listen to streams that contain events in which the checkbox is checked:
    return _update_button = new Element.html('''
        <button>
           <input checked type=checkbox/>
           Update
         </button>'''
      )
      // other listeners here...
      ..query("input").onChange.
          where((e) => e.target.checked).
          listen((e)=> ice.updatePreview());
With that, I have another feature complete and am comforted in the knowledge that I have a strong test describing it. That is a good win.


Day #798

No comments:

Post a Comment