Thursday, May 30, 2013

Life with Many Small Code Files in Dart

‹prev | My Chain | next›

Now that I have my one-class-per-dialog branch merged into master, I am ready to continue “regular” development on the Dart version of the ICE Code Editor. Hopefully I can make good use of the newly separate code files.

I start with the hit-enter-to-save feature. More specifically, I start with a test. And since I have a new_project_dialog_test.dart file, it is easy to figure out where to put that test. At the bottom of the file, I add:
    test("hitting the enter key saves", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');

      query('input').value = 'My New Project';
      document.activeElement.dispatchEvent(
        new KeyboardEvent(
          'keyup',
          keyIdentifier: new String.fromCharCode(27)
        )
      );

      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Open');

      expect(
        queryAll('div'),
        helpers.elementsContain('My New Project')
      );
    });
This is a test of the functional variety. It clicks the menu button, selects the “New” menu item and types in “My New Project” for the project name. Then, I fake a key event (because Dart cannot do the real thing) for Enter. Finally, I test that a project was created by inspecting the “Open” menu.

In addition to being something of hack, that keyboard event sticks out among my otherwise pristine DOM test helpers. I move it and a couple of thoughtful shortcuts into my helers.dart:
hitEnter()=> type(13);
hitEscape()=> type(27);

type(int charCode) {
  document.activeElement.dispatchEvent(
    new KeyboardEvent(
      'keyup',
      keyIdentifier: new String.fromCharCode(charCode)
    )
  );
}
This means that my test can now be written as:
    test("hitting the enter key saves", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');

      query('input').value = 'My New Project';
      helpers.hitEnter();

      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Open');

      expect(
        queryAll('div'),
        helpers.elementsContain('My New Project')
      );
    });
That's much nicer!

Of course, my test fails, but it is easy enough to get passing by adding some code. Thanks to my one-class-per-dialog refactoring, finding the class is much easier now. In new_project_dialog.dart, I listen to the onKeyUp event stream:
part of ice;

class NewProjectDialog {
  // ...
  open() {
    // ...
    dialog.query('button')
      ..onClick.listen((e)=> _create());

    dialog.query('input')
      ..onKeyUp.listen((e){if (_isEnterKey(e)) _create();});
  }
  // ...
}
Next, I need to make sure that creating a new project opens the project immediately. It is kind of an obvious feature, but one that I have yet to drive with tests. The test:
    test("creating a new project opens it immediately", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');
      query('input').value = 'My Project';
      helpers.click('button', text: 'Save');
      editor.content = 'asdf';
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Save');

      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');
      query('input').value = 'My New Project';
      helpers.click('button', text: 'Save');

      expect(
        editor.content,
        equals('')
      );
    });
Exercising the menus in the full-screen editor, I create a project, edit the content, save it and then create a new project. At this point the content should be blank, but this test tells me that:
FAIL: New Project Dialog creating a new project opens it immediately
  Expected: ''
       but: was 'asdf'.
That is a pretty easy test to get passing—I only need set the content of the editor to the empty string immediately after creating a project:
part of ice;

class NewProjectDialog {
  // ...
  _create() {
    var title = query('.ice-dialog').query('input').value;
    if(store.containsKey(title)) {
      // prevent user from over-writing an existing project...
    }
    else {
      store[title] = {};
      ice.content = '';

      _hideDialog();
    }
  }
}
With that, the project is up to 59 passing tests.

Before calling it a night, I take one more advantage of the newly separate classes and tests. I add some TODOs to the new_project_dialog_test.dart test file:
part of ice_test;

new_project_dialog_tests(){
  group("New Project Dialog", (){
    test("new project input field has focus", (){ /* ... */}
    test("cannot have a duplicate name", () { /* ... */}
    test("can be named", (){ /* ... */}
    test("clicking the new menu item closes the main menu", (){ /* ... */}
    test("the escape key closes the new project dialog", (){ /* ... */}
    test("hitting the enter key saves", (){ /* ... */}
    test("creating a new project opens it immediately", (){ /* ... */}
  });
  // TODO: templates
  // TODO: blank name behavior
}
That makes it easy to pick up with a #pairwithme partner or the next time I have to work on something. I am so glad that multiple code files are cheap in Dart. I am even more glad that I finally put that feature to good use in this project.


Day #767

No comments:

Post a Comment