Wednesday, October 2, 2013

TDD the Shadow DOM in Polymer.dart


I am digging messing about with Polymer.dart and Shadow DOM. That's some good clean fun.

Without much difficulty, I have used Polymer to create an <ice-code-editor> tag that embeds nicely into a regular web page:



It seems that it was easy, in part, because I cheated. The actual code editor is not part of the shadow DOM that contains the title of the element:



In some respects, this might qualify as no-harm, no-foul. After all, it works and no harm is being caused by the ICE Code Editor being part of the main DOM tree. On the other hand, encapsulating all of a Polymer element inside the shadow DOM can keep the contents secure. More relevant to this particular case, it will prevent conflicts between elements. ICE has been developed primarily for a one-instance-per-page use case. It might work with multiple instances, but why risk conflicts if I don't have to?

Anyhow, tonight, I am going to attempt to drive the editor into the shadow DOM using tests (that's a pretty cool sentence, eh?). After last night, I already have some tests that are probing the shadow DOM:
    test("can set line number", (){
      expect(
        query('ice-code-editor').shadowRoot.query('h1').text,
        contains('(42)')
      );
    });
Using that test as a basis, I can describe my expectations as:
    test("creates an editor and preview", (){
      expect(
        query('ice-code-editor').shadowRoot.query('.ice-code-editor-editor'),
        isNotNull
      );

      expect(
        query('ice-code-editor').shadowRoot.query('.ice-code-editor-preview'),
        isNotNull
      );
    });
And, just like that, I have a failing test:
PASS: [polymer] can embed code 
PASS: [polymer] can set line number 
FAIL: [polymer] creates an editor and preview
  Expected: not null
    Actual: <null>
A quick look at my Polymer code shows where I need to address the situation:
@CustomTag('ice-code-editor')
class IceCodeEditorElement extends PolymerElement with ObservableMixin {
  @published String src;
  @published int line_number;

  void created() {
    super.created();

    var container = new DivElement();
    host.append(container);
    var editor = new ICE.Editor(container);
    // Other setup...
  }
}
Instead of adding the container element to the document, I need only add it to the Polymer element's shadow root:
@CustomTag('ice-code-editor')
class IceCodeEditorElement extends PolymerElement with ObservableMixin {
  @published String src;
  @published int line_number;

  void created() {
    super.created();

    var container = new DivElement();
    shadowRoot.append(container);
    var editor = new ICE.Editor(container);
    // Other setup...
  }
}
Easy peasy. Except, of course, it does not work at all.

It turns out that the JavaScript code editor (ACE code editor) that is being used via js-interop does a lot of mucking with the DOM. The real DOM. The DOM that needs to have access to document and various top-level methods.

So I am pretty much out of luck in trying to add the editor to the shadow DOM. At least until the underlying JavaScript code supports the shadow DOM. That said, I am not at a complete dead end. Even though the editor part of ICE cannot be added to the shadow DOM, the preview layer can be:
    test("creates a shadow preview", (){
      expect(
        query('ice-code-editor').shadowRoot.query('.ice-code-editor-preview'),
        isNotNull
      );
    });
I make that test pass by adding a <div> to the shadow DOM and then passing it to the ICE Editor constructor:
@CustomTag('ice-code-editor')
class IceCodeEditorElement extends PolymerElement with ObservableMixin {
  // ...
  void created() {
    super.created();

    var container = new DivElement();
    host.append(container);

    var preview_el = new DivElement();
    shadowRoot.append(preview_el);

    var editor = new ICE.Editor(container, preview_el: preview_el);
    // Other setup...
  }
}
Now that I think about it, it will not hurt to test that the editor is added to the main DOM tree as well:
    test("creates an editor", (){
      expect(
        query('ice-code-editor').query('.ice-code-editor-editor'),
        isNotNull
      );
    });
With that, I have four passing tests:
PASS: [polymer] can embed code 
PASS: [polymer] can set line number 
PASS: [polymer] creates a shadow preview 
PASS: [polymer] creates an editor 
 
All 4 tests passed. 
Best of all, I drove new behavior—element encapsulation—with tests. At some point I will need to verify that multiple instances of <ice-code-editor> on the same page will not cause trouble, but I will leave that for another day. This first pass at TDDing Polymer.dart features seems a fine stopping point for tonight.


Day #892

No comments:

Post a Comment