Tuesday, October 1, 2013

Testing the Shadow DOM


Yet again, the gap between where I expect to find trouble and where I actually find it… amazes. Last night I set out to test a Polymer.dart version of the ICE Code Editor. I cobbled something akin to a test together, but it proved unexpectedly difficult in unexpected ways.

I intentionally kept the implementation and the test heavy—the custom <ice-code-editor> tag has elements of js-interop and HTTP request while the test was of the full-stack variety. I had expected the js-interop or HTTP request bits to cause me problems—and they probably will, if I can get to them. The main problem wound up being the full-stack test.

I suppose that I should have expected this. “Full stack,” as it relates to Polymer, include multiple parts. There is the actual page that uses the custom Polymer element. When testing, this looks something like:
<html>
  <head>
    <link rel="import" href="packages/ice_code_editor/polymer/ice_code_editor.html">
    <!-- Polymer and test setup here --> 
    <script type="application/dart" src="test.dart"></script>
  </head>
  <body>
    <ice-code-editor src="embed.html" line_number="0"></ice-code-editor>
  </body>
</html>
For the <ice-code-editor> element to work, Polymer has to import the definition in the linked ice_code_editor.html file. Even that is not sufficient as ice_code_editor.html contains a reference to the backing class, which is located in yet another file:
<polymer-element name="ice-code-editor" attributes="src,line_number">
  <template><!-- template work here --></template>
  <script type="application/dart" src="ice_code_editor_element.dart"></script>
</polymer-element>
Last night's problem was in the initial load of the ice_code_editor.html template. When I load the test page in Dartium, I get a lovely CORS violation:
XMLHttpRequest cannot load 
  file:///home/chris/repos/ice-code-editor/test/polymer/packages/ice_code_editor/polymer/ice_code_editor.html. 
Cross origin requests are only supported for HTTP.
I was able to get the test working by loading the test page (and everything else) from a simple web server. Possibly because all of my client-side Dart testing to date has been from file:// URLs, running a web server to support tests—even full stack tests—feels like overkill. Realistically, Polymer testing will likely have to involve at least some full-blown web server work. Tonight, I would like to see if I can skip it by supplying a command line option to Dartium.

The --allow-file-access-from-files option should do the trick, so I close Dartium and restart it with:
$ chrome --allow-file-access-from-files
Currently, my test does not actually set an expectation, so I am only checking that everything compiles. In this case, it does as I see no errors or warnings in the console—only a pleasant, test successful message:



But what about real testing? When I use the <ice-code-editor> element on a real page, it currently places an <h1> above the actual code editor that mirror the attributes used in the tag:



If I set the src and line_number attributes in my test page, then I ought to be able to query that <h1> for those values:
<ice-code-editor src="embed.html" line_number="42"></ice-code-editor>
In test.dart, I try to query for the src value by checking the text contents of the first <h1> on the page:
    test("can embed code", (){
      expect(query('h1').text, 'foo');
    });
But that fails. In fact there is no <h1> on the page—not even in the working sample page. The <h1> is in the shadow DOM of the <ice-code-editor>. That is, I now have a DOM fragment that is completely separate from the main document DOM. This shadow DOM is included when the page is rendered, but it is entirely encapsulated within the <ice-code-editor>'s shadow root.

All of this means that I need to query for the <h1> inside the shadow root of <ice-code-editor>:
test("can embed code", (){
      expect(
        query('ice-code-editor').shadowRoot.query('h1').text,
        contains('embed.html')
      );
    });
And that works!
PASS: [polymer] can embed code
All 1 tests passed. 
Just for the heck of it, I write a separate test to query for the line number. This works without incident. The end result test looks as follows:
library polymer_test;

import 'package:unittest/unittest.dart';
import 'dart:html';
import 'dart:async';

main() {
  setUp((){
    var ready = new Completer();
    new Timer(
      new Duration(milliseconds: 250),
      ready.complete
    );
    return ready.future;
  });

  group("[polymer]", (){
    test("can embed code", (){
      expect(
        query('ice-code-editor').shadowRoot.query('h1').text,
        contains('embed.html')
      );
    });
    test("can set line number", (){
      expect(
        query('ice-code-editor').shadowRoot.query('h1').text,
        contains('(42)')
      );
    });
  });
}
The 250 millisecond delay in the setUp() gives ICE enough time to get up and running. I am unsure why (something to investigate another night) such a high number is needed more when running the test page from content_shell than directly in Dartium. I will find a better way of implementing that delay another day. More pressing is that, when I inspect things in the JavaScript console, I find that that the ICE code editor is outside of the <ice-code-editor> shadow root. It is actually in the main document DOM tree, which violates the spirit of Polymer. And I would still like to get this tested without relying on the --allow-file-access-from-files command line option for Dartium.

Those issues aside, I have two simple tests of my Polymer custom element. Once I figured out that --allow-file-access-from-files trick, it was pretty darn easy to write these tests. And where there are tests… robust, accurate, maintainable code is sure to follow.



Day #891

No comments:

Post a Comment