Thursday, August 8, 2013

Better (but still not great) Keyboard Testing in Dart


After struggling (again) with keyboard events in Dart last night, it occurs to me that I have missed an opportunity to improve what tests I have.

I have built up several test helpers in the ICE Code Editor. One of them creates KeyboardEvent for control characters:
typeCtrl(char) {
  document.activeElement.dispatchEvent(
    new KeyboardEvent(
      'keydown',
      keyIdentifier: char,
      ctrlKey: true
    )
  );
}
For whatever UI element happens to be currently active, this helper will create a keydown keyboard event for pressing the Control key along with the supplied character. This is wonderful for writing easy-to-read functional UI tests:
    test("can open the new dialog", (){
      helpers.typeCtrl('n');
      expect(
        queryAll('button'),
        helpers.elementsContain('Save')
      );
    });
In other words, when I hit Ctrl+N, I expect that a dialog with a “Save” button will appear.

The problem is that I am passing in the character 'n' to the typeCtrl() helper, which is then assigned to the keyIdentifier named parameter in the helper. The reason that this is a problem is that, as I noted last night, Chrome (and Dartium) do not make the string 'n' available in real events. Instead, they populate the event's $dom_keyIdentifier with a unicode encoded string for the “N” key: 'U+004E'.

To support both the 'n' string that my tests supply and the 'U+004E' string that Chrome supplies, I have code that checks for both:
    document.onKeyDown.listen((e) {
      if (!e.ctrlKey) return;
      switch(e.$dom_keyIdentifier) {
        case 'n':
        case 'U+004E':
          new NewProjectDialog(this).open();
          e.preventDefault();
          break;
        // ...
      }
    });
What I should be doing in my helper is converting the 'n' character into that weird unicode encoded string. I can do that with a simple function that does just that:
String keyIdentifierFor(char) {
  if (char.codeUnits.length != 1) fail("Don't know how to type “$char”");

  // Keys are uppercase (see Event.keyCode)
  var key = char.toUpperCase();

  return 'U+00' + key.codeUnits.first.toRadixString(16).toUpperCase();
}
I am not handling more complex characters than simple, single-byte ASCII for the time being. It should not be too hard to do so, but until that is needed, I fail the helper if something more than a simple ASCII character is encountered. Keyup and keydown keyCodes in old-fashioned DOM coding are the uppercase version of the character pressed (keypress supplies the actual character generated by the key press), which is the reason for the intermediate key that is assigned the uppercase version of the supplied character. Finally, I convert the string to its hexadecimal representation.

The conversion is a bit awkward. The codeUnits of a Dart string are the actual bytes that represent a character in UTF-16. In this case, I am only handling the simplest ASCII characters, which only have a single byte (which is ensured by the guard clause at the top of this function). I take the integer value for that byte and convert it to a hexadecimal string with toRadixString(16). Lastly, I convert the resultant hexadecimal string to upper case so that '4e' becomes '4E'. Like I said: awkward. If anyone knows a better way to do this, please let me know.

With that function in hand, I can update my typeCtrl() helper to read:
typeCtrl(char) {
  document.activeElement.dispatchEvent(
    new KeyboardEvent(
      'keydown',
      keyIdentifier: keyIdentifierFor(char),
      ctrlKey: true
    )
  );
}
And finally the actual code becomes:
      switch(e.$dom_keyIdentifier) {
        case 'U+004E':
          new NewProjectDialog(this).open();
          e.preventDefault();
          break;
        // ....
      }
That might seem a long way to go to eliminate a single line from the switch statement, but that misses the point. What I have done here is made my test better mimic real keyboard events that the code will encounter in the browser.

The whole purpose of testing code is robust, accurate, maintainable code. With this change, I have gone a long way toward improving the middle of that triumvirate. My code is no longer cluttered with testing noise that has no effect in a production environment.

Of course, once Dart gets better at normalizing this behavior across browsers and supports generating these events with accurate properties (e.g. keyCode) this will all be orders of magnitude better. Until then, I will take my small, but important, victories where I can.


Day #837

No comments:

Post a Comment