Sunday, May 19, 2013

Verifying Persistent Browser Storage with Dart Integration Tests

‹prev | My Chain | next›

One of the crazy things about blogging every day—even the routine stuff of BDDing a new feature—is my ability to run into seemingly daily problems. Last night, I realized that I could not simulate keyboard events in Dart tests. Thanks to Damon Douglas, my #pairwithme partner, I have a solution. It is not an ideal solution, but it will suffice.

So tonight, I try to find yet another daily problem.

I am going to attempt to drive the saving of projects in the ICE Code Editor with tests. In some ways, this is a useless feature because the editor will (eventually) auto-save on every change. Still, it makes people feel more comfortable if it is around. Also, it is a good opportunity for mayhem as this is the first time that I need to use the Store class, which interfaces with localStorage. Problems are sure to abound!

So I start with a test. I create a new full-screen editor instance, set the content, save it with the menu and then start a new instance. The new instance should retain the contents of the previous session by virtue of the Store class that we wrote a couple of weeks ago:
  group("saving projects", (){
    var editor;

    setUp(()=> editor = new Full(enable_javascript_mode: false));
    tearDown(()=> document.query('#ice').remove());

    test("a saved project is loaded when the editor starts", (){
      editor.content = 'asdf';

      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();

      queryAll('li').
        firstWhere((e)=> e.text=='Save').
        click();

      document.query('#ice').remove();
      editor = new Full(enable_javascript_mode: false);

      expect(editor.content, equals('asdf'));
    });
  });
I probably need to write some helper functions for clicking buttons and menu items, but I will leave that for tomorrow. I have other problems tonight. Specifically, since there is no save action yet, I get a nice failing test:
FAIL: saving projects a saved project is loaded when the editor starts
  Expected: 'asdf'
       but: was ''.
In the Full class for full-screen editing, I need to initialize an instance of the Store class. The constructor is just the place for this:
class Full {
  Editor _ice;
  Store _store;

  Full({enable_javascript_mode: true}) {
    // ...
    _ice = new Editor('#ice', enable_javascript_mode: enable_javascript_mode);
    _store = new Store();
    // ...
  }
  // ...
}
Next, I need a menu item that will do the saving of the contents:
  Element get _saveMenuItem {
    return new Element.html('<li>Save</li>')
      ..onClick.listen((e)=> _save());
  }

  void _save() {
    _store['asdf'] = {'code': content};
  }
The _saveMenuItem getter returns an <li> element that, when clicked, calls the _save() method, which is responsible for updating the actual store. The name is obviously wrong and something that a subsequent test will have to drive. But it should suffice for now.

With that, the only other thing that I need to do is update the constructor to set the content from the store (if present):
  Full({enable_javascript_mode: true}) {
    // ..
    _ice = new Editor('#ice', enable_javascript_mode: enable_javascript_mode);
    _store = new Store();
    // ...
    editorReady.then((_)=> content = _store.projects.first['code']);
  }
This makes use of the underlying Future that completes when the JavaScript editor (ACE) finishes loading and doing its own initialization. Of course, that takes some time, which causes my test to still fail. In the test, I also have to wait for the future to complete before checking that the content is retained. Dart may not do me any favors when testing keyboard events, but it does make testing asynchronous events a breeze:
  group("saving projects", (){
    var editor;

    setUp(()=> editor = new Full(enable_javascript_mode: false));
    tearDown(() {
      document.query('#ice').remove();
      new Store().clear();
    });

    test("a saved project is loaded when the editor starts", (){
      editor.content = 'asdf';

      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();

      queryAll('li').
        firstWhere((e)=> e.text=='Save').
        click();

      document.query('#ice').remove();
      editor = new Full(enable_javascript_mode: false);

      _test(_) {
        expect(editor.content, equals('asdf'));
      };
      editor.editorReady.then(expectAsync1(_test));
    });
The expectAsync1() test function declares to my test that there will be an asynchronous call (and that it will receive one argument) and that the test should not consider itself done until that wrapper is called. Once the expectAsync1() function is called by the editorReady completer, then the private _test() method is called, which checks the editor content.

And it works! If I run the test, I have now added one more passing test to the test suite:
PASS: saving projects a saved project is loaded when the editor starts

All 31 tests passed. 
Best of all, if I remove the restore-from-storage future in the constructor, this test fails, which gives me more confidence in the value of this test.

There is definitely more work ahead of me, but it is pretty exciting to have taken the next step toward a working persistent store in the full-screen version of ICE. It is even more exciting to have it working under a strong test to help guard against regressions.


Day #756

No comments:

Post a Comment