Monday, January 14, 2008

Testing JavaScript

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" +
      "", "<"+getName ()+">", 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!

No comments: