Wednesday, November 20, 2013

Actual Testing of Polymer Elements


Last night, I was able to get the Karma test runner to test a Polymer element. I think.

Honestly, I have good reason to think that it worked: the debug page in Chrome included the Shadow DOM. But I did not actually test any of the Polymer code or resulting display, so tonight I make sure that I have this working.

By way of review, I setup Karma with mostly default values (including using the Jasmine testing framework). The only changes were to preprocess and include test/*.html testing templates and to serve but not include the Polymer files in the testing context page. The relevant section of the configuration:
    /**
     * Compile HTML into JS so that they can be used as templates
     */
    preprocessors: {
      'test/*.html': 'html2js'
    },

    /**
     * Don't include Polymer HTML and JS because Polymer is very
     * particular about the order in which they are added. Serve them,
     * but defer loading to the test setup. Include test HTML
     * fixtures.
     */
    // list of files / patterns to load in the browser
    files: [
      {pattern: 'scripts/**/*.html', included: false},
      {pattern: 'scripts/**/*.js', included: false},
      'test/**/*Spec.js',
      'test/*.html'
    ],
The Polymer project is picky about when Polymer.js needs to be loaded. Well, not too picky—just before code that touches the DOM:
  <head>
    <!-- ... -->
    <!-- 1. Load Polymer before any code that touches the DOM. -->
    <script src="scripts/polymer.min.js"></script>
    <!-- 2. Load component(s) -->
    <link rel="import" href="scripts/pricing-plans.html">
    <link rel="import" href="scripts/pricing-plan.html">
  </head>
This is why I could not allow Karma to include the library itself—by the time Karma includes scripts, it is too late. So my Jasmine test setup does that:
describe('<pricing-plan>', function(){
  beforeEach(function(){
    var script = document.createElement("script");
    script.src = "/base/scripts/polymer.min.js";
    document.getElementsByTagName("head")[0].appendChild(script);

    var link = document.createElement("link");
    link.rel = "import";
    link.href = "/base/pricing-plan.html";
    document.getElementsByTagName("head")[0].appendChild(link);

    document.body.innerHTML = __html__['test/pricing-plan-fixture.html'];
  });
  // Tests go here...
});
And, as I mentioned at the outset, that seems to work. Karma's debug test page has the results that I would expect. But I am only testing that the <pricing-plan> tag, which is defined in the testing template, exists:
  // ...
  describe('defaults', function(){
    var el;
    beforeEach(function(){
      el = document.getElementsByTagName('pricing-plan')[0];
    });

    it('should create an element', function() {
      expect(el).toNotBe(undefined);
    });
  });
  // ...
That test would pass with or without Polymer running properly (and it did last night, which caused me more than a little confusion). What I really want to know about this particular polymer element is: does its shadow DOM include all of the Bootstrap <div> tags that I am trying to hide?

And that's where everything falls apart. Of course, it's always the second test that breaks things...

If I add a test that looks for the default pricing plan's ("Plan") in the shadow DOM of my custom element, it passes. But only if I comment out the original test:
  describe('defaults', function(){
    var el;
    beforeEach(function(){
      el = document.getElementsByTagName('pricing-plan')[0];
    });

    // it('should create an element', function() {
    //   expect(el).toNotBe(undefined);
    // });

    it('defaults to "Plan" as the name', function() {
      expect(el.shadowRoot.textContent).toContain('Plan');
    });
  });
If I have both tests running, then I get assertion failed messages from the second test:
FAILED <pricing-plan> defaults defaults to "Plan" as the name 
More bothersome is an endless stream of Polymer call stack errors:
RangeError {stack: "RangeError: Maximum call stack size exceeded↵    a…ripts/polymer.min.js:32:16781), <anonymous>:2:12)", message: "Maximum call stack size exceeded"}
 "RangeError: Maximum call stack size exceeded
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:1:10)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)" polymer.min.js:32
RangeError {stack: "RangeError: Maximum call stack size exceeded↵    a…ripts/polymer.min.js:32:16781), <anonymous>:2:12)", message: "Maximum call stack size exceeded"}
 "RangeError: Maximum call stack size exceeded
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:1:10)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)
    at Object.eval (eval at k (http://localhost:9876/base/scripts/polymer.min.js:32:16781), <anonymous>:2:12)" 
After a little investigation, I surmise that this is due to the beforeEach() setup which adds the Polymer script file multiple times. And this seems to be the case as I can get both tests passing with a conditional that loads the Polymer files only once:
describe('<pricing-plan>', function(){
  var previouslyLoaded = false;

  beforeEach(function(){
    if (!previouslyLoaded) {
      var el = document.createElement("script");
      el.src = "/base/scripts/polymer.min.js";
      document.getElementsByTagName("head")[0].appendChild(el);

      var link = document.createElement("link");
      link.rel = "import";
      link.href = "/base/scripts/pricing-plan.html";
      document.getElementsByTagName("head")[0].appendChild(link);

      document.body.innerHTML = __html__['test/pricing-plan-fixture.html'];
    }

    previouslyLoaded = true;
    waits(100);
  });
  //  Multiple tests here....
});
Two other things that I note here are that the document.body.innerHTML replacement also needs to be done only once. If that is brought outside the conditional, the second test will fail. Similarly, the waits(100) at the end of the setup also seems to be necessary to give Polymer adequate time to get ready to do its thing.

This does not feel terribly robust. What happens, for instance, I try to test multiple tags with different attributes or dynamically added custom elements? What if I need to test multiple Polymer elements that work together? Will that setup stand up or will I see the return of strange errors? Also, I feel as though I am likely missing a Polymer callback or two with the need for waits(100). Hopefully I can answer those questions and make some improvements in the days to come.


Day #941


No comments:

Post a Comment