Send to Kindle

Friday, May 24, 2013

Rethinking DOM Menus in Dart

‹prev | My Chain | next›

My house is a mess and I am not quite sure how to fix it. The house in this case is one of the classes in the Dart version of the ICE Code Editor. This is not a Dart problem—I had the same situation in the JavaScript version. But the problem is more obvious in Dart.

Much of the effort of late in ICE has gone into the full-screen, lite-IDE that is used in 3D Game Programming for Kids. The class, Full starts off well enough:
class Full {
  Full() { /* ... */ }
  Future get editorReady => _ice.editorReady;
  String get content => _ice.content;
  void set content(data) => _ice.content = data;
  // ...
}
(the Full() method is the constructor)

As we have been building out the various menus and dialogs that go into an IDE, things have gone a little wrong. The menus and dialogs are clearly private methods—no external class should ever need to open the full-screen share dialog. Since Dart has first-class support for private methods, I have been dutifully marking these methods as private. It turns out that there are a lot of them:
class Full {
  // ...
  _attachToolbar() { /* ... */ }
  _attachMainMenuButton(parent) { /* ... */ }
  _attachKeyboardHandlers() { /* ... */ }
  _attachMouseHandlers() { /* ... */ }
  _showMainMenu() { /* ... */ }
  _hideMenu() { /* ... */ }
  _hideDialog() { /* ... */ }

  get _newProjectMenuItem { /* ... */ }
  _openNewProjectDialog() { /* ... */ }
  _saveNewProject() { /* ... */ }

  get _projectsMenuItem { /* ... */ }
  _openProjectsMenu() { /* ... */ }
  _openProject(title) { /* ... */ }

  get _saveMenuItem { /* ... */ }
  void _save() { /* ... */ }

  get _shareMenuItem { /* ... */ }
  _openShareDialog() { /* ... */ }
}
I am not quite certain how best to reduce the noise. The most obvious thing—the thing that I have been threatening since this was in JavaScript—is to move dialogs and their associated actions into self-contained classes. The biggest unknown for me is how to allow communication back into the main class.

The dialog that needs the most access is the Projects dialog, whose current entry point is _projectsMenutItem(). In addition to doing menu-like things, it needs to read from and write to the localStorage Store class. It also needs to be able to update the code editor when switching between projects. Since that seems the hardest, I start with it.

The entry point is still the menu list item that goes on the main menu. This will have to be exposed as a getter, which I call el:
class ProjectsDialog {
  // ...
  Element get el {
    return new Element.html('<li>Projects</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> _openProjectsMenu());
  }
  // ...
}
I pull in the _openProjectsMenu() method without change, along with the _openProject() method. Already the cohesion of these methods is improved. To continue to work, they need access to the parent element of the Full editor, as well as the editor itself and the data store. So I define a constructor that accepts all three:
class ProjectsDialog {
  var parent, ice, store;
  ProjectsDialog(this.parent, this.ice, this.store);
  Element get el {
    return new Element.html('<li>Projects</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> _openProjectsMenu());
  }
  // ...
}
With that, I can return the main menu, which is still in the Full class, and inject the new ProjectsDialog as:
class Full {
  // ...
  _showMainMenu() {
    var menu = new Element.html('<ul class=ice-menu>');
    el.children.add(menu);

    menu.children
      ..add(new ProjectsDialog(el, _ice, _store).el)
      ..add(_newProjectMenuItem)
      ..add(new Element.html('<li>Rename</li>'))
      ..add(new Element.html('<li>Make a Copy</li>'))
      ..add(_saveMenuItem)
      ..add(_shareMenuItem)
      ..add(new Element.html('<li>Download</li>'))
      ..add(new Element.html('<li>Help</li>'));
  }
  // ...
}
And that works. All of my tests still pass. I even give it a try in the sample app and it still works.

It seems a bit ugly to pass in those three parameters to the ProjectsDialog constructor. If only there was something that encapsulated those three objects… like the Full object that is creating them maybe?

Then the menu list could start as:
    menu.children
      ..add(new ProjectsDialog(this).el)
      ..add(_newProjectMenuItem)
      ..add(new Element.html('<li>Rename</li>'))
      // ...
That works, but the _ice and _store properties can no longer be private. I am not sure that is worth the change, so I call it a night here to give the idea a chance to percolate.

Regardless, I like the overall approach to extracting the menu dialogs out of the Full IDE class. This approach definitely has promise. And having a strong test suite in place to guard against regressions is invaluable.

Day #761

Thursday, May 23, 2013

Minor Refactoring under Test in Dart

‹prev | My Chain | next›

Today, I would like to start thinking about how to consolidate some of my Dart code in the ICE Code Editor. I think that it is a bit early to start breaking the sub-menus and dialogs out into Dart classes. So instead, I am going to scavenge the codebase for things that are asking to be removed or consolidated.

One of the first things that asks to come out are styles. I love using method cascades for CSS styles in Dart:
  _openNewProjectDialog() {
    var dialog = new Element.html(
        '''
        <div class=ice-dialog>
        <label>Name:<input type="text" size="30"></label>
        <button>Save</button>
        </div>
        '''
      );

    dialog.style
      ..position = 'absolute'
      ..margin = '2px'
      ..right = '20px'
      ..top = '45px'
      ..zIndex = '999';
    // ...
  }
As much as I like a good method cascade, I am setting the same style for multiple dialogs. In fact, I am setting the same style for sub-menus as well. So, reluctantly, I pull that out into vanilla CSS:
.ice-menu, .ice-dialog {
  /* ... */
  position: absolute;
  margin: 2px;
  right: 20px;
  top: 45px;
  z-index: 999;
}
Sometimes there is no substitution for a low-tech solution.

I clean out a few other minor things and then come across:
  _hideMenu() {
    if (query('.ice-menu') == null) return;
    query('.ice-menu').remove();
    // queryAll('.ice-menu').forEach((e)=> e.remove());
  }
I really dislike that conditional. It seems like that queryAll() ought to be equivalent, if not a better solution. Instead of finding one .ice-menu element, it will find them all and remove each. If there are no matching elements, which is what the current conditional is guarding, then the forEach() should be a no-op.

But I commented that out for a reason. Specifically, if I replace the current code with the queryAll() version, I get test failures:
FAIL: project menu lists project names
  Expected: List of elements contains match 'My New Project'
       but: Element list content was <[☰X, ☰, X, , , , , , , , , , , , , X, , , ]>.
Interestingly, I think that I was relying on coincidence to make the “list project names” feature work. In particular, the way that I opened the project list sub-menu was opening the menu first, then closing the main menu:
  _projectsMenuItem() {
    return new Element.html('<li>Projects</li>')
      ..onClick.listen((e)=> _openProjectsMenu())
      ..onClick.listen((e)=> _hideMenu());
  }
This, and a bunch of other tests, pass if I hide all menus before opening the project menu:
  _projectsMenuItem() {
    return new Element.html('<li>Projects</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> _openProjectsMenu());
  }
In the end, the previous conditional-then-remove-the-first-element-in-the-DOM approach was less than robust. So this refactoring / cleanup has helped improve the strength of the codebase. Hopefully it has cleaned enough cruft so that I have a better handle on how to better approach those sub-menus and dialogs. I will get started on that tomorrow.


Day #760

Wednesday, May 22, 2013

Driving Dart Integration Tests

‹prev | My Chain | next›

The crazy thing about driving things with tests is that stuff gets done. I have been having a good old time playing around with the test suite in the Dart version of the ICE Code Editor. I spent so much time fiddling with the tests that I barely noticed the progress with functionality. Mostly thanks to #pairwithme sessions, the functionality of the ICE Code Editor is really coming along.

Tonight, I take a step back to see how it is working with the new test helpers that I have written over the past two nights when driving new functionality. Thanks to last night's #pairwithme session with Santiago Arias, ICE now lists the currently saved projects in the Projects menu. Tonight I would like to be able to click on old ones to make them active.

I start with a long UI workflow test:
    test("click names to switch between projects", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');

      query('input').value = 'Project #1';
      helpers.click('button', text: 'Save');

      editor.content = 'Code #1';
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Save');

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

      query('input').value = 'Project #2';
      helpers.click('button', text: 'Save');

      editor.content = 'Code #2';
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Save');

      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');
      helpers.click('li', text: 'Project #1');

      expect(
        editor.content,
        equals('Code #1')
      );
    });
This fully exercises the full screen ICE menus by clicking the menu button (the UTF-8 ☰ symbol), then creating a new project, setting the content and saving the project. I do that twice, at which point the content for the second project is still active. I then open the Projects menu and select the first project from the list. The expectation is that the editor content will have changed to “Code #1” the content of the first project.

It is so cool that, thanks to Dart's testing facilities and some simple helpers, the test is so expressive and yet completely functional. The first time I run that test, I get a failure. The exact failure that I had hoped to get to drive this feature:
FAIL: project menu click names to switch between projects
  Expected: 'Code #1'
       but: was 'Code #2'.
How awesome is that?

Then again, maybe my UI testing expectations are just too low. Perhaps this is really how it ought to be. Regardless, this is reality in the world of Dart and I embrace it by writing the code that implements this. With that bit of smugness fully internalized, I run into all sorts of problems.

The implementation is not too hard:
    _store.forEach((title, data) {
      var project = new Element.html('<li>${title}</li>')
        ..onClick.listen((e)=> _openProject(title))
        ..onClick.listen((e)=> _hideMenu());

      menu.query('ul').children.add(project);
    });
For each item in the data store, I create a menu item for the Projects menu. Each project menu item gets a couple of handlers to open the project and hide the menu. The underlying Store class could support opening a project better, but that is not really the problem.

The problem is that, after saving items by clicking the “Save” option from the main menu, I was not closing the main menu. It would stay up causing subsequent tests to fail because the wrong menus were now active.

I could have written this test such that the localStorage store was initialized with projects in place. This would have avoided the save-not-closing-the-menu issue (though I would not have found the bug otherwise) and the subsequent implementation would likely end up exactly the same. But there is something exciting about a test that exercises the full stack in milliseconds. If I had other tests that covered similar ground, I might manually initialize the localStorage, but I really prefer to keep this around.

It turns out to be quite the effort to keep it around. The test continues to fail and is compounded with a problem running these tests solo due to js-interop concerns. Thankfully, tonight's #pairwithme pair, Daniel Gempesaw, helps me through it.

We drive the problem away with another test:
    test("clicking save closes the main menu", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text:  'Save');

      expect(queryAll('li').map((e)=> e.text).toList(), isEmpty);
    });
All that is needed to make that pass is a second on-click listener for the “Save” menu item:
  Element get _saveMenuItem {
    return new Element.html('<li>Save</li>')
      ..onClick.listen((e)=> _save())
      ..onClick.listen((e)=> _hideMenu());
  }
With that, not one, but two tests are passing. More importantly, it is now possible to switch between projects in the Dart version of the ICE Code Editor. Yay!


Day #759

Tuesday, May 21, 2013

Custom Test Matchers in Dart

‹prev | My Chain | next›

After last night, I have helpered my way to a nice looking Dart test:
    test("clicking the project menu item opens the project dialog", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        contains(matches('Saved Projects'))
      );
    });
That's downright pretty, except… that expect() is, let's face it, plain yucky.

The thing that I am testing is kinda OK. It is a map of the text contents of <div> elements. I could do without the map(), but it's not horrible. The toList() is bothersome. It can be omitted, but it is useful to have around. For instance, if I cause an intentional failure by naming the menu "Saved Code" instead of "Saved Projects", I get a nice error message that gives me an idea of what went wrong:
FAIL: project menu clicking the project menu item opens the project dialog
  Expected: contains match 'Saved Projects'
       but: was <[☰X
          Saved Code
          , ☰, X, , , , , , , , , , , , , X, , , , 
          Saved Code
          ]>.
If I omit the toList(), then the object that I am testing is an Iterable. It still passes or fails as desired, but the failure message is less nice:
FAIL: project menu clicking the project menu item opens the project dialog
  Expected: contains match 'Saved Projects'
       but: was <Instance of 'MappedListIterable':554001401>.
So I leave toList() as a bit of test code ugliness so that I get nicer test code output. I can live with that.

The expected value is not quite as nice. Dart provides a nice matcher for lists: contains(). So in this case I am expecting a list that contains something. That something is anything that matches the string 'Saved Projects'. It works, but it is hard to read.

But hard to read matchers are what custom matchers are for. For this expectation, I may not even need to write an entirely new matcher. Instead, I start by inheriting from CustomMatcher. These nifty little things set a description ("List of elements") and a feature name ("Element list content") in the constructor:
class ElementListMatcher extends CustomMatcher {
  ElementListMatcher(matcher) :
      super("List of elements", "Element list content", matcher);

  featureValueOf(elements) => elements.map((e)=> e.text).toList();
}
I then pick a value to extract from the actual value. If the actual value is a list of elements, then the above extracts the text contents in List form. That is just what I had to do by hand in my test, but all of the ugliness of mapping and converting from an iterable to a list is done in the custom matcher.

I then create a top-level helper that uses this matcher to check the extracted/featured list to see if it contains a match:
elements_contain(Pattern content) =>
  new ElementListMatcher(contains(matches(content)));
Those are two pretty simple helpers that let me rewrite my test entirely with helpers as:
    test("clicking the project menu item opens the project dialog", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');

      expect(
        queryAll('div'),
        helpers.elements_contain('Saved Projects')
      );
    });
That is pretty all the way through: I click a button with the menu icon, I click the "Projects" menu item, then I expect one of the <div> tags to contain the text "Saved Projects". Wonderful!

And best of all it works!

If I again intentionally make my test fail, I get:
FAIL: project menu clicking the project menu item opens the project dialog
  Expected: List of elements contains match 'Saved Projects'
       but: Element list content was <[☰X
          Saved Code
          , ☰, X, , , , , , , , , , , , , X, , , , 
          Saved Code
          ]>.
That is even more expressive than what I had when I manually extracted a list to test and the test content is easier to read. The use of the helpers prefix from last night is the only remaining noise (and I still think it worth keeping about). All in all, these test matchers are pretty powerful.


Day #758

Monday, May 20, 2013

Dart Test Helpers

‹prev | My Chain | next›

I find myself repeating a lot of the same test actions in my ICE Code Editor test suite. This seems a fine opportunity to explore test helpers in Dart unit tests.

The one in particular that I find myself doing a lot is clicking on element, usually with particular content:
      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();
My first instinct is that the helpers should be a separate library that is imported into my test suite. The main reason is that I can import with an “as” prefix to make it patently obvious where the helper methods live. So, in my main ice_test.dart main test file, I add the import statement:
library ice_test;

import 'package:unittest/unittest.dart';
// ...
import 'helpers.dart' as helpers;
import 'package:ice_code_editor/ice.dart';

main(){
  // tests go here...
}
In helpers.dart, I start with a single click() function:
library ice_test_helpers;

import 'dart:html';

void click(String selector, {text}) {
  if (text == null) return query(selector).click();

  queryAll(selector).
    firstWhere((e)=> e.text==text).
    click();
}
The click function requires a string selector that will be used to query for elements to click. If no text is specified—if the optional, named parameter text is null—then I query for the first matching selector and click it. If the text parameter is specified, then I query for all matching selectors, find the first that contains the supplied text and click that.

I continue to use the firstWhere() because it will throw an exception if no matching element is found. I may want to bundle that into a new exception that makes it more obvious what has gone wrong in the test, but I leave it for now.

With that, I can change the test that verifies one of the ways to close a menu:
    test("the menu button closes the projects dialog", (){
      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();

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

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

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        isNot(contains(matches('Saved Projects')))
      );
    });
Instead, I can write that as:
    test("the menu button closes the projects dialog", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');
      helpers.click('button', text: '☰');

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        isNot(contains(matches('Saved Projects')))
      );
    });
Holy clearer intent Batman! It is much easier to see that this test clicks the menu button, then the Projects menu item, then the menu button again.

I might have omitted the “helpers” prefix from my import statement and thus been able to treat click() as a top-level function. I tend to think that the prefix will aid in long-term maintainability of the test suite as there will never be a question as to the source of the helper function.


Day #757

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

Saturday, May 18, 2013

BDD a Dart Menu

‹prev | My Chain | next›

The sharing feature in the ICE Code Editor is not quite done, but I am going to risk starting a new feature tonight. It is a bit of a concern having multiple features under development at the same time, but it could be a good thing for my #pairwithme sessions.

The most interesting next feature is the project menu. What makes it interesting is that it combines three different classes in the project: the core editor, the localStorage, and the full-screen IDE. I start with four empty tests that still start me on the way:
  group("project menu", (){
    skip_test("clicking the project menu item opens the project dialog", (){});
    skip_test("the escape key closes the project dialog", (){});
    skip_test("the menu button closes the projects dialog", (){});
    skip_test("contains a default project on first load", (){});
  });
I will probably leave that last one, which begins to exercise the localStorage component, until tonight's #pairwithme session, but hopefully I can get through the rest.

I start by adding the usual setup and teardown:
  group("project menu", (){
    setUp(()=> new Full(enable_javascript_mode: false));
    tearDown(()=> document.query('#ice').remove());

    skip_test("clicking the project menu item opens the project dialog", (){});
    // ...
  });
The enable_javascript_mode option is a test-only feature. It disables JavaScript syntax highlighting because the underlying ACE code editor uses web workers, which do not work for file:// URLs. Since the tests are run from a URL like file:///home/chris/repos/ice-code-editor/test/index.html, the web workers are guaranteed to fail, adding messy warnings to the test output.

I convert the first test from a skip_test() to the real thing and set my expectations:
    test("clicking the project menu item opens the project dialog", (){
      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();

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

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        contains(matches('Saved Projects'))
      );
    });
This test says that, after clicking the menu button and the “Projects” menu item, I expect that the project sub-menu will be open. I like using the firstWhere() method when looking for elements to click in tests—it throws exceptions if there is no matching element. That is what happens in this case:
FAIL: project menu clicking the project menu item opens the project dialog
  Caught Bad state: No matching element
  Object&ListMixin.firstWhere                                    dart:collection 561:5
  full_tests.<anonymous closure>.<anonymous closure>             file:///home/chris/repos/ice-code-editor/test/full_test.dart 90:19
This is because I have named the menu item “Open.” I rename it to “Projects” and now I have a failure that the projects dialog is not open:
FAIL: project menu clicking the project menu item opens the project dialog
  Expected: contains match 'Saved Projects'
       but: was <[☰XNewProjectsSaveMake a CopyShareDownloadHelp, ☰, X, , , , , , , , , , , , , X, , , ]>.
  
  full_tests.<anonymous closure>.<anonymous closure>             file:///home/chris/repos/ice-code-editor/test/full_test.dart 93:13
I make that pass by calling a new private method to add the Projects menu item:
    menu.children
      ..add(new Element.html('<li>New</li>'))
      ..add(_projectsMenuItem())
      // ...
The new private method creates the menu item and attaches a click listener:
  _projectsMenuItem() {
    return new Element.html('<li>Projects</li>')
      ..onClick.listen((e) => _openProjectsMenu());
  }
Finally, the menu that opens the project menu and makes the test pass:
  _openProjectsMenu() {
    var menu = new Element.html(
        '''
        <div class=ice-menu>
        <h1>Saved Projects
        </div>
        '''
    );

    el.children.add(menu);

    menu.style
      ..maxHeight = '560px'
      ..overflowY = 'auto'
      ..position = 'absolute'
      ..right = '17px'
      ..top = '60px'
      ..zIndex = '1000';
  }
Both the CSS and the code seem ripe to extraction. I have the feeling that this is not the last menu that I need. Still, I will worry about generalizing the behavior later.

Update: The close-menu-with-escape proves to be extremely difficult. But, thanks to Damon Douglas, tonight's #pairwithme partner, I have a solution.

As best I can tell, there is no way to properly simulate keyboard events in Dart. It is possible to create an instance of KeyboardEvent, but it is not possible to set the charCode of that event—either in the constructor or via a setter. Craziness!

We found that it is possible to specify a key identifier in the constructor. If the character code for the escape key is 27, then I can create an escape keyboard event with:
    test("the escape key closes the project dialog", (){
      // open the project menu

      document.body.dispatchEvent(
        new KeyboardEvent(
          'keyup',
          keyIdentifier: new String.fromCharCode(27)
        )
      );

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        isNot(contains(matches('Saved Projects')))
      );
    });
Unfortunately, this still does not set a charCode on the resulting event. Nevertheless, it does set information that can be passed from the test into the application code. To get this test to pass, we have to check both the charCode and the keyIdentifier values in the application code:
    document.onKeyUp.listen((e) {
      if (e.keyCode == 27) _hideMenu();
      if (e.$dom_keyIdentifier.codeUnits.first == 27) _hideMenu();
    });
It is never a good thing to call one of the dollar-sign methods in Dart. It is also not a good thing to write code just to make a test pass. There is no way that second conditional will get triggered in live code and it does contain the same spirit as the real event conditional on the previous line.

Still, I will be much happier once there is a real way to test keyboard events in Dart. This is a useful first approximation, but nothing beats the real thing.


Day #755