Design Patterns for JavaScript

18. April, 2011

Here is a good collection of design patterns for JavaScript: Essential JavaScript Design Patterns For Beginners


Firefox performance

24. November, 2010
Mozilla Firefox Icon

Image via Wikipedia

It’s 2010, Firefox is going 4.0 soon and it still has no way to tell why it’s sluggish.

Yes, it would be nice if the FF developers could make the browser so fast that it would never become sluggish in the first place. I just fear that this is simply not possible with the current design. Look at Chrome: Every tab runs in it’s own process. So what if one of them is slow? All the other still feel crisp.

On FF, if one tab takes over the machine, you’re doomed. Yes, I could disable JavaScript and every add-on individually. Why bother? Starting Chrome solves the issue in no time.

If you care, support Bug 505104: Please add a tool to gauge Addon performance.


Using Java in BIRT reports

12. November, 2010
BIRT Project

Image via Wikipedia

If you need to add complex operations to BIRT reports, you have several options. One that is often overlooked is to write the operation in Java and then use the Java code in the report. This is more simple than you’d think.

Instead of creating a “Report” project, create a Java project for your reports. Now you can put the Java code in the same project or a different project and add that second project to the list of dependencies of your report project. Note that this only works if the report project is of type “Java”.

When you edit your code, you just need to save and run your report (using the various ways to run the preview).

You can even debug the code. There is just one thing you need to be aware of: In the debug configuration, you can specify if you want to debug Java, JavaScript of both. JavaScript is the default. In this mode, Java breakpoints have no effect.


Error creating array in JavaScript

6. October, 2010

If you see this error in JavaScript (for example in a BIRT report):

org.mozilla.javascript.EvaluatorException: error instantiating (): class java.sql.Array is interface or abstract

then don’t include the package java.sql (as in importPackage(Packages.java.sql);) because if you do, new Array() will find the SQL class first and it will fail.


env.js has a new website

17. May, 2010

env.js has a new website at http://www.envjs.com/.

Just in case you don’t know why you need it:

Envjs is a simulated browser environment written in javascript.

If you still don’t know: You can use env.js to simulate a web browser in unit tests (among other things).


env.js is Back

11. May, 2009

After quite some time of inactivity, env.js is back. There is a Google group and a git repository.

In case you’re wondering what env.js is: It emulates a web browser in pure JavaScript. What on earth could that be good for? This allows you to run your web application in a unit test. You can write your JavaScript, load env.js, your own code and then run it. You’ll have access to document, events, the DOM, everything. No browser bugs, yet, but that will probably come, too. With this gem, you can finally run your web app in a single process, with every bit of information readily available to your IDE’s debugger. No more messing with a remote or local web server, deploying your application and hoping that Tomcat could reload all classes, no more external browser process and guessing what might cause the odd behavior.


Testing the Impossible: JavaScript in a Web Page

18. November, 2008

How do you run JUnit tests on JavaScript in a web page? Impossible?

Here is what you need: First, get a copy of Rhino (at least 1.6R7). Then, save a copy of the JavaScript code at the bottom as “env.js“. And here is the setup code for the JUnit test:

    Context cx;
    Global scope;

    public void setupContext () throws IllegalAccessException,
            InstantiationException, InvocationTargetException
    {
        cx = Context.enter();
        scope = new Global();
        scope.init (cx);

        addScript(cx, scope, new File ("html/env.js"));

        File f = new File ("html/demo.html");
        cx.evaluateString(scope, 
                "window.location = '"+f.toURL()+"';\n"
                + "", "setupContext", 1, null);
    }

    public void addScript (Context cx, Scriptable scope, File file) throws IOException
    {
        Reader in = new FileReader (file);
        cx.evaluateReader(scope, in, file.getAbsolutePath(), 1, null);
    }

This will load “demo.html” into the browser simulation. The problem here: The loading is asynchronous (just like in a real browser). Now what? We need synchronization:

import org.mozilla.javascript.ScriptableObject;

public class JSJSynchronize extends ScriptableObject
{
    public Object data;
    public Object lock = new Object ();
    
    public JSJSynchronize()
    {
    }
    
    @Override
    public String getClassName ()
    {
        return "JSJSynchronize";
    }
    
    public Object jsGet_data()
    {
        synchronized (lock)
        {
            try
            {
                lock.wait ();
            }
            catch (InterruptedException e)
            {
                throw new RuntimeException ("Should not happen", e);
            }
            
            return data;
        }
    }

    public void jsSet_data(Object data)
    {
        synchronized (lock)
        {
            this.data = data;
            lock.notify ();
        }
    }
    
    public Object getData()
    {
        synchronized (lock)
        {
            try
            {
                lock.wait ();
            }
            catch (InterruptedException e)
            {
                throw new RuntimeException ("Should not happen", e);
            }
            
            return data;
        }
    }

    public void setData(Object data)
    {
        synchronized (lock)
        {
            this.data = data;
            lock.notify ();
        }
    }
    
}

With this code and “window.onload”, we can wait for the html to load:

        JSJSynchronize jsjSynchronize;
        ScriptableObject.defineClass(scope, JSJSynchronize.class);
        
        jsjSynchronize = (JSJSynchronize)cx.newObject (scope, "JSJSynchronize");
        scope.put("jsjSynchronize", scope, jsjSynchronize);

        cx.evaluateString(scope, 
                "window.location = '"+f.toURL()+"';\n" +
                "window.onload = function(){\n" +
                "    print('Window loaded');\n" +
                "    jsjSynchronize.data = window;\n" +
                "};\n" +
                "", "", 1, null);

        ScriptableObject window = (ScriptableObject)jsjSynchronize.getData();
        System.out.println ("window="+window);
        ScriptableObject document = (ScriptableObject)scope.get ("document", scope);
        System.out.println ("document="+document);
        System.out.println ("document.forms="+document.get ("forms", document));
        ScriptableObject navigator = (ScriptableObject)scope.get ("navigator", scope);
        System.out.println ("navigator="+navigator);
        System.out.println ("navigator.location="+navigator.get ("location", navigator));

        // I've been too lazy to parse the HTML for the scripts:
        addScript(cx, scope, new File ("src/main/webapp/script/prototype.js"));

Slightly modified version of env.js, original by John Resig (original code):

/*
 * Simulated browser environment for Rhino
 *   By John Resig <http://ejohn.org/>
 * Copyright 2007 John Resig, under the MIT License
 * http://jqueryjs.googlecode.com/svn/trunk/jquery/build/runtest/
 * Revision 5251
 */

// The window Object
var window = this;

// generic enumeration
Function.prototype.forEach = function(object, block, context) {
 for (var key in object) {
  if (typeof this.prototype[key] == "undefined") {
   block.call(context, object[key], key, object);
  }
 }
};

// globally resolve forEach enumeration
var forEach = function(object, block, context) {
 if (object) {
  var resolve = Object; // default
  if (object instanceof Function) {
   // functions have a "length" property
   resolve = Function;
  } else if (object.forEach instanceof Function) {
   // the object implements a custom forEach method so use that
   object.forEach(block, context);
   return;
  } else if (typeof object.length == "number") {
   // the object is array-like
   resolve = Array;
  }
  resolve.forEach(object, block, context);
 }
};

function collectForms(document) {
 var result = document.body.getElementsByTagName('form');
 //print('collectForms');
 document.forms = result;
  
 for (var i=0; i<result.length; i++) {
     var f = result[i];
     f.name = f.attributes['name'];
     //print('Form '+f.name);
     document[f.name] = f;
     f.elements = f.getElementsByTagName('input');
     
     for(var j=0; j<f.elements.length; j++) {
         var e = f.elements[j];
         var attr = e.attributes;
         
         //forEach(attr, print);
         e.type = attr['type'];
         e.name = attr['name'];
         e.className = attr['class'];
         
         f[e.name] = e;
  //print('    Input '+e.name);
     }
 }
}

(function(){

 // Browser Navigator

 window.navigator = {
  get userAgent(){
   return "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3";
  },
  get appVersion(){
   return "Mozilla/5.0";
  }
 };
 
 var curLocation = (new java.io.File("./")).toURL();
 
 window.__defineSetter__("location", function(url){
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.onreadystatechange = function(){
   curLocation = new java.net.URL( curLocation, url );
   window.document = xhr.responseXML;
   collectForms(window.document);

   var event = document.createEvent();
   event.initEvent("load");
   window.dispatchEvent( event );
  };
  xhr.send();
 });
 
 window.__defineGetter__("location", function(url){
  return {
   get protocol(){
    return curLocation.getProtocol() + ":";
   },
   get href(){
    return curLocation.toString();
   },
   toString: function(){
    return this.href;
   }
  };
 });
 
 // Timers

 var timers = [];
 
 window.setTimeout = function(fn, time){
  var num;
  return num = setInterval(function(){
   fn();
   clearInterval(num);
  }, time);
 };
 
 window.setInterval = function(fn, time){
  var num = timers.length;
  
  timers[num] = new java.lang.Thread(new java.lang.Runnable({
   run: function(){
    while (true){
     java.lang.Thread.currentThread().sleep(time);
     fn();
    }
   }
  }));
  
  timers[num].start();
 
  return num;
 };
 
 window.clearInterval = function(num){
  if ( timers[num] ) {
   timers[num].stop();
   delete timers[num];
  }
 };
 
 // Window Events
 
 var events = [{}];

 window.addEventListener = function(type, fn){
  if ( !this.uuid || this == window ) {
   this.uuid = events.length;
   events[this.uuid] = {};
  }
    
  if ( !events[this.uuid][type] )
   events[this.uuid][type] = [];
  
  if ( events[this.uuid][type].indexOf( fn ) < 0 )
   events[this.uuid][type].push( fn );
 };
 
 window.removeEventListener = function(type, fn){
    if ( !this.uuid || this == window ) {
        this.uuid = events.length;
        events[this.uuid] = {};
    }
    
    if ( !events[this.uuid][type] )
   events[this.uuid][type] = [];
   
  events[this.uuid][type] =
   events[this.uuid][type].filter(function(f){
    return f != fn;
   });
 };
 
 window.dispatchEvent = function(event){
  if ( event.type ) {
   if ( this.uuid && events[this.uuid][event.type] ) {
    var self = this;
   
    events[this.uuid][event.type].forEach(function(fn){
     fn.call( self, event );
    });
   }
   
   if ( this["on" + event.type] )
    this["on" + event.type].call( self, event );
  }
 };
 
 // DOM Document
 
 window.DOMDocument = function(file){
  this._file = file;
  var factory = Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance();
  factory.setValidating(false);
  this._dom = factory.newDocumentBuilder().parse(file);
  
  if ( !obj_nodes.containsKey( this._dom ) )
   obj_nodes.put( this._dom, this );
 };
 
 DOMDocument.prototype = {
  createTextNode: function(text){
   return makeNode( this._dom.createTextNode(
    text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")) );
  },
  createElement: function(name){
   return makeNode( this._dom.createElement(name.toLowerCase()) );
  },
  getElementsByTagName: function(name){
   return new DOMNodeList( this._dom.getElementsByTagName(
    name.toLowerCase()) );
  },
  getElementById: function(id){
   var elems = this._dom.getElementsByTagName("*");
   
   for ( var i = 0; i < elems.length; i++ ) {
    var elem = elems.item(i);
    if ( elem.getAttribute("id") == id )
     return makeNode(elem);
   }
   
   return null;
  },
  get body(){
   return this.getElementsByTagName("body")[0];
  },
  get documentElement(){
   return makeNode( this._dom.getDocumentElement() );
  },
  get ownerDocument(){
   return null;
  },
  addEventListener: window.addEventListener,
  removeEventListener: window.removeEventListener,
  dispatchEvent: window.dispatchEvent,
  get nodeName() {
   return "#document";
  },
  importNode: function(node, deep){
   return makeNode( this._dom.importNode(node._dom, deep) );
  },
  toString: function(){
   return "Document" + (typeof this._file == "string" ?
    ": " + this._file : "");
  },
  get innerHTML(){
   return this.documentElement.outerHTML;
  },
  
  get defaultView(){
   return {
    getComputedStyle: function(elem){
     return {
      getPropertyValue: function(prop){
       prop = prop.replace(/-(w)/g,function(m,c){
        return c.toUpperCase();
       });
       var val = elem.style[prop];
       
       if ( prop == "opacity" && val == "" )
        val = "1";
        
       return val;
      }
     };
    }
   };
  },
  
  createEvent: function(){
   return {
    type: "",
    initEvent: function(type){
     this.type = type;
    }
   };
  }
 };
 
 function getDocument(node){
  return obj_nodes.get(node);
 }
 
 // DOM NodeList
 
 window.DOMNodeList = function(list){
  this._dom = list;
  this.length = list.getLength();
  
  for ( var i = 0; i < this.length; i++ ) {
   var node = list.item(i);
   this[i] = makeNode( node );
  }
 };
 
 DOMNodeList.prototype = {
  toString: function(){
   return "[ " +
    Array.prototype.join.call( this, ", " ) + " ]";
  },
  get outerHTML(){
   return Array.prototype.map.call(
    this, function(node){return node.outerHTML;}).join('');
  }
 };
 
 // DOM Node
 
 window.DOMNode = function(node){
  this._dom = node;
 };
 
 DOMNode.prototype = {
  get nodeType(){
   return this._dom.getNodeType();
  },
  get nodeValue(){
   return this._dom.getNodeValue();
  },
  get nodeName() {
   return this._dom.getNodeName();
  },
  cloneNode: function(deep){
   return makeNode( this._dom.cloneNode(deep) );
  },
  get ownerDocument(){
   return getDocument( this._dom.ownerDocument );
  },
  get documentElement(){
   return makeNode( this._dom.documentElement );
  },
  get parentNode() {
   return makeNode( this._dom.getParentNode() );
  },
  get nextSibling() {
   return makeNode( this._dom.getNextSibling() );
  },
  get previousSibling() {
   return makeNode( this._dom.getPreviousSibling() );
  },
  toString: function(){
   return '"' + this.nodeValue + '"';
  },
  get outerHTML(){
   return this.nodeValue;
  }
 };

 // DOM Element

 window.DOMElement = function(elem){
  this._dom = elem;
  this.style = {
   get opacity(){ return this._opacity; },
   set opacity(val){ this._opacity = val + ""; }
  };
  
  // Load CSS info
  var styles = (this.getAttribute("style") || "").split(/s*;s*/);
  
  for ( var i = 0; i < styles.length; i++ ) {
   var style = styles[i].split(/s*:s*/);
   if ( style.length == 2 )
    this.style[ style[0] ] = style[1];
  }
 };
 
 DOMElement.prototype = extend( new DOMNode(), {
  get nodeName(){
   return this.tagName.toUpperCase();
  },
  get tagName(){
   return this._dom.getTagName();
  },
  toString: function(){
   return "<" + this.tagName + (this.id ? "#" + this.id : "" ) + ">";
  },
  get outerHTML(){
   var ret = "<" + this.tagName, attr = this.attributes;
   
   for ( var i in attr )
    ret += " " + i + "='" + attr[i] + "'";
    
   if ( this.childNodes.length || this.nodeName == "SCRIPT" )
    ret += ">" + this.childNodes.outerHTML + 
     "</" + this.tagName + ">";
   else
    ret += "/>";
   
   return ret;
  },
  
  get attributes(){
   var attr = {}, attrs = this._dom.getAttributes();
   
   for ( var i = 0; i < attrs.getLength(); i++ )
    attr[ attrs.item(i).nodeName ] = attrs.item(i).nodeValue;
    
   return attr;
  },
  
  get innerHTML(){
   return this.childNodes.outerHTML; 
  },
  set innerHTML(html){
   html = html.replace(/</?([A-Z]+)/g, function(m){
    return m.toLowerCase();
   });
   
   var nodes = this.ownerDocument.importNode(
    new DOMDocument( new java.io.ByteArrayInputStream(
     (new java.lang.String("<wrap>" + html + "</wrap>"))
      .getBytes("UTF8"))).documentElement, true).childNodes;
    
   while (this.firstChild)
    this.removeChild( this.firstChild );
   
   for ( var i = 0; i < nodes.length; i++ )
    this.appendChild( nodes[i] );
  },
  
  get textContent(){
   return nav(this.childNodes);
   
   function nav(nodes){
    var str = "";
    for ( var i = 0; i < nodes.length; i++ )
     if ( nodes[i].nodeType == 3 )
      str += nodes[i].nodeValue;
     else if ( nodes[i].nodeType == 1 )
      str += nav(nodes[i].childNodes);
    return str;
   }
  },
  set textContent(text){
   while (this.firstChild)
    this.removeChild( this.firstChild );
   this.appendChild( this.ownerDocument.createTextNode(text));
  },
  
  style: {},
  clientHeight: 0,
  clientWidth: 0,
  offsetHeight: 0,
  offsetWidth: 0,
  
  get disabled() {
   var val = this.getAttribute("disabled");
   return val != "false" && !!val;
  },
  set disabled(val) { return this.setAttribute("disabled",val); },
  
  get checked() {
   var val = this.getAttribute("checked");
   return val != "false" && !!val;
  },
  set checked(val) { return this.setAttribute("checked",val); },
  
  get selected() {
   if ( !this._selectDone ) {
    this._selectDone = true;
    
    if ( this.nodeName == "OPTION" && !this.parentNode.getAttribute("multiple") ) {
     var opt = this.parentNode.getElementsByTagName("option");
     
     if ( this == opt[0] ) {
      var select = true;
      
      for ( var i = 1; i < opt.length; i++ )
       if ( opt[i].selected ) {
        select = false;
        break;
       }
       
      if ( select )
       this.selected = true;
     }
    }
   }
   
   var val = this.getAttribute("selected");
   return val != "false" && !!val;
  },
  set selected(val) { return this.setAttribute("selected",val); },

  get className() { return this.getAttribute("class") || ""; },
  set className(val) {
   if (typeof val != 'string') { val = "" + val; }
   return this.setAttribute("class",
    val.replace(/(^s*|s*$)/g,""));
  },
  
  get type() { return this.getAttribute("type") || ""; },
  set type(val) { return this.setAttribute("type",val); },
  
  get value() { return this.getAttribute("value") || ""; },
  set value(val) { return this.setAttribute("value",val); },
  
  get src() { return this.getAttribute("src") || ""; },
  set src(val) { return this.setAttribute("src",val); },
  
  get id() { return this.getAttribute("id") || ""; },
  set id(val) { return this.setAttribute("id",val); },
  
  getAttribute: function(name){
   return this._dom.hasAttribute(name) ?
    new String( this._dom.getAttribute(name) ) :
    null;
  },
  setAttribute: function(name,value){
   this._dom.setAttribute(name,value);
  },
  removeAttribute: function(name){
   this._dom.removeAttribute(name);
  },
  
  get childNodes(){
   return new DOMNodeList( this._dom.getChildNodes() );
  },
  get firstChild(){
   return makeNode( this._dom.getFirstChild() );
  },
  get lastChild(){
   return makeNode( this._dom.getLastChild() );
  },
  appendChild: function(node){
   this._dom.appendChild( node._dom );
  },
  insertBefore: function(node,before){
   this._dom.insertBefore( node._dom, before ? before._dom : before );
  },
  removeChild: function(node){
   this._dom.removeChild( node._dom );
  },

  getElementsByTagName: DOMDocument.prototype.getElementsByTagName,
  
  addEventListener: window.addEventListener,
  removeEventListener: window.removeEventListener,
  dispatchEvent: window.dispatchEvent,
  
  click: function(){
   var event = document.createEvent();
   event.initEvent("click");
   this.dispatchEvent(event);
  },
  submit: function(){
   var event = document.createEvent();
   event.initEvent("submit");
   this.dispatchEvent(event);
  },
  focus: function(){
   var event = document.createEvent();
   event.initEvent("focus");
   this.dispatchEvent(event);
  },
  blur: function(){
   var event = document.createEvent();
   event.initEvent("blur");
   this.dispatchEvent(event);
  },
  get elements(){
   return this.getElementsByTagName("*");
  },
  get contentWindow(){
   return this.nodeName == "IFRAME" ? {
    document: this.contentDocument
   } : null;
  },
  get contentDocument(){
   if ( this.nodeName == "IFRAME" ) {
    if ( !this._doc )
     this._doc = new DOMDocument(
      new java.io.ByteArrayInputStream((new java.lang.String(
      "<html><head><title></title></head><body></body></html>"))
      .getBytes("UTF8")));
    return this._doc;
   } else
    return null;
  }
 });
 
 // Helper method for extending one object with another
 
 function extend(a,b) {
  for ( var i in b ) {
   var g = b.__lookupGetter__(i), s = b.__lookupSetter__(i);
   
   if ( g || s ) {
    if ( g )
     a.__defineGetter__(i, g);
    if ( s )
     a.__defineSetter__(i, s);
   } else
    a[i] = b[i];
  }
  return a;
 }
 
 // Helper method for generating the right
 // DOM objects based upon the type
 
 var obj_nodes = new java.util.HashMap();
 
 function makeNode(node){
  if ( node ) {
   if ( !obj_nodes.containsKey( node ) )
    obj_nodes.put( node, node.getNodeType() == 
     Packages.org.w3c.dom.Node.ELEMENT_NODE ?
      new DOMElement( node ) : new DOMNode( node ) );
   
   return obj_nodes.get(node);
  } else
   return null;
 }
 
 // XMLHttpRequest
 // Originally implemented by Yehuda Katz

 window.XMLHttpRequest = function(){
  this.headers = {};
  this.responseHeaders = {};
 };
 
 XMLHttpRequest.prototype = {
  open: function(method, url, async, user, password){ 
   this.readyState = 1;
   if (async)
    this.async = true;
   this.method = method || "GET";
   this.url = url;
   this.onreadystatechange();
  },
  setRequestHeader: function(header, value){
   this.headers[header] = value;
  },
  getResponseHeader: function(header){ },
  send: function(data){
   var self = this;
   
   function makeRequest(){
    var url = new java.net.URL(curLocation, self.url);
    
    if ( url.getProtocol() == "file" ) {
     if ( self.method == "PUT" ) {
      var out = new java.io.FileWriter( 
        new java.io.File( new java.net.URI( url.toString() ) ) ),
       text = new java.lang.String( data || "" );
      
      out.write( text, 0, text.length() );
      out.flush();
      out.close();
     } else if ( self.method == "DELETE" ) {
      var file = new java.io.File( new java.net.URI( url.toString() ) );
      file["delete"]();
     } else {
      var connection = url.openConnection();
      connection.connect();
      handleResponse();
     }
    } else { 
     var connection = url.openConnection();
     
     connection.setRequestMethod( self.method );
     
     // Add headers to Java connection
     for (var header in self.headers)
      connection.addRequestProperty(header, self.headers[header]);
    
     connection.connect();
     
     // Stick the response headers into responseHeaders
     for (var i = 0; ; i++) { 
      var headerName = connection.getHeaderFieldKey(i); 
      var headerValue = connection.getHeaderField(i); 
      if (!headerName && !headerValue) break; 
      if (headerName)
       self.responseHeaders[headerName] = headerValue;
     }
     
     handleResponse();
    }
    
    function handleResponse(){
     self.readyState = 4;
     self.status = parseInt(connection.responseCode) || undefined;
     self.statusText = connection.responseMessage || "";
     
     var stream = new java.io.InputStreamReader(connection.getInputStream()),
      buffer = new java.io.BufferedReader(stream), line;
     
     while ((line = buffer.readLine()) != null)
      self.responseText += line;
      
     self.responseXML = null;
     
     if ( self.responseText.match(/^s*</) ) {
      //try {
       self.responseXML = new DOMDocument(
        new java.io.ByteArrayInputStream(
         (new java.lang.String(
          self.responseText)).getBytes("UTF8")));
      //} catch(e) {
      //}
     }
    }
    
    self.onreadystatechange();
   }

   if (this.async)
    (new java.lang.Thread(new java.lang.Runnable({
     run: makeRequest
    }))).start();
   else
    makeRequest();
  },
  abort: function(){},
  onreadystatechange: function(){},
  getResponseHeader: function(header){
   if (this.readyState < 3)
    throw new Error("INVALID_STATE_ERR");
   else {
    var returnedHeaders = [];
    for (var rHeader in this.responseHeaders) {
     if (rHeader.match(new Regexp(header, "i")))
      returnedHeaders.push(this.responseHeaders[rHeader]);
    }
   
    if (returnedHeaders.length)
     return returnedHeaders.join(", ");
   }
   
   return null;
  },
  getAllResponseHeaders: function(header){
   if (this.readyState < 3)
    throw new Error("INVALID_STATE_ERR");
   else {
    var returnedHeaders = [];
    
    for (var header in this.responseHeaders)
     returnedHeaders.push( header + ": " + this.responseHeaders[header] );
    
    return returnedHeaders.join("rn");
   }
  },
  async: true,
  readyState: 0,
  responseText: "",
  status: 0
 };
})();

Testing JavaScript

14. January, 2008

If you’re test mad like me, then the <script> tag in HTML was probably one sore spot for you as it was for me: How to test the damn thing? Well, now, there is a way: John Resig wrote a small script which you can source into Rhino 1.6R6 (or later; 1.6R5 won’t work, though. You’ll get “missing : after property id”). Afterwards, you’ll have window, document, nagivation, even XMLHttpRequest!

Yes, you can actually test AJAX within unit tests, now! TDD fans, start your engines!

Unfortunately, it doesn’t emulate browser bugs, yet ;-> But you can fix that. I, for example, had problems to get the code load HTML 3.2 files. Especially this code made the SAX parser vomit:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 //EN">

The fix here is to download some XHTML DTD (like XHTML 1 Strict), put it somewhere (along with the three entity files xhtml-lat1.ent,
xhtml-special.ent and xhtml-symbol.ent) and change the DTD to point to the new file:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 //EN" "html/xhtml1-strict.dtd">

In my case, I’ve put the files in a subdirectory “html/” of the directory I start the tests from. (Hm … shouldn’t this path be raltive to the HTML file?? Well, it isn’t.)

Also, the env.js supplied doesn’t support forms. Here is my which fix:

function collectForms() {
    document.forms = document.body.getElementsByTagName('FORM');
    
    for (var i=0; i<document.forms.length; i++) {
        var f = document.forms[i];
        f.name = f.attributes['name'];
        //print('Form '+f.name);
        f.elements = f.getElementsByTagName('INPUT');
        
        for(var j=0; j<f.elements.length; j++) {
            var e = f.elements[j];
            var attr = e.attributes;
            
            forEach(attr, print);
            e.type = attr['type'];
            e.name = attr['name'];
            e.className = attr['class'];
            
            _elements[ f.name + '.' + e.name ] = e;
        }
        //print(f.elements);
    }
}

Note: I also had to remove the calls to toLowerCase() for the tag names (*not* the attributes!), too. Otherwise, document.body would return UNDEFINED for me. But that’s because I’m stuck with old HTML; If you can convert all tag and attribute names to lowercase, then you’re safe.

Lastly, the loading of the document is asynchronous. To fix this, we need a class to synchronize the Java and the JavaScript thread. That one is simple:

package test.js;

import org.mozilla.javascript.ScriptableObject;

import test.ShouldNotHappenException;

public class JSJSynchronize extends ScriptableObject
{
    public Object data;
    public Object lock = new Object ();
    
    @Override
    public String getClassName () {
        return "JSJSynchronize";
    }
    
    public Object jsGet_data() {
        synchronized (lock) {
            try {
                lock.wait ();
            }
            catch (InterruptedException e) {
                throw new ShouldNotHappenException(e);
            }
            
            return data;
        }
    }

    public void jsSet_data(Object data) {
        synchronized (lock) {
            this.data = data;
            lock.notify ();
        }
    }
    
    public Object getData() {
        synchronized (lock) {
            try {
                lock.wait ();
            }
            catch (InterruptedException e) {
                throw new ShouldNotHappenException(e);
            }
            
            return data;
        }
    }

    public void setData(Object data) {
        synchronized (lock) {
            this.data = data;
            lock.notify ();
        }
    }
    
}

ShouldNotHappenException is derived from RuntimeException. After registering that with

        jsjSynchronize = (JSJSynchronize)cx.newObject (scope, "JSJSynchronize");
        scope.put("jsjSynchronize", scope, jsjSynchronize);

in the test, I can use the new jsjSynchronize global variable in JavaScript in wondow.onload:

    public void testAddTextFilters() throws Exception
    {
        setupContext ();
        addScript(cx, scope, new File ("html/env.js"));
        cx.evaluateString(scope, 
                "window.location = 'file:///d:/devm2/globus/abs/webapp/html/testAbsSkuOutput.html';n" +
      "window.onload = function(){n" +
      "    jsjSynchronize.data = window;n" +
      "};n" +
      "", "", 1, null);
        
        ScriptableObject window = (ScriptableObject)jsjSynchronize.getData();
        System.out.println ("window="+window);

At this point, the document has been loaded and you can access all the fields, elements, etc. Good luck!