Thursday, October 10, 2013

Bleeding Edge Dart Key Events


Please let this work. Please let this work. Please let this work.

For a very long time, I have been trying to get keyboard testing working in Dart. The API has been very much a moving target, but recent changes would seem to suggest that hope is in sight. And possibly already in the builds.

To try the new API out, I start by updating the pubspec.yaml for my ICE Code Editor project:
name: ice_code_editor
# ...
dependencies:
  crypto: any
  js: any
  ctrl_alt_foo:
    path: /home/chris/repos/ctrl-alt-foo
  json: any
dev_dependencies:
  unittest: any
Most of my keyboard shortcut and testing has been extracted out into that ctrl_alt_foo package. By pointing the pubspec.yaml dependency to my local copy, I can edit ctrl_alt_foo code and try the changes out immediately in ICE.

After a quick pub update, I am ready to try some testing. The first test that does some keyboard interaction is verifying that the Escape key closes dialogs:
    test("editor has focus after closing a dialog", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Make a Copy');
      helpers.hitEscape();

      var el = document.
        query('.ice-code-editor-editor').
        query('textarea.ace_text-input');

      expect(document.activeElement, el);
    });
That helper is exported from the ctrl_alt_foo package. I replace the current, non-working implementation with:
hitEscape() {
  KeyEvent.
    keyDownEvent.
    forTarget(document.activeElement).
    add(
      new KeyEvent('keypress', keyCode: KeyName.ESC)
    );
}
The forTarget() method gives me a custom stream. Dart streams do not expose an add() method—a corresponding stream sink would have that—but the new custom stream does support adding objects directly to the stream. At least this is the hope.

Sadly, my hope is dashed when I try to run the test:
ERROR: Focus editor has focus after closing a dialog
  Test failed: Caught No constructor 'KeyEvent' with matching arguments declared in class 'KeyEvent'.
  
  NoSuchMethodError: incorrect number of arguments passed to method named 'KeyEvent'
  Receiver: Type: class 'KeyEvent'
  Tried calling: KeyEvent("keypress", keyCode: "Esc")
  Found: KeyEvent(KeyboardEvent)
  dart:core-patch/errors_patch.dart                                                                                               NoSuchMethodError._throwNew
  package:ctrl_alt_foo/helpers.dart 22:19                                                                                         hitEscape
  ../full_test.dart 200:24                                                                                                        full_tests.<fn>.<fn>
  package:unittest/src/test_case.dart 111:30
This error is coming from my attempt to create the new instance of a KeyEvent. In other words, the most recent release of Dart does not support this yet.

So it's on to the continuous build for me:
$ wget http://gsdview.appspot.com/dart-editor-archive-continuous/latest/darteditor-linux-64.zip
After installing that, restarting Dartium, and re-running the test… it still fails. But it is a different failure!
FAIL: Focus editor has focus after closing a dialog
  Expected: TextAreaElement:<textarea>
    Actual: InputElement:<input>
It seems that I have succeeded in generating a custom event. At the very least, I am no longer seeing outright failures when I create and dispatch my custom event. Not satisfied by moral victories, it is now time to figure out why that custom event is not having the desired effect.

My first attempt is to update the shortcut code in ctrl_alt_foo. The private _createStream() has always been responsible for creating stream subscriptions. Now I can use the new KeyEvent interface to do so:
  void _createStream() {
    var keyCode = // determine keycode here...
    var stream = KeyEvent.keyDownEvent.forTarget(document.body);
    var subscription = stream.listen((e) {
      if (e.keyCode != keyCode) return;

      if (e.ctrlKey  != isCtrl) return;
      if (e.shiftKey != isShift) return;
      if (e.metaKey  != isMeta) return;

      e.preventDefault();
      cb();
    });

    subscriptions.add(subscription);
  }
None of the other code needs to change here, which is nice. Nice, except my test continues to fail. Worse, when I try the copy dialog (the subject of this test) in the application, I get a lovely stack trace:
Exception: InvalidStateError: Internal Dartium Exception
undefined
Greaaaat.

There are a lot of event listeners in the ICE Code Editor, so I decide to try this in a simple test case. I create a new project with a pubspect.yaml that pulls in the browser compatibility package:
name: test
dependencies:
  browser: any
Next, I add a simple page:
<head>
  <script src="packages/browser/dart.js"></script>
  <script src="main.dart" type="application/dart"></script>
</head>
<h1>Hello</h1>
<div style="width:600px; height: 400px" id="ice"></div>
Finally, I add the referenced main.dart as:
import 'dart:html';
main(){
  var stream = KeyEvent.keyDownEvent.forTarget(document.body);
  var subscription = stream.listen((e) {
    print('yo');
  });
}
When I load the page and hit a key, I still get the same error:



The test code for the new KeyEvent feature exercises three different subscription models. When I try these in my sample page:
main(){
  // var stream = KeyEvent.keyDownEvent.forTarget(document);
  // var subscription = stream.listen((e) {
  //   print('yo');
  // });

  // var subscription = KeyboardEventStream.onKeyDown(document.body).listen(
  //     (e) => print('KeyboardEventStream listener'));
  // var subscription2 = KeyEvent.keyDownEvent.forTarget(document.body).listen(
  //     (e) => print('KeyEvent listener'));
  var subscription3 = document.body.onKeyDown.listen(
      (e) => print('regular listener'));
}
Only the element property stream works. The KeyboardEventStream and KeyEvent streams both generate that same Internal Dartium Exception.

Well, they don't call it bleeding edge for nothing. The new KeyEvent API looks fairly nice and ought to be more or less a drop-in replacement for what I have currently. But it may be a while longer before I can use it.


Day #900

No comments:

Post a Comment