Showing posts with label phantomjs. Show all posts
Showing posts with label phantomjs. Show all posts

Saturday, February 15, 2014

Polymer and the Native Elements PhantomJS Lacks


The first edition of Patterns in Polymer is due today, leaving me swamped with last minute edits and fixes. But the gods of the chain require their daily sacrifice…

I have very much enjoyed mucking with testing Polymer under Karma and PhantomJS. Testing Polymer with Karma running the tests on Chrome works just fine, but, so far, I have been unable to get the same tests to run on PhantomJS. For the past two days, I have not made much progress getting it working, but the yaks along the way have been useful.

Unfortunately, my stopping point last night suggests that, if it is to work on PhantomJS, some changes will need to be made to Polymer itself. The errors that I had seen in the compressed version of Polymer:
PhantomJS 1.9.7 (Linux) ERROR
        TypeError: 'undefined' is not an object (evaluating 'a.prototype')
        at /home/chris/repos/polymer-book/book/code-js/svg/bower_components/platform/platform.js:29
Turned out to be coming from code in Polymer that wrapped existing elements in polyfilled equivalents. The errors from the uncompressed version of Polymer were along the lines of:
PhantomJS 1.9.7 (Linux) ERROR
        TypeError: 'undefined' is not an object (evaluating 'nativeConstructor.prototype')
        at /home/chris/repos/polymer-book/play/svg/js/scripts/platform/platform.js:1324
That comes from the Polymer platform's register() method for native objects:
        function register(nativeConstructor, wrapperConstructor, opt_instance) {
          // ...
        }
The crux of the problem would appear to be that PhantomJS lacks certain native elements. And, unfortunately, the code in Polymer that tries to use these elements appears extremely procedural. There are a series of anonymous function calls in the Polymer platform that try to wrap the native types that look like:
    (function(scope) {
        "";
        // ...
        var OriginalHTMLImageElement = window.HTMLImageElement;
        // ...
        registerWrapper(OriginalHTMLImageElement, HTMLImageElement, document.createElement("img"));
        // ...
    })(window.ShadowDOMPolyfill);
After manually commenting out this HTMLImageElement wrapper in the platform library (because PhatomJS seems to lack it), I find that I also need to comment out similar code for HTMLMediaElement, HTMLContentElement, and Selection.

That gets me past the original problem. Unfortunately, I find myself dumped into a morass of undefined and null values. At this point, I think it best to call this a dead mine vein. Between some of the seemingly innocuous (at the time) ways in which I shaved yaks previously and the actual hand editing of Polymer code today, I think I am likely too far gone to extract any more useful information from this exercise.

Although ultimately unsuccessful, this expedition was far from a waste of time. I learned much about how Polymer is built and some of the inner workings of the platform. I also got to know PhantomJS a bit better. To be sure, I would have liked to have gotten Karma Polymer tests running on PhantomJS. Maybe I can take another run at this with an alternate approach some day.

For now, it's back to editing. So many typos…


Day #1,027

Friday, February 14, 2014

Yak Boxed: Day 2


I cannot stop. I have no idea how close I am to getting Polymer testing working with Karma and PhatomJS, but last night's stopping point felt a little arbitrary.

I enjoyed last night's yaks, so tonight, I give myself an extension on yak-boxed development. Owing to the need to finish up Patterns in Polymer, I yak-box tonight's efforts to no more than 4 yaks shaved before I call it a night.

Last night ended when I was unable to figure out from where the complaints about undefined PolymerExpression were originating. Before tackling that, I take a step back and shave…

Yak #1

If I am building the unminified polyfill platform from source, I ought to do the same with the actual Polymer code. Unlike building platform.js, that requires a little manual effort. Working with the same polymer-dev repository that I downloaded last night, I edit the Grunt configuration. Specifically, I uncomment the configuration that tells the uglify plugin to leave the result uncompressed and human readable:
module.exports = function(grunt) {
  var readManifest = require('../tools/loader/readManifest.js');
  var Polymer = readManifest('build.json');

  grunt.initConfig({
    // ...
    uglify: {
      // ...
      Polymer: {
        options: {
          sourceMap: true,
          sourceMapName: 'build/polymer.js.map',
          sourceMapIncludeSources: true,
          banner: grunt.file.read('LICENSE') + '// @version: <%= buildversion %>',
          mangle: false, beautify: true, compress: false
        },
        files: {
          'build/polymer.js': Polymer
        }
      }
    },
    // ...
  });
  // ...
};
With that, the default Grunt task in polymer-dev will build the very large version of Polymer that I desire:
➜  polymer-dev git:(master) ✗ grunt
Running "version" task

Running "uglify:Polymer" (uglify) task
File build/polymer.js.map created (source map).
File build/polymer.js created.

Done, without errors.
➜  polymer-dev git:(master) ✗ ls -lh build
total 172K
-rw-r--r-- 1 chris chris  208 Feb 13 23:07 polymer.html
-rw-r--r-- 1 chris chris  53K Feb 14 22:28 polymer.js
-rw-r--r-- 1 chris chris 110K Feb 14 22:28 polymer.js.map
This brings me to…

Yak #2

I need these really large files because trying to debug minfied code in PhantomJS is rubbish. I need real line numbers and readable code if I am going to make any headway. I also think that I need the HTML import files in addition to the JS files. Last night I tried copying the generated JavaScript over top of Bower installed components. Tonight, I carve out space for these builds:
➜  js git:(master) mkdir scripts
➜  js git:(master) cp -r ~/repos/polymer_local/components/polymer-dev/build scripts/polymer
➜  js git:(master) ✗ cp -r ~/repos/polymer_local/components/platform-dev/build scripts/platform
Then, in my karma.conf.js configuration file, I add this scripts directory to the list of files that will be served, but not automatically included:
    files: [
      'test/PolymerSetup.js',
      {pattern: 'elements/**', included: false, served: true},
      {pattern: 'scripts/**', included: false, served: true},
      {pattern: 'bower_components/**', included: false, served: true},
      'test/**/*Spec.js'
    ],
    // ...
They are not automatically included because a test setup script does that. I also have to update that to pull in these newly build versions of Polymer and its platform:
// ...
var script = document.createElement("script");
script.src = "/base/scripts/platform/platform-lite.concat.js";
document.getElementsByTagName("head")[0].appendChild(script);
// ...
With that, I still get the following error when I run karma:
PhantomJS 1.9.7 (Linux) ERROR
        ReferenceError: Can't find variable: PolymerExpressions
        at undefined:29
So I am on to…

Yak #3

I think that yesterday's grunt concat task for building Polymer's platform was incorrect. Instead, I need to run grunt with no arguments to build the default (minified) task. BUT… first, I modify the build task with the same non-compress options that I used for building Polymer. After re-copying the build directory into my application's scripts directory and pointing everything to the now non-compressed platform.js, I get a new error:
PhantomJS 1.9.7 (Linux) ERROR
        SyntaxError: Parse error
        at /home/chris/repos/polymer-book/play/svg/js/scripts/platform/platform.js:2662
PhantomJS 1.9.7 (Linux): Executed 0 of 1 ERROR (0.165 secs / 0 secs)
Bother. That is not the same “prototype” error that started me on this adventure, which means that it is time for…

Yak #4

This parse error occurs on a setter in platform.js:
            // ...
            set textContent(textContent) {
              // ...
            },
            // ...
This turns out to be due to the parameter being the same name as the method. That seems like bad form, but not necessarily worth of a parse error. Unfortunately, I do not have many options other than to manually change each of these. Prefixing the parameter with an underscore (and updating the method body accordingly) does the trick:
            // ...
            set textContent(_textContent) {
              // ...
            },
            // ...
And, with that, I finally get:
PhantomJS 1.9.7 (Linux) ERROR
        TypeError: 'undefined' is not an object (evaluating 'nativeConstructor.prototype')
        at /home/chris/repos/polymer-book/play/svg/js/scripts/platform/platform.js:1324
That looks very much like my original error from the compressed JavaScript:
PhantomJS 1.9.7 (Linux) ERROR
        TypeError: 'undefined' is not an object (evaluating 'a.prototype')
        at /home/chris/repos/polymer-book/book/code-js/svg/bower_components/platform/platform.js:29
Unfortunately, the code in question is rather opaque:
        function register(nativeConstructor, wrapperConstructor, opt_instance) {
            var nativePrototype = nativeConstructor.prototype;
            registerInternal(nativePrototype, wrapperConstructor, opt_instance);
            mixinStatics(wrapperConstructor, nativeConstructor);
        }
I am not at all sure how Polymer is reaching this point and what I might do to trick PhantomJS into using a more appropriate value here. But, I have reached my yak limit for tonight, so I leave that for another day.

Day #1,026

Thursday, February 13, 2014

Yak Boxed Development


Last night I found that I could not test my Polymer code under PhantomJS. Today I put all of the JavaScript code sample in Patterns in Polymer under test. It turns out to be really, really slow to restart Chrome for every chapter. Who knew?!

So tonight, I revisit the problems with running Karma Polymer tests on PhantomJS. Rather than time-boxing this, I will yak-box it. I will shave no more than 7 yaks in an effort to get this working.

I am still working with the same setup that I identified way back when I started researching Polymer. The only difference is that I am now using Bower to install Polymer and other dependencies. The means that the files setting in karma.conf.js has changed to:
    files: [
      'test/PolymerSetup.js',
      {pattern: 'elements/**', included: false, served: true},
      {pattern: 'bower_components/**', included: false, served: true},
      'test/**/*Spec.js'
    ]
And the PolyerSetup.js file now loads in the polyfilled platform accordingly:
// ...
var script = document.createElement("script");
script.src = "/base/bower_components/platform/platform.js";
document.getElementsByTagName("head")[0].appendChild(script);
// ...

Yak #1

My smoke test passes in Chrome, but fails PhantomJS with:
PhantomJS 1.9.7 (Linux) ERROR
        ReferenceError: Can't find variable: Window
        at /home/chris/repos/polymer-book/play/svg/js/bower_components/platform/platform.js:30
I fixed that last night by declaring a Window function:
function Window(){};
Well “fix” is a strong word…

Yak #2

I am now getting a different error:
PhantomJS 1.9.7 (Linux) ERROR
        TypeError: 'undefined' is not an object (evaluating 'a.prototype')
        at /home/chris/repos/polymer-book/play/svg/js/bower_components/platform/platform.js:29
My problem here is that this is a minified version of the library and line 29 is very long and contains many a.prototype references.

So I try setting debug flags in PolymerSetup.js before the polyfilled platform is loaded:
function Window(){};
Platform = {
  flags: {
   debug: true,
   log: 'bind,ready'
  }
};
That has absolutely no effect.

Yak #3

So I install Polymer from source in the hopes that I can use an unminified version of the library:
➜  repos  mkdir polymer_local; cd polymer_local
➜  polymer_local  git clone https://github.com/Polymer/tools.git
Cloning into 'tools'...
...
➜  polymer_local  ./tools/bin/pull-all.sh
...
Looking through the various repositories, I can find absolutely no indication of where an unminified version of the platform is, so it seems that I need to build it.

Yak #4

Eventually, I find that the Grunt file in platform-dev looks like it builds. So I move into that directory, npm install the dependencies and try one of the Grunt tasks that looks promising:
➜  platform-dev git:(master) npm install
...
➜  platform-dev git:(master) grunt build-lite
Running "concat:lite" (concat) task
File "build/platform-lite.concat.js" created.

Done, without errors.
Now I need to figure out how to use that file in my existing tests.

Yak #5

Here, I cheat and copy this directly on top of the bower installed version:
➜  platform git:(master) ✗ mv platform.js platform.js.orig
➜  platform git:(master) ✗ cp ~/repos/polymer_local/components/platform-dev/build/platform-lite.concat.js platform.js
With that, I finally have a more useful error message:
PhantomJS 1.9.7 (Linux) ERROR
        TypeError: 'undefined' is not a function (evaluating 'importer.loaded.bind(importer)')
        at /home/chris/repos/polymer-book/play/svg/js/bower_components/platform/platform.js:1106

Yak #6

That turns out to be a simple lack of Function.prototype.bind in PhantomJS. The workaround comes from MDN, which has a polyfilled version of this method. I add that to the beginning of PolymerSetup.js:
function Window(){};
if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    // ...
  };
}
With that, I get:
PhantomJS 1.9.7 (Linux) ERROR
        ReferenceError: Can't find variable: PolymerExpressions
        at undefined:29

Yak #7

Unfortunately, that one has me stumped.

Ah well, I may not have managed to get this working, but I was finally able to figure out how to install a non-minified version of Polymer. Thanks to source maps, that is not usually necessary, but there are the odd occasions (like this) where it comes in handy.

Day #1,026

Monday, December 19, 2011

Exit Codes from PhantomJS Jasmine Runs

‹prev | My Chain | next›

Last night, I was able to get some pretty OK output from my jasmine test suite when run with PhantomJS. I had to use the jasmine server provided by the jasmine ruby gem to get my require.js structured Backbone.js application running under test, but it was worth it if it makes testing easy on a continuous integration server.

The thing about continuous integration servers is that they like proper exit codes from builds. A non-zero exit code indicates a failure on Unix systems, but the run-jasmine.js script that is supplied in the PhantomJS examples always returns 0:
        waitFor(
          function(){
            return page.evaluate(function(){
              // Run the Jasmine suite on the page
            });
          }, 
          function(){
            page.evaluate(function(){ 
              // Inspect the results
            });
            phantom.exit();
          }
        );
The problem is that, inside the page.evaluate the anonymous function has no access to PhantomJS variables. So something like this will not work:
        var passed;
        waitFor(
          function(){ // Run the Jasmine suite on the page }, 
          function(){
            page.evaluate(function(){ 
              // Inspect the results

              passed = jasmineEnv.currentRunner().results().passed();
            });
            phantom.exit(passed);
          }
        );
The value of passed at the very end will always be undefined.

I could return a value from page.evaluate, but the enclosing waitFor() does not return a value.

Without any better solution, I try to write to the file system:
page.evaluate(function(){
  var fs = require('fs');
  // ...
})
But even that does not work for me:
Error: Module name 'fs' has not been loaded yet for context: _
http://requirejs.org/docs/errors.html#notloaded
Unfortunately, I seem to be stuck here.

Update: Figured it out. The indentation in my Javascript was confusing me. It turns out that I can use the return value from page.evaluate():
        waitFor(
          function(){ // Run the Jasmine suite on the page }, 
          function(){
            var passed = page.evaluate(function(){ 
              // Inspect the results

              return jasmineEnv.currentRunner().results().passed();
            });
            phantom.exit(passed ? 0 : 1);
          }
        );
Now, if I run the test suite with an and-and echo, I do not see the and-and echo when the suite is failing:
➜  calendar git:(requirejs-jasmine-gem) ✗ phantomjs run-jasmine.js http://localhost:8888 && echo "\n\n    PASSED\!\!\!\!\n\n"
...
'waitFor()' finished in 980ms.

Failed: 1

Calendar
  the initial view
    the page title
      Expected '<h1>Funky Calendar<span> (2011-12) </span></h1>' to have text 'Remember, Remember, the Fifth of Novemeber'.
But, when I fix that spec and re-run the suite, I do see the and-and echo output:
➜  calendar git:(requirejs-jasmine-gem) ✗ phantomjs run-jasmine.js http://localhost:8888 && echo "\n\n    PASSED\!\!\!\!\n\n"
...
'waitFor()' finished in 1138ms.

Succeeded.




    PASSED!!!!

Yay!


Day #239

Sunday, December 18, 2011

Somewhat Pretty Printing with PhantomJS, Jasmine and Backbone.js

‹prev | My Chain | next›

I now have the jasmine specs covering my Backbone.js application running under PhantomJS. Running, but the failure leaves something to be desired. The exit code is a success and the failure message simply parrots the entire suite with no indication of which specs failed.

I know that there are terminal reporters, a la the node-jasmine. They do not seem to be a quick drop-in replacement in my little PhantomJS setup. Besides, I hope that I can hack together a little something that will be good enough™.

I still have my intentionally failing spec in place:


Back in the run.html.erb page runner for the jasmine gem, I make the jasmineEnv variable global so that I might access it from within PhantomJS:
require(['Calendar', 'backbone'], function(Calendar, Backbone){
  window.Cal = Calendar;
  window.Backbone = Backbone;

  window.jasmineEnv = jasmine.getEnv();
  jasmineEnv.updateInterval = 1000;

  var reporter = new jasmine.TrivialReporter();

  jasmineEnv.addReporter(reporter);

  jasmineEnv.specFilter = function(spec) {
    return reporter.specFilter(spec);
  };

  jasmineEnv.execute();
});
Then, in the PhantomJS contributed run-jasmine.js, I log the number of failed specs found in the current spec runner:
  waitFor(
    function(){ /* finished at to display */ }, 
    function(){
      page.evaluate(function(){
        console.log('Failed: ' + jasmineEnv.currentRunner().results().failedCount);
        // ...
      });
  });
(I uncover that chain by fiddling with things in Chrome's Javascript console)

With that logger in place, when I run my intentionally failing specs, I see:
# lots of normal application console.log output ...

[object Arguments]
'waitFor()' finished in 1164ms.
Failed: 1

# The text for all specs in the suite...
Looking more closely at the PhantomJS run-jasmine.js script, I notice that it is quite shallow:
  waitFor(
    function(){ /* finished at to display */ }, 
    function(){
      page.evaluate(function(){
        console.log('Failed: ' + jasmineEnv.currentRunner().results().failedCount);
        list = document.body.querySelectorAll('div.jasmine_reporter > div.suite.failed');
        for (i = 0; i < list.length; ++i) { /* ... */ }
      });
  });
By "shallow", I mean that the script is simply grabbing the top-level failed spec and then logging the inner text of each immediate-child:
           list = document.body.querySelectorAll('div.jasmine_reporter > div.suite.failed');
           for (i = 0; i < list.length; ++i) {
               el = list[i];
               desc = el.querySelectorAll('.description');
               console.log('');
               for (j = 0; j < desc.length; ++j) {
                   console.log(desc[j].innerText);
               }
           }
As can be seen from the screen shot of my failing spec, I am nest things quite a bit. What this translates into is a bunch of nested spec groups, each of which have some failing and some passing specs:


To paraphrase the immortal philospher of movies, I know this... it's recursion! So I create a log_failure recursive function to log the specs properly:
  waitFor(
    function(){ /* finished at to display */ }, 
    function(){
      page.evaluate(function(){
        console.log('Failed: ' + jasmineEnv.currentRunner().results().failedCount);

        function has_message(el) { /* children className == spec failed */ }
        function failed_children(el) { /* all children w/ className == 'suite failed' */ }

        function log_failure(failure_el, indent) {
          if (typeof(indent) == 'undefined') indent = '';

          console.log(indent + failure_el.querySelector('.description').innerText);

          if (has_message(failure_el)) {
            console.log(indent + '  ' + failure_el.querySelector('.messages > .fail').innerText);
          }
          else {
            failed_children(failure_el).forEach(function (failed_child) {
              log_failure(failed_child, indent + '  ');
            });
          }

          log_failure(document.body.querySelectorAll('div.jasmine_reporter > div.suite.failed')[0]);
        }
      })
    });
That is kinda ugly because I had to work with HTML Collections instead of arrays. Still, it seems to work because the output produces:
'waitFor()' finished in 1117ms.
Failed: 1
Calendar
  the initial view
    the page title
      Expected '<h1>Funky Calendar<span> (2011-12) </span></h1>' to have text 'Remember, Remember the Fifth of Novemeber'.
I think I can live with that.



Day #238

Saturday, December 17, 2011

Phantom.js and Backbone.js (and require.js)

‹prev | My Chain | next›

Yesterday I was able to make use of my mad ruby hacker skills to get the jasmine server to run the jasmine specs for my backbone.js and require.js application. Today, I hope to use my lovely specs:


The hope is to get them running under PhantomJS—a headless webkit and Javascript engine. First I need to install the thing. For that, I follow the build instructions for Ubuntu. I already have the QT dependencies installed, so I download the source code and compile:
➜  Downloads  wget http://phantomjs.googlecode.com/files/phantomjs-1.3.0-source.tar.gz
...
2011-12-17 23:00:29 (144 KB/s) - `phantomjs-1.3.0-source.tar.gz' saved [409428/409428]

➜  Downloads  sha1sum phantomjs-1.3.0-source.tar.gz 
76902ad0956cf212cc9bb845f290690f53eca576  phantomjs-1.3.0-source.tar.gz


➜  src  tar zxf ../Downloads/phantomjs-1.3.0-source.tar.gz
➜  src  cd phantomjs-1.3.0

➜  phantomjs-1.3.0  qmake-qt4 && make
cd src/ && /usr/bin/qmake-qt4 /home/cstrom/src/phantomjs-1.3.0/src/phantomjs.pro -o Makefile.phantomjs
cd src/ && make -f Makefile.phantomjs
make[1]: Entering directory `/home/cstrom/src/phantomjs-1.3.0/src'
...
make[1]: Leaving directory `/home/cstrom/src/phantomjs-1.3.0/src'

➜  phantomjs-1.3.0  ls -l bin
total 324
-rwxrwxr-x 1 cstrom cstrom 330534 2011-12-17 23:05 phantomjs
I copy that phantomjs executable into my bin directory (which is in my $PATH):
➜  phantomjs-1.3.0  cp bin/phantomjs ~/bin/
I am not quite ready just yet. In the same source code, PhantomJS includes a script for running jasmine tests. That sounds like exactly what I want, I copy it into my applications source directory:
➜  phantomjs-1.3.0  cp examples/run-jasmine.js ~/repos/calendar
With that, I am ready to give PhantomJS and the jasmine server a try. First, I spin up the jasmine server:
➜  calendar git:(master) ✗ rake jasmine
your tests are here:
  http://localhost:8888/
In a separate terminal, I then point PhantomJS at the run-jasmine.js script and my running gem server:
➜  calendar git:(requirejs-jasmine-gem) ✗ phantomjs run-jasmine.js http://localhost:8888
[setDefault]
[setMonth] %s
Error: Backbone.history has already been started
[setMonth] %s
...
[setDefault]
[setMonth] %s
editClick
Error: Backbone.history has already been started
[setMonth] %s
[fetch] Dang
[object Arguments]
'waitFor()' finished in 1253ms.
Hrm... I guess that worked OK. The exit status was cool:
➜  calendar git:(requirejs-jasmine-gem) ✗ echo $?                                       
0
All of the output was just my various calls to console.log(). Really, I should remove them because they will break older browsers like IE. It is interesting that PhantomJS does not seem to honor the sprintf format of console.log:
define(function(require) {
  var Backbone = require('backbone')
    , to_iso8601 = require('Calendar/Helpers.to_iso8601');

  return Backbone.Router.extend({
    // ....
    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
});
The "[fetch] Dang" error seems legit. It is not causing anything to fail, but I seem to have missed a sinon.js stub somewhere in my tests.

All of the Backbone.history messages are a minor headache incurred when testing Backbone.js applications with jasmine.

So it all seems to be working. To be sure, I intentionally break a test:
    describe("the page title", function() {
      it("contains the current month", function() {
        expect($('h1')).toHaveText("Remember, Remember the Fifth of Novemeber");
      });
    });
In the browser, this failure looks like:


In PhantomJS, it looks like:
➜  calendar git:(requirejs-jasmine-gem) ✗ phantomjs run-jasmine.js http://localhost:8888
...
'waitFor()' finished in 1044ms.


Calendar
routing
defaults to the current month
sets the date of the appointment collection
the initial view
the page title
contains the current month
a collection of appointments
populates the calendar with appointments
adding an appointment
sends clicks on day to an add dialog
displays the date clicked in the add dialog
adds a new appointment to the UI when saved
deleting an appointment
clicking the "X" removes records from the data store and UI
updating an appointment
binds click events on the appointment to an edit dialog
displays model updates
can edit appointments through an edit dialog
navigated view
I guess that failed. The output was different. Even so that exist code remained the same:
➜  calendar git:(requirejs-jasmine-gem) ✗ echo $?
0
Hrm... that output is nothing more than the entire text of the test suite. Hopefully fixing that is just a simple matter of futzing with the run-jasmine.js script. I think I will leave that for tomorrow.

For now, I am happy with the progress I made tonight. I can run my Jasmine test suite under the Jasmine server (standalone still works as well) and under PhantomJS. The output from PhantomJS leaves a little to be desired, but it is still workable in a continuous integration environment. Hopefully, by tomorrow, I can make it even better.


Day #237