Thursday, June 20, 2013

Testing Async Menus in Dart

‹prev | My Chain | next›

I have been using the pre-beta version of the ICE Code Editor for all updates that I make as the first edition of 3D Game Programming for Kids nears. For the most part, it has worked brilliantly which obviously falls under the “win” category.

In the last couple of day, however, I have been annoyed to find that focus seems to be broken. When a dialog, like the new project dialog, is opened, its text field should have keyboard focus. Instead, the code editor retains focus:



What is worrisome here is that I have a test for this:
    test("new project input field has focus", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');

      expect(
        query('.ice-dialog input[type=text]'),
        equals(document.activeElement)
      );
    });
And the test still seems to be passing despite visual evidence to the contrary:
PASS: New Project Dialog new project input field has focus
I know what the problem is. I have a vague idea how to fix it. But I am concerned about testing.

The problem is that, when the “New” menu item is clicked, two things happen. First the new project dialog is shown. Second, the main menu is hidden. The problem is that, a few days ago, I added a feature that gave focus to the code editor whenever a menu or dialog was hidden:
_hideDialog() {
  queryAll('.ice-menu').forEach((e)=> e.remove());
  queryAll('.ice-dialog').forEach((e)=> e.remove());
  query('#ice').dispatchEvent(new UIEvent('focus'));
}
The first step here is to add another test. I think the current test still has value, so instead I add a new test that waits for a tiny bit before checking focus:
    test("input field retains focus if nothing else happens", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');

      var wait = new Duration(milliseconds: 100);
      new Timer(wait, expectAsync0((){
        expect(
          query('.ice-dialog input[type=text]'),
          equals(document.activeElement)
        );
      }));
    });
This is the same test except for the 100 millisecond wait. I use an expectAsync0() check to ensure that the test will not prematurely pass without that the callback being called. The callback simple checks the same expectation that the current test does. This time it fails:
FAIL: New Project Dialog input field retains focus if nothing else happens
  Expected: TextAreaElement:<textarea>
    Actual: InputElement:<input>
In the end, I decide to solve this in the MenuAction class:
class MenuItem {
  MenuAction action;
  MenuItem(this.action);
  Element get el {
    return new Element.html('<li>${action.name}</li>')
    ..onClick.listen((e)=> _hideMenu(focus:false))
      ..onClick.listen((e)=> action.open())
      ..onClick.listen((e)=> maybeFocus());
  }
}
Where the _maybeFocus() function checks for open dialogs:
_maybeFocus() {
  if (document.activeElement.tagName == 'INPUT') return;
  query('#ice').dispatchEvent(new UIEvent('focus'));
}
That seems to do the trick when I try it manually, but the test still fails. And if the test still fails, I think the feature is not quite baked.

Tonight's #pairwithme pair, Santiago Arias, helps me track down the continued failure to the update-preview, which also fires a focus event. We remove that, but again have a false positive—the test that drove it continues to pass even without the feature. Bah. Too many events floating around and the tests that cover them are proving insufficient.

Oh well, there is progress tonight. The focus feature seems to be working again and tonight's new test look solid as it was failing and we were able to make it pass. As for the current false positive, there is always tomorrow.


Day #788

No comments:

Post a Comment