Saturday, October 12, 2013

KeyEvents are for Unit Tests


It took me a while, but I think that I have finally realized that the latest KeyEvent changes in Dart are for unit tests, not acceptance tests. I like unit tests for driving new functionality, but rather prefer having acceptance tests around as being better at catching bugs—or at least giving me the confidence that swaths of the application are behaving more or less as designed. Still, unit tests are pretty nice and I hope to put them to use tonight.

Toward that end, I give up (at least temporarily) the hope to get the acceptance tests in the ICE Code Editor working. Instead, I am going to see if I can get the ctrl_alt_foo shortcut library working again.

Actually, working again is not quite right. It works now and the build has never once failed. But it works through an absolutely insane hack involving dynamically created JavaScript code. For the record, I opted for that hack not because Dart lacks keyboard handling, but because it lacked the ability to test it well.

With the JavaScript hack and various Dart hacks that preceded it, I tried my best to stay close to existing Dart APIs. Where Dart has KeyEvent, ctrl_alt_foo has KeyEventX. Where Dart has KeyboardEventStream, ctrl_alt_foo has KeyboardEventStreamX. My sincere hope is that, even though acceptance tests may be out of the question, the recent KeyEvent patched to Dart's bleeding edge are sufficient to remove my “X” versions of Dart core classes.

From what I learned yesterday, I don't think that removing these is an option just yet. For one thing, I need to keep some of the decoration that I add to KeyEvent (e.g. isCtrl, isChar, etc). More pertinent to testing, I need to keep KeyboardEventStreamX because I need it to return the same stream every time.

As I found yesterday, adding custom keyboard events to a stream for testing only works if I am adding to the same stream on which the code is listening. What this means in practice is that it is not (currently) possible to dispatch events to elements in Dart as is possible in JavaScript. Dart limits developers to adding events to streams. If I create multiple streams for an element, then add an event to the last stream, only the last stream will see the event—not all of the streams listening to the same element.

In other words, my existing approach to KeyboardEventStreamX is not going to work because it returns a new stream every time its “on” class methods are invoked:
class KeyboardEventStreamX extends KeyboardEventStream {
  static Stream onKeyDown(EventTarget target) {
    return Element.
      keyDownEvent.
      forTarget(target).
      map((e)=> new KeyEventX(e));
  }
}
Instead, I switch to ShortCut, which needs a way to always use the same stream, but also to return a stream that wraps normal KeyEvent instances in KeyEventX. I settle on returning a static stream controller stream, which will wrap KeyEventX instances and also exposing a static dispatchEvent() method for placing events directly on the single instance of the document.body stream:
class ShortCut {
  // ...
  static var _stream = KeyboardEventStream.onKeyDown(document.body);
  static var _streamController = new StreamController.broadcast();

  static get stream {
    _stream.listen((e) {_streamController.add(new KeyEventX(e));});
    return _streamController.stream;
  }

  static void dispatchEvent(KeyEvent e)=> _stream.add(e);
  // ...
}
I then use this to dispatch events as:
type(String key) {
  ShortCut.dispatchEvent(
    new KeyEvent('keydown', keyCode: keyCodeFor(key))
  );
And can test that the events are working with:
  test("can listen for key events", (){
    _s = ShortCut.stream.listen(expectAsync1((e) {
      expect(e.isKey('A'), true);
    }));

    type('A');
  });
That works:
PASS: can listen for key events 
But I don't know if that makes my life much easier. I am guaranteed one stream for listening to events. The _stream static variable is assigned when ShortCut is loaded. If I limit my testing to dispatching keyboard events through ShortCut.dispatchEvent() that same stream will always be used. So I have side-stepped the multi-stream problem. But the ShortCut.stream value is just a regular Stream, not a CustomStream that supports add(). Forcing events through dispatchEvent() seems likely to cause confusion in the future.

I will call it a night here and think on this approach.


Day #902

No comments:

Post a Comment