Wednesday 23 July 2008

Global Eval in Rhino

I've been working to complete John Resig's env.js simulated browser environment in Rhino so that it will run unit tests from the command line in exactly the same way as the browser. The end goal was to get behaviour in env.js to a point where there are practically no differences with a real browser, so that enterprises can incorporate running javascript unit tests into their CI environment.

Using the jquery QUnit test suite as my benchmark, I've been trying make the tests pass. The going has been tough, with countless nuances to fix up, but I managed to pass the 1.1.4 core tests, and the 1.2.6 ajax and event tests. However, now stumped on the 1.2.6 core tests and realising that I won't ever have enough spare time to implement full CSS support, I'm going to blog about other areas which I've been successful with.

One change that made a big difference was the additional of a global eval. This means adding script elements will evaluate them in the global scope, and ajax loading of dynamic scripts with ajax now works as expected.

The trick is to use rhino's load() function, which loads javascript from a file system or URL location and evaluates it in the global scope. To load arbitrary snippets of javascript, the text is written to a temporary file on the filesystem, load() is called, then the file is deleted. Luckily, env.js has already implemented writing and deleting files!



.....

  // run node through execScripts whenever added to the dom
  appendChild: function(node){
    this._dom.appendChild( node._dom );
    execScripts(node);
  },
  insertBefore: function(node,before){
    this._dom.insertBefore( node._dom, before ? before._dom : before );
    execScripts(node);
  },

.....  

  function execScripts(node) {
    if ( node.nodeName == "SCRIPT" ) {
      if ( !node.getAttribute("src") ) {
        globalEval ( node.textContent );     
      } else {
        var src = node.getAttribute("src");
        load(src); // you'll actually have to resolve relative URLs here   
      }
      if (node.onload && typeof node.onload == "function") {
        node.onload();
      }     
    } else if (node.nodeType==1) {  
      var scripts = node.getElementsByTagName("script");
      for ( var i = 0; i < scripts.length; i++ ) {
        execScripts( scripts[i] );
      }
    }
  }
  var globalEvalCounter = (new Date()).getTime();  // temp file name
  function globalEval(data) {
    try {                        // write to java temp directory
      var javatmpdir = java.lang.System.getProperty("java.io.tmpdir")+"";
      var folder = "file:///" + javatmpdir.replace(/\\/g, "/");
      var tempfile = folder + (globalEvalCounter++);
      var xhrPut = new XMLHttpRequest();
      xhrPut.open("PUT", tempfile, false);
      xhrPut.send(data);
      load(tempfile);
      xhrPut = null;
      var xhrDel = new XMLHttpRequest();
      xhrDel.open("DELETE", tempfile, false);
      xhrDel.send();
      xhrDel = null;
    } catch(ex) {
      throw new Error("Error occurred");
    }
  }
 

This mechanism allows you to mimic browser behaviour much more closely, and load the scripts defined in the HTML file (like a real browser) rather than a separate js file to load the unit tests. Doing that also allows you to fire the document ready event at the right time. More on this in my next post.