Tuesday, April 30, 2013

Getting Started with a Browser Dart Test Suite

‹prev | My Chain | next›

One of the main reasons that I am switching the ICE Code Editor from JavaScript to Dart is for testing. I keep making all sorts of changes to a moderately large project with no safety net. With the Dart version, I will have type analysis safety net from the outset. Even so, I love me some tests.

The ICE Code Editor is very much a browser-based application. This means that the tests need a simple web page to serve as context for my tests. So I create the following as test/index.html:
<html>
<head>
  <title>ICE Test Suite</title>
  <script type="application/dart" src="editor_test.dart"></script>

  <script type="text/javascript">
    // start dart
    navigator.webkitStartDart();
  </script>
</head>

<body>
<h1>Test!</h1>

</body>
</html>
There is not much to this page. The body has an <h1>, which serves no purpose other than to have something on the page when viewed in a browser. The two <script> tags load the test suite and start the dart engine.

As for the test suite, I start very simple by testing some default values in the Editor class. The test outline will look something like this:
import 'package:unittest/unittest.dart';
import 'package:ice_code_editor/editor.dart';
import 'dart:html';

main() {
  group("defaults", () {
    // tests will go here...
  });
}
The import statements pull in the libraries that are needed for testing: the unit test library, the class being tested, and 'dart:html' for simple DOM manipulation and querying. Next comes the main() function. All Dart scripts need a main() entry point, so I oblige. I start by jamming all of my tests directly inside the body of the main() function. Eventually, I will pull them out, but this will do for now. Lastly, I add a test group(). This is not strictly necessary, but I seem to recall that certain test output formatters appreciate a group().

With that, I am ready to test the class. It is possible to disable auto-update of the preview layer when code changes take place. I am going to test that an Editor instance has auto-update on by default. The test for this is simple enough:
    test("defaults to auto-update the preview", () {
      var it = new Editor('ice');
      expect(it.autoupdate, equals(true));
    });
The entire content of editor_test.dart is now:
import 'package:unittest/unittest.dart';
import 'package:ice_code_editor/editor.dart';
import 'dart:html';

main() {
  group("defaults", () {
    test("defaults to auto-update the preview", () {
      var it = new Editor('ice');
      expect(it.autoupdate, equals(true));
    });
  });
}
To make that pass, I define the Editor class in lib/editor.dart as:
import 'dart:html';

class Editor {
  bool edit_only, autoupdate;
  String title;

  Editor(el, {this.edit_only, this.autoupdate:true, this.title}) {
  }
}
And it passes. When I load the test page up in Dartium and check the console, I see:



Unfortunately, the bug that suppresses console output in DumpRenderTree stills seems to be in place. When I try to test from the command-line, I see console output indicating that the unit test suite is starting, but nothing else:
➜  ice-code-editor git:(master) ✗ DumpRenderTree test/index.html      
CONSOLE MESSAGE: unittest-suite-wait-for-done
Content-Type: text/plain
layer at (0,0) size 800x600
  RenderView at (0,0) size 800x600
layer at (0,0) size 800x600
  RenderBlock {HTML} at (0,0) size 800x600
    RenderBody {BODY} at (8,8) size 784x571
      RenderBlock {H1} at (0,0) size 784x37
        RenderText {#text} at (0,0) size 69x36
          text run at (0,0) width 69: "Test!"
#EOF
#EOF
Happily, the submitter of that bug noted a workaround. I downgrade the unittest version on which I am depending by editing my package's pubspec.yaml to indicate that I want to peg ICE at version 4.0:
name: ice_code_editor
version: 0.0.1
description: Code Editor + Preview
author: Chris Strom <chris@eeecomputes.com>
homepage: https://github.com/eee-c/ice-code-editor
dependencies:
  unittest: 0.4.0
  js: any
Then re-run pub install:
➜  ice-code-editor git:(master) ✗ pub install
Resolving dependencies...
Dependencies installed!
I can then get the desired unit test output on the command-line as well:
➜  ice-code-editor git:(master) ✗ DumpRenderTree test/index.html
CONSOLE MESSAGE: unittest-suite-wait-for-done
Content-Type: text/plain
layer at (0,0) size 800x600
  RenderView at (0,0) size 800x600
layer at (0,0) size 800x600
  RenderBlock {HTML} at (0,0) size 800x600
    RenderBody {BODY} at (8,8) size 784x571
      RenderBlock {H1} at (0,0) size 784x37
        RenderText {#text} at (0,0) size 69x36
          text run at (0,0) width 69: "Test!"
#EOF
#EOF
CONSOLE MESSAGE: PASS: defaults defaults to auto-update the preview
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 1 tests passed.
CONSOLE MESSAGE: unittest-suite-success
For good measure, I ensure that dart_analyzer is happy:
➜  ice-code-editor git:(master) ✗ echo $?
0
Before calling it a night, I get started on the js-interop testing, which I need for things like incorporating ACE code editor. I will leave the actual testing until tomorrow, but I would like to ensure that turning it on does not break anything. So I add it to the Editor definition:
import 'dart:html';
import 'package:js/js.dart' as js;

class Editor {
  bool edit_only, autoupdate;
  String title;

  Editor(el, {this.edit_only:false, this.autoupdate:true, this.title}) {
    var context = js.context;
    context.ace.edit(el);
  }
}
The addition of the js import does not break anything, but trying to retrieve the js.context property cause all sort of bad:
unittest-suite-wait-for-done undefined:1
Uncaught ReferenceError: ReceivePortSync is not defined index.html:54
Exception: 'file:///home/chris/repos/ice-code-editor/test/packages/unittest/unittest.dart': Error: line 763 pos 28: type 'ExpectException' is not loaded
    String message = (e is ExpectException) ? e.message : 'Caught $e';
                           ^
malformed type used.
Stack Trace: #0      _registerException (file:///home/chris/repos/ice-code-editor/test/packages/unittest/unittest.dart:763:28)
#1      guardAsync (file:///home/chris/repos/ice-code-editor/test/packages/unittest/unittest.dart:744:23)
#2      _nextBatch._nextBatch (file:///home/chris/repos/ice-code-editor/test/packages/unittest/unittest.dart:782:23)
#3      runTests.runTests.<anonymous closure> (file:///home/chris/repos/ice-code-editor/test/packages/unittest/unittest.dart:731:16)
#4      _defer.<anonymous closure> (file:///home/chris/repos/ice-code-editor/test/packages/unittest/unittest.dart:688:13)
#5      _ReceivePortImpl._handleMessage (dart:isolate-patch:81:92)
This turns out to be due to the web page that provides context for the test suite. More specifically, it is not sufficient to start the Dart engine if I want to perform any kind of js-interop. There is additional setup required that, thankfully, is included in the browser dart package that is already part of my package dependencies. All I need do is replace this <script> tag in the web page:
<script type="text/javascript">
    // start dart
    navigator.webkitStartDart();
  </script>
With a single source <script> tag instead:
<script src="packages/browser/dart.js"></script>
Armed with that, I am ready to start testing the ACE setup via js-interop. And I'll pick back up there tomorrow.


Day #737

No comments:

Post a Comment