Friday, June 20, 2014

More Dart Testing of JavaScript Polymer Elements


I finally got so fed up with JavaScript testing frameworks last night that I did something truly silly. I started testing my JavaScript with Dart.

I begin to despair of the state of testing in JavaScript. It seems that no matter how much progress is made, testing new things will always be prohibitively hard. The ever growing number of JavaScript testing frameworks and runners do a fair job of facilitating known coding domains, but stray too far from the known and… ick.

The ick last night came in the form if a simple Polymer element, <apply-author-styles>. This element copies CSS from the main page into other Polymer elements—the idea being that native elements inside these other Polymer elements should look like native elements on the page. The actual test is ridiculously easy: add a <style> to the page, insert <apply-author-styles> into test element, and verify that the same <style> is now part of the test element's shadow DOM.

Four days ago I made the mistake of starting a post exploring this test with the questions, “how hard can it be?”

Well, Karma can not handle it because there is no way to alter the test page to insert a <style> tag (at least not without resorting to JavaScript which won't work on this element). It turns out that grunt-contrib-jasmine will not work either, mostly because its browser is incompatible with Polymer.

I have no doubt that there is a test runner / test framework out there that is capable of the job, but I have no desire to spend my days building on an already too familiar knowledge of so many JavaScript testing frameworks. Life is too damn short.

So last night, I gave up and used Dart. And it just worked. I am not going to claim that Dart is the end solution for all JavaScript testing, but it worked in this case and not without good reason. The default implementation is embedded in a Chrome variant so both Dart and JavaScript code should work. Dart has an excellent built-in testing framework and runner. So it seemed worth a try and I was rewarded. That said, there are still a couple of outstanding questions.

First up, is why does the test pass in content_shell, but not in Dartium? As mentioned above, the test is simple enough: I define a page style that includes orange button borders, include <apply-author-styles> to a test fixture tag <x-foo>, then test:
  group("[styles]", (){
    test('the app returns OK', (){
      var xFoo = query('x-foo');
      expect(xFoo.shadowRoot.text, contains('orange'));
    });
  });
Isn't that a pretty test? I find the <x-foo> tag on the test page and set my expectation that its shadow root contains “orange.” This works fine in content_shell, the headless testing version of Dartium:
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: [styles] the app returns OK
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 1 tests passed.
CONSOLE MESSAGE: unittest-suite-success
CONSOLE WARNING: line 213: PASS
But fails in Dartium proper:
unittest-suite-wait-for-done
ERROR: [styles] the app returns OK
  Test failed: Caught The null object does not have a getter 'text'.
  
  NoSuchMethodError: method not found: 'text'
  Receiver: null
  Arguments: []
  dart:core-patch/object_patch.dart 45                                                                                       Object.noSuchMethod
  test.dart 22:30 
It seems that my test <x-foo> element simply does not have a shadowRoot. Since it is null, calling text on it raises an exception. But why?

If I inspect the actual element on the page, it really does not have a shadow DOM:



Compare the same element as seen on Chrome 35:



In the JavaScript console settings for Dartium, the “Show Shadow DOM” setting is enabled and yet… no shadow DOM. And I cannot figure out why. My best guess is that the version of Chromium (34) against with Dartium is built lacks some native functionality that Polymer would otherwise use. Lacking that, it places the element directly in the light DOM which works visually, but breaks my test.

I am a little concerned that content_shell works differently than Dartium. Interestingly, it reports a really old Chrome version in the navigator.userAgent property:
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/19.77.34.5 (Dart) Safari/537.36"
I may have to report that. For now, I am content that it behaves like it ought to behave.

To prove that it is not all sunshine and roses, I still need to deal with some impedance mismatch between Dart and JavaScript. Specifically, the project is laid out like a Bower project, not a Dart project. This forces me manually symlink a bunch of things in the test directory:
➜  apply-author-styles git:(master) ✗ ls -l test
total 28
drwxr-xr-x 2 chris chris 4096 Jun 19 23:37 apply-author-styles
lrwxrwxrwx 1 chris chris   29 Jun 19 23:32 core-ajax -> ../bower_components/core-ajax
lrwxrwxrwx 1 chris chris   39 Jun 19 23:32 core-component-page -> ../bower_components/core-component-page
-rw-r--r-- 1 chris chris  634 Jun 19 23:49 index.html
lrwxrwxrwx 1 chris chris   11 Jun 19 23:37 packages -> ../packages
lrwxrwxrwx 1 chris chris   28 Jun 19 23:32 platform -> ../bower_components/platform
lrwxrwxrwx 1 chris chris   27 Jun 19 23:32 polymer -> ../bower_components/polymer
-rw-r--r-- 1 chris chris  773 Jun 20 23:07 test.dart
-rwxr-xr-x 1 chris chris  292 Jun 20 22:18 test_runner.sh
-rw-r--r-- 1 chris chris  300 Jun 19 23:44 x-foo.html
Crazy as it might seem, I am still using Grunt to manage the test run (mostly for the simple, static web server). Thankfully, it includes symlink and clean tasks. So I install those:
$ npm install grunt-contrib-symlink --save-dev
$ npm install grunt-contrib-clean --save-dev
Then I load those tasks in my Gruntfile.js:
module.exports = function(grunt) {
  grunt.initConfig({
    // ...
  });

  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-symlink');
  grunt.loadNpmTasks('grunt-shell');
  grunt.loadNpmTasks('grunt-contrib-connect');
  grunt.loadNpmTasks('grunt-contrib-clean');

  grunt.registerTask('default', ['symlink:test_files', 'connect', 'shell:test', 'clean:test_files']);
};
Also in there, I start my default testing task with a new symlink:test_files task and end with a clean:test_files task. Configuration of both is fairly easy thanks to the nice documentation:
module.exports = function(grunt) {
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    // ...
    symlink: {
      test_files: {
        files: [
          {
            expand: true,
            overwrite: true,
            cwd: 'bower_components',
            src: ['*'],
            dest: 'test'
          }
        ]
      }
    },
    // ...
    clean: {
      test_files: ['test/core-*', 'test/platform', 'test/polymer']
    }
  });
  // ...
  grunt.registerTask('default', ['symlink:test_files', 'connect', 'shell:test', 'clean:test_files']);
};
That cleans things up fairly nicely. Now when I run my default grunt command, it starts by creating the necessary symbolic links, runs the static server, then the tests, and finally cleans everything up after itself.

Unfortunately, this leaves me in a state of using NPM, Grunt, Bower, and Dart on this very simple Polymer element. It might be worth investigating replacing Grunt/NPM with something like Hop in Dart. But I leave that for another day.


Day #99

2 comments:

  1. About apply author styles to polymer elements, I see a big problem in the polymer design itself.
    There’s an inherent and unresolved tension in creating styleable general-purpose components. Polymer is just born as a platform, and it may not shows yet. This problem will become evident when a component evolves and acquires more styling. Suppose you give your component a light gray background so that its “out of the box” appearance looks reasonable. An author overrides that background to be red so that it fits in with their red visual theme. Later, you decide that your component requires a border somewhere to clearly delineate its contents from the outer page. Unfortunately, the aforementioned author wasn’t prepared to override this new border. When they pick up a new version of your component, they’ll end up with a red background but a gray border. That may not be what they want. If this happens too often, the author may come to feel that the use of a general-purpose component is not worth the trouble. The best resolution to this tension is still an open question. For the time being, your best bet is to give your general-purpose component an extremely basic visual appearance. But with time authors would end thinking think that is easier and safer to write all the elements himself in plain html with his own styling, and this could spell the end of the Polymer Elements.

    ReplyDelete
    Replies
    1. I'm not _too_ worried about that. Yet.

      Some of it might be solved by adding a keep-default-element-styles attribute to apply-author-elements. Without it, any styling added to the element would have to come from the page and only the page. With it, then the Polymer element would get its styles from a combination of element and page styles.

      But really, the apply-author-styles tag is an edge case. I think the work the team is doing with the core-style tag (https://github.com/Polymer/core-style/blob/master/core-style.html) will eventually solve this and many other styling problems.

      Delete