Saturday, February 9, 2013

QUnit Inside Firefox Extensions

I have a fairly large amount of code inside one of my Firefox extensions (Shopping Helper) .  I also have always manually tested this code, which made changes hard to do. I recently did a major refactoring of the code, and have plans to add more features, so it was time to get serious about automating the testing, with the first step of adding a comprehensive unit test suite.  Now, just about every language has a useful framework,  usually called something like "FooUnit" (e.g., JUnit, PyUnit), so that's the sort of thing I wanted for my extension.

Here's the problem: Firefox extensions are JavaScript, but it runs in a very different context that the normal JavaScript inside an HTML page situation.  This is typically called "chrome code", to differentiate it from the code that runs in the HTML document itself.  This alternate run-time environment has always made debugging and development tricky for Firefox extensions.  It gets further complicated by security issues where the chrome code is trusted more than code inserted into HTML, so there is a "wall" between the two worlds.

Looking for JavaScript unit test frameworks, QUnit seemed to be exactly what I was looking for: same basic framework as the FooUnits, relatively mature and well regarded.  It is nicely done, but it is also not done for the purposes of being used as chrome code inside a Firefox extension.  The question is whether it would run as-is inside chrome code (very doubtful) or could be made to run inside chrome code (more hopeful) without too much effort (a bit worrisome).

Short answer is that QUnit does not run as-is in the chrome context, but can be made to do so with a moderate amount of work (it took me about 5 hours). Since I have now done this work, no one else needs to do this, so for you, it will be relatively easy with all the details I give below.

First off, here are the files you need, which are based on version 1.11.0 of QUnit.
Below are the details of the changes I had to make and how to go about using this inside Firefox chrome code.


QUnit Javascript Changes


Chrome vs. Non-chrome Control

To not have to fork the QUnit code for the modifications, I defined a global variable QU_ISCHROME whose setting can be changed to easily switch to being used inside a Firefox extension in a chrome window instead of a browser window.  This will default to 'false' to give the previously existing behavior and can be changed to 'true' when including in the XUL/chrome Firefox extension context. It should be a simple one value change to get the effect, though I did not check if it ran outside a XUL window.


Altering createElement() calls

Since QUnit creates HTML DOM nodes, and since it will live inside an XML/XUL window, when creating these nodes, we have to explicitly define the namespace when creating them because the XUL namespace is not the standard HTML tags.  We do this by replacing all calls like this:

  document.createElement( "tag" );

with something like this:

  document.createElementNS( QU_HTMLNS, "html:tag" );

where 'QU_HTMLNS' is a conveniently defined global variable we put at the top of the Javascript file which is defined as:

  var QU_HTMLNS = "";

We add a convenience function QU_createElement() and wrap all node creation in it to keep the code cleaner.


 Altering innerHTML Content Assignments

While doing an assignment of an HTML fragment to a node via the innerHTML property works fine in a browser window, this does not work well in a chrome window. Thus, we wrap this assignment in a helper function QU_setInnerHTML() so that when QU_ISCHROME is true, it uses a proper parser directly.


Altering innerHtml Content Fetching

Similar to the assignment problem of innerHTML, using it as a value does not work that will in the chrome context either, so we also wrap these in a helper function QU_getInnerHTML().


Pedantic QUnit Javascript (optional)

In a more pedantic mode, a number of JavaScript warning were generated around functions that did not always return a value and properties that were used but not defined.


Extension Development Environment Notes

I package up my extension with a bash script that conditionally includes testing and debug code based upon a command line parameter.  So all the QUnit code is not in the production build.  This requires a bit of a customized extension environment though one could just manually add and remove the QUnit code if you did not want to have to replicate conditional building. When included by the build script, the code also insert a menu option to allow invoking the unit test window.


XUL Window Notes


CSS Include

Need to have the CSS includes after the XML declaration, but before the window itself.

  <?xml-stylesheet href="chrome://myextension/skin/qunit-1.11.0.css"


Namespace include

The window XML object needs the attribute to ensure that HTML elements can be put inside it:



JavaScript include

Within the window XML, include the QUnit JavaScript.  Here I have renamed it to reflect that it is the version with the QU_ISCHROME=true setting:

  <script type="application/x-javascript"


Body Elements

As with the normal QUnit usage, you need the special two "div" elements. The twist here is that the HTML tag names need to be preceded with "html:" to explicitly define their namespace.

   <html:div id="qunit"></html:div>
   <html:div id="qunit-fixture"></html:div>


Full Example XUL Window

<?xml version="1.0"?>

<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://myextension/skin/qunit-1.11.0.css" type="text/css"?>

<window id="MyExtensionUnitTestDialog"
        ondialogcancel="return SHUTD_onDialogCancel();"
         title="My Extension Unit Test Console"

  <script type="application/x-javascript"
  <script type="application/x-javascript"
  <vbox flex="1">
     <html:div id="qunit"></html:div>
     <html:div id="qunit-fixture"></html:div>

XUL (extension) Javascript Notes

To test functions in the main extension within the QUnit XUl windo, we need access to the main window where the extension chrome code runs.  I am not sure if there are better ways to do this, but this works by iterating over windows.

    var parentWindow = null
    var wm = Components.classes[";1"].getService(Components.interfaces.nsIWindowMediator);
    var enumerator = wm.getEnumerator("navigator:browser");
       var win = enumerator.getNext();
       if ( ! win.document )
       parentWindow = win;

You then can invoke functions from the extension with:

  parentWindow.someFunctionName( someArg1, someArg2 );


QUnit CSS Changes (optional)

Inside the chrome windows, the CSS property "-moz-border-radius" does not apply, so it gives a warning.

No comments: