Tuesday, October 15, 2013

Yet Another Awful Hack Needed to Test Keyboard Events in Dart


I really want to be done with this. But at the same time, I cannot leave code broken when is likely to sit for a while.

To summarize: it is currently impossible to generate custom keyboard events in Dart. This is a problem because it is quite difficult to test applications that involve keyboard interaction. This makes me sad.

Hope is not all lost. Recent changes to the bleeding edge of Dart have begun to reintroduce the ability to dynamically create keyboard events. More than just generate keyboard events, the KeyEvent class promises to normalize keyboard behavior across browsers. But since this is bleeding edge, there are problems:
  1. When used in live code, KeyEvent listeners do not work, throwing “Internal Dartium Errors”
  2. Even in test code, tests relying on KeyEvent streams need access to the same event stream used by the application—dispatching to elements is not sufficient
  3. Creating a KeyEvent wraps a corresponding low-level KeyboardEvent that can be dispatched to elements, but it lacks actual keyboard data (keyCode, charCode)
  4. Dispatching the high-level KeyEvent generates Internal Dartium Errors—in test and application code
In a desperate attempt to get my tests passing and useful I have come up with two workarounds. The first addresses #1 and #2. Instead of listening to a stream of KeyEvent, I listen to Element “on” properties like onKeyDown. I still get Internal Dartium Errors, but somehow these do not halt execution. Instead they are seen as warnings, allowing the code to proceed. I still need to accommodate #2, but I do so by caching a single stream in a class. And where I have used that workaround, my code is actually better for it. Reusing a single stream rather than creating new ones each time a listener is needed is a help.

But there are times that I need to dispatch to dynamically created elements, like menu systems. For that, I can use #3 above, though the application code needs to accept zero valued keyCode properties on events. That is not too horrible. The structure of the code remains unaffected for now and once KeyEvent wrapped keyboard events support setting values—I only need remove checks for zero values. I had hoped this would be the end of it until Dart's KeyEvent class stabilized.

Unfortunately, my hack for #3 works when dispatching only one event to an element. My text input fields tend only to need to listen for an Enter keydown event. If there is any keydown event with keyCode equals zero, I can safely assume that I am trying to dynamically create an Enter keydown. Unfortunately for me, the ICE Code Editor also include an arrow-key navigatiable menu system. My tests had been fairly nice thanks to some nice helper code:
      test("down arrow key moves forward in list", (){
        helpers.typeCtrl('o');
        helpers.typeIn('project 1');
        // project 11
        // project 10 *
        // project 1

        helpers.arrowDown(2);

        expect(
          document.activeElement.text,
          equals('Project 10')
        );
      });
And so, finally, I believe that I am out of luck keeping things somewhat sane. Because the menu is dynamically generated, my test cannot easily gain access to a stream to add custom events. So my workaround for #1 and #2 is out. Because I need to distinguish between up and down events, my workaround for #3 is out. I really, really want to stop working on this, but I cannot leave the tests failing. I do not want to skip tests and hope that I come back to fix them later. I need to record the last dynamically created keycode somewhere. If I cannot do that on the event being dispatched, then I will set it in a common class:
class Keys {
  static int _lastKeyCode;
  static set lastKeyCode(v) { _lastKeyCode = v; }
  static get lastKeyCode {
    print('Horrible hack. FIXME ASAP!!!!');
    return _lastKeyCode;
  }
  // ...
}
I use a private, static variable, _lastKeyCode, to hold the value. I define a static getter and setter that wrap this private variable, making it seem like the Keys class has a lastKeyCode property. But in there, I print out a FIXME message that I am not going to ignore for long. To complete my horrid hack, my helpers need to set the “property”:
arrowDown([times=1]) {
  var e = new KeyEvent('keydown', keyCode: KeyCode.DOWN).wrapped;
  Keys.lastKeyCode = KeyCode.DOWN;

  new Iterable.generate(times, (i) {
    document.activeElement.dispatchEvent(e);
  }).toList();
}
And my application code needs to honor it:
  _handleDown(e) {
    if (e.keyCode != KeyCode.DOWN && Keys.lastKeyCode != KeyCode.DOWN) return;
    // ...
  }

Yup, that's pretty ugly. But I have my keyboard handling tests passing again for the first time in two months. I am in no rush to push this to production, but hopefully I am better prepared for the stabilization of KeyEvent. All in all, this is a tough problem to solve—especially given that Dart needs to compile to JavaScript that normalizes behavior across browsers. Light is at the end of the tunnel and I think I have enough duct tape to keep this thing running until the end is reached.

Day #905

No comments:

Post a Comment