Sunday, November 30, 2014

Jasime/Karma Testing a Polymer Form Input Element


Tonight, I test. Last night, I was too uncertain to test, but having written some exploratory code, I have a better handle on what I'd like my <a-form-input> Polymer element to do.

The <a-form-input> element is a hack to get Polymer elements to behave like native HTML <form> input elements. The hack is simple enough—it breaks Polymer element encapsulation (admittedly one of the principal tenants of Polymer coding) to inject a hidden <input> into the DOM of the containing document. My hesitation last night was caused by uncertainty over how subclass' shadow DOM would interact with the containing page. This is an important point as <a-form-input> is meant to serve as a Polymer "base class" for other elements that want to work in forms. Thankfully, my exploratory code proved that my initial concerns were unwarranted (though problems may still arise).

So tonight, I test. I have a simple <x-double> test element that extends <a-form-input> for unit testing purposes:
<link rel="import" href="../a-form-input.html">
<polymer-element name="x-double" extends="a-form-input" attributes="in">
  <template></template>
  <script>
    Polymer("x-double", {
      attached: function(){
        this.super();
        this.inChanged();
      },
      inChanged: function(){
        this.value = parseInt(this.in) * 2;
      }
    });
  </script>
</polymer-element>
As a test element, it is intentionally simple. It observes the in attribute for changes and, upon seeing one, doubles the in-value placing the result in the value attribute. To test <a-form-input>, this <x-double> element extends it.

I am using Karma and Jasmine to test. The Karma configuration includes the test and library code files for serving, but does not load it:
    // list of files / patterns to load in the browser
    files: [
      'bower_components/webcomponentsjs/webcomponents.js',
      'test/PolymerSetup.js',

      {pattern: 'bower_components/**', included: false, served: true},
      {pattern: '*.html', included: false, served: true},
      {pattern: '*.js', included: false, served: true},
      {pattern: 'test/*.html', included: false, served: true},
      'test/*Spec.js'
    ],
The testing setup, taken directly from Patterns in Polymer is responsible for loading the elements.

The setup for the tests themselves will also be lifted from the book. I create a container element (making it easier to tear down after each test) which contains the target element being tested:
describe('<a-form-input> element', function(){
  var container, el;

  beforeEach(function(done){
    container = document.createElement("div");
    container.innerHTML = '<x-double></x-double>';
    document.body.appendChild(container);
    el = document.querySelector('x-double');
    setTimeout(done, 0); // One event loop for elements to register in Polymer
  });
  afterEach(function(){
    document.body.removeChild(container);
  });
  // Tests here...
});
As for the tests themselves, I can borrow them from the Dart version. The most useful tests (I think) would be the following, which describe the usual properties and attributes of a form input element:
  describe('form input element', function(){
    it('is created in the parent light DOM', function(){});
    it('updates name when "name" is updated', function() {});
    it('updates value when internal state changes', function() {});
  });

  describe('properties', function(){
    it('updates name when name is set', function() {});
    it('updates value when internal state changes', function() {});
  });
And, thanks to last night's exploratory code, these tests just work when implemented:
  describe('form input element', function(){
    it('is created in the parent light DOM', function(){
      var input = container.querySelector('input');
      expect(input).not.toBe(null);
    });
    it('updates name when "name" is updated', function(done) {
      var input = container.querySelector('input');

      el.name = 'my_input_name';
      el.async(function(){
        expect(input.name).toEqual('my_input_name');
        done();
      });
    });
    it('updates value when internal state changes', function(done) {
      var input = container.querySelector('input');

      el.in = '5';
      el.async(function(){
        expect(input.value).toEqual('10');
        done();
      });
    });
  });
Well all but one of them works:
Chrome 39.0.2171 (Linux) <a-form-input> element form input element updates name when "name" is updated FAILED
        Expected 'null' to equal 'my_input_name'.
        Error: Expected 'null' to equal 'my_input_name'.
            at x-double.<anonymous> (/home/chris/repos/a-form-input/test/AFormInputSpec.js:27:28)
            at x-double.<anonymous> (/home/chris/repos/a-form-input/bower_components/polymer/polymer.js:8958:34)
...
Chrome 39.0.2171 (Linux): Executed 5 of 5 (1 FAILED) (0.146 secs / 0.141 secs)
I have yet to teach <a-form-input> how to update the name attribute on the input element as I had done with the value attribute. The fix is easy enough—I need only add a corresponding nameChanged() method to react accordingly:
Polymer('a-form-input', {
  publish: {
    name: {value: 0, reflect: true},
    value: {value: 0, reflect: true},

    attached: function(){
      this.lightInput = document.createElement('input');
      this.lightInput.type = 'hidden';
      this.lightInput.name = this.getAttribute('name');

      this.parentElement.appendChild(this.lightInput);
    },

    nameChanged: function(){
      this.lightInput.name = this.name;
    },

    valueChanged: function(){
      this.lightInput.value = this.value;
    }
  }
});
With that, I have all of the tests passing that I hoped to have from the outset:
INFO [watcher]: Changed file "/home/chris/repos/a-form-input/a_form_input.js".
.....
Chrome 39.0.2171 (Linux): Executed 5 of 5 SUCCESS (0.158 secs / 0.152 secs)
I should probably expand on the distinction between attributes and properties, but so far, so good. This seems like a reasonable solution for the book.


Day #10

No comments:

Post a Comment