Saturday, November 22, 2014

Testing Polymer Updates to Other Elements


Yesterday, I committed a programming cardinal sin. Polymer saw a new release and my code broke. That will happen and is hardly a sin. I fixed my code to address the issue and pushed the fix to the code repository. Again, not a sin.

The sin that I need to confess? I pushed the fix without an accompanying test.

For shame. My penance, of course, is to write a test—and make sure that the test really captures the failures so that I never see them again.

The broken code from last night involved a MutationObserver that synchronizes values from a Polymer element with a plain-old form <input>. The fix was in both the Polymer code (I was not publishing the attribute properly) and in the MutationObserver code which was using some very old wait-for-Polymer setup. The new observer code looks like:
Polymer.whenPolymerReady(function(){
  var input = document.querySelector('#pizza_value');
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.attributeName == 'state') {
    });
  });
  observer.observe(document.querySelector('x-pizza'), {attributes: true});
});
That is nice, but tied to the implementation of my particular code: the #pizza_value, x-pizza, and state values are all hard-coded. That is not great in production code, but for example code in the Patterns in Polymer book, it is fine. Except for testing.

I really want to test the inside of that, so I refactor it as:
Polymer.whenPolymerReady(function(){
  var input = document.querySelector('#pizza_value'),
      el = document.querySelector('x-pizza');
  syncInputFromAttr(input, el, 'state');
});

function syncInputFromAttr(input, el, attr) {
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.attributeName == attr) {
        input.value = mutation.target[attr];
      }
    });
  });
  observer.observe(el, {attributes: true});
}
That should make it easier to test and I think easier to explain in the book (if nothing else, I can split the polymer-ready and observer discussion apart).

Now, let's see if I can test that...

I am still using Karma and Jasmine for testing. I start in my Karma configuration, to which I add this synchronization script to the list of files to load:
    files: [
      'bower_components/webcomponentsjs/webcomponents.js',
      'test/PolymerSetup.js',
      'scripts/sync_polymer_in_form.js',
      {pattern: 'elements/**', included: false, served: true},
      {pattern: 'bower_components/**', included: false, served: true},
      'test/**/*Spec.js'
    ]
That generates an error:
Chrome 39.0.2171 (Linux) ERROR
  Uncaught NotFoundError: Failed to execute 'observe' on 'MutationObserver': The provided node was null.
  at /home/chris/repos/polymer-book/book/code-js/plain_old_forms/scripts/sync_polymer_in_form.js:16
This is because the elements in the when-polymer-ready block do not exist in my tests. I am loath to add conditionals into the actual book code as it is messy to filter out of the book. Instead, I cheat by maybe-adding the <x-pizza> element needed by the smoke test:
document.addEventListener("DOMContentLoaded", function(event) {
  var el = document.querySelector('x-pizza');
  if (el) return;

  document.body.appendChild(document.createElement('x-pizza'));
});
My existing (minimal) test again runs without error, so now I need to write an actual test. I will create an <input> element to synchronize to the value of my Polymer element's state attribute. At the end of the test, the input should have a value of "pepperoni" to reflect the change to the <x-pizza> element:
  describe('syncing <input> values', function(){
    var input;
    // Need setup here....

    it('updates the input', function(){
      expect(input.value).toEqual('pepperoni');
    });
  });
Now, I just need the setup to create the <input> and connect it to the already created <x-pizza> element. Something like the following ought to do the trick:
  describe('syncing <input> values', function(){
    var input;

    beforeEach(function(done){
      input = document.createElement('input');
      container.appendChild(input);

      syncInputFromAttr(input, el, 'state');

      el.model.firstHalfToppings.push('pepperoni');
      el.async(done);
    });

    it('updates the input', function(){
      expect(input.value).toEqual('pepperoni');
    });
  });
Creating the <input> element and appending it to the test's containing <div> is easy enough. Connecting the <input> element's value to the <x-pizza> element's state attribute is done by the newly created syncInputFromAttr(). Lastly, I need to update the internal state of the <x-pizza>, which can be done via the firstHalfTopping property.

The async() callback of Jasmine's done() is the only real trick. When Polymer is done updating the internal state, its bound variables, and the UI, then it calls the callback supplied to async(). By supplying Jasmine's done(), I tell the tests to wait to execute until Polymer has completely updated the element—and allowed the Mutation Observer to see the change and do the same.

Happily, this works as desired:
SUCCESS <x-pizza> element content has a shadow DOM
SUCCESS <x-pizza> syncing <input> values updates the input
More importantly, I can break the test by reverting last night's fixes (not publishing the attribute or using old watch code). Since I can break the test like this, I know that I have successfully captured the error that I previously missed. Better yet, I know that my code will never fail like this again.

Up tomorrow: a little refactoring with Object.observe().



Patterns in Polymer




Day #2

2 comments:

  1. On this exemple, it's Javascript, so you use Karma and Jasmine to test the code. Make sense.
    On the Dart version of THE book, will you show another way to test it ?

    GĂ©rald Reinhart

    ReplyDelete
    Replies
    1. Yup, the book uses two different libraries. It starts with the very excellent unittest that is baked into the Dart language. The book also uses the scheduled_test library, which is also supported by the Dart authors, for better async testing support.

      The bottom line is that Dart testing is leaps and bounds ahead of JavaScript testing :)

      Delete