The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
// $Id: /mirror/openjsan/users/theory/Test.Simple/trunk/lib/Test/Harness/Browser.js 2139 2008-08-22T23:44:09.596660Z ingy  $

/*global JSAN, Test, ActiveXObject */

if (typeof JSAN != 'undefined') JSAN.use('Test.Harness');
else {
    if (typeof Test == 'undefined') Test = {};
    if (!Test.Harness) Test.Harness = {};
}

if (window.parent != window &&
    location.href.replace(/[?#].+/, "") == parent.location.href.replace(/[?#].+/, ""))
{

    // Build fake T.H.B so original script from this file doesn't throw
    // exception. This is a bit of a hack...
    Test.Harness.Browser = function() {
        this.runTests = function() {};
        this.encoding = function () { return this };
    };

    // We're in a test iframe. Set up the necessary parts and load the
    // test script with XMLHttpRequest (to support Safari and Opera).
    var __MY = {};
    __MY.pre = document.createElement("pre");
    __MY.pre.id = "test";
    if (window.parent.Test.Harness.Browser._encoding) {
        // Set all scripts to use the appropriate encoding.
        __MY.scripts = document.getElementsByTagName('script');
        for (var j = 0; j < __MY.scripts.length; j++) {
            __MY.scripts[j].charset =
                window.parent.Test.Harness.Browser._encoding;
        }
    }

    // XXX replace with a script element at some point? Safari is due to
    // have this working soon (not sure about IE or Opera):
    // http://bugs.webkit.org/show_bug.cgi?id=3748
    __MY.inc = window.parent.Test.Harness.Browser.includes;
    __MY.req = typeof XMLHttpRequest != 'undefined'
      ? new XMLHttpRequest()
      : new ActiveXObject("Microsoft.XMLHTTP");

    for (var k = 0; k < __MY.inc.length; k++) {
        __MY.req.open("GET", __MY.inc[k], false);
        __MY.req.send(null);
        var stat = __MY.req.status;
        //           OK   Not Modified    IE Cached   Safari cached
        if (stat == 200 || stat == 304 || stat == 0 || stat == null) {
            eval(__MY.req.responseText);
        } else {
            throw new Error(
                "Unable to load " + __MY.inc[k]
                + ': Status ' + __MY.req.status
            );
        }
    }

    // IE 6 SP 2 doesn't seem to run the onload() event, so we force the
    // issue.
    Test.Builder._finish(Test);

    // XXX Opera throws a DOM exception here, but I don't know what to do
    // about that.
    __MY.body = document.body
        || document.getElementsByTagName("body")[0].appendChild(__MY.pre);
    if (__MY.body) __MY.body.appendChild(__MY.pre);
    else if (document.appendChild) document.appendChild(__MY.pre);

} else {
    Test.Harness.Browser = function () {
        this.includes = Test.Harness.Browser.includes = [];
        Array.prototype.push.apply(Test.Harness.Browser.includes, arguments);
        this.includes.push('');
    };

    Test.Harness.Browser.VERSION = '0.28';

    Test.Harness.Browser.runTests = function () {
        var harness = new Test.Harness.Browser();
        harness.runTests.apply(harness, arguments);
    };

    Test.Harness.Browser.prototype = new Test.Harness();
    Test.Harness.Browser.prototype.interval = 100;

    Test.Harness.Browser.prototype._setupFrame = function () {
        // Setup the iFrame to run the tests.
        var node = document.getElementById('buffer');
        if (node) return node.contentWindow || frames.buffer;
        node = document.createElement("iframe");
        node.setAttribute("id", "buffer");
        node.setAttribute("name", "buffer");
        // Safari makes it impossible to do anything with the iframe if it's
        // set to display:none. See:
        // http://www.quirksmode.org/bugreports/archives/2005/02/hidden_iframes.html
        if (/Safari|Konqueror/.test(navigator.userAgent)) {
            node.style.visibility = "hidden";
            node.style.height = "0"; 
            node.style.width = "0";
        } else
            node.style.display = "none";
        document.body.appendChild(node);
        return node.contentWindow || frames.buffer;
    };

    Test.Harness.Browser.prototype._setupOutput = function () {
        // Setup the pre element for test output.
        var node = document.createElement('pre');
        node.setAttribute('id', 'output');
        document.body.appendChild(node);
        fixoutput = function(node) {
            // Trailing space added and replaced to work around yet another
            // Safari bug.
            node.innerHTML = node.innerHTML.replace(
                / ?(\w[\w\.\-]+?\w)(?=\.\.\.)/m,
                (/MSIE/.test(navigator.userAgent) ? '<br>' : '') +
                '<a href="$1">$1</a>'
            ) + ' ';
        };
        return {
            pass: function (msg) {
                node.appendChild(document.createTextNode(msg));
                window.scrollTo(0, document.body.offsetHeight
                                || document.body.scrollHeight);
                fixoutput(node);
            },
            fail: function (msg) {
                var red = document.createElement('span');
                red.setAttribute('style', 'color: red; font-weight: bold');
                node.appendChild(red);
                red.appendChild(document.createTextNode(msg));
                window.scrollTo(0, document.body.offsetHeight
                                || document.body.scrollHeight);
            }
        };
    };

    Test.Harness.Browser.prototype._setupSummary = function () {
        // Setup the div for the summary.
        var node = document.createElement("div");
        node.setAttribute("id", "summary");
        node.setAttribute(
            "style", "white-space:pre; font-family: Verdana,Arial,serif;"
        );
        document.body.appendChild(node);
        return function (msg) {
            node.appendChild(document.createTextNode(msg));
            window.scrollTo(0, document.body.offsetHeight
                            || document.body.scrollHeight);
        };
};

    Test.Harness.Browser.prototype.runTests = function () {
        Test.Harness.Browser._encoding = this.encoding();
        var files = this.args.file
        ? typeof this.args.file == 'string' ? [this.args.file] : this.args.file
        : arguments;
        if (!files.length) return;
        var outfiles = this.outFileNames(files);
        var buffer = this._setupFrame();
        var harness = this;
        var ti = 0;
        var start;
        var output = this._setupOutput();
        var summaryOutput = this._setupSummary();
        // These depend on how we're watching for a test to finish.
        var finish = function () {}, runNext = function () {};

        // This function handles most of the work of outputting results and
        // running the next test, if there is one.
        var runner = function () {
            harness.outputResults(
                buffer.Test.Builder.Test,
                files[ti],
                output,
                harness.args
            );

            if (files[++ti]) {
                output.pass(
                    outfiles[ti]
                    + (harness.args.verbose ? Test.Harness.LF : '')
                );
                harness.runTest(files[ti], buffer);
                runNext();
            } else {
                harness.outputSummary(summaryOutput, new Date() - start);
                finish();
            }
        };

        // XXX Support IE's propertychange event?
        // http://msdn.microsoft.com/workshop/author/dhtml/reference/events/onpropertychange.asp
        if (Object.watch) {
            // We can use the cool watch method, and avoid setting timeouts!
            // We just need to unwatch() when all tests are finished.
            finish = function () { Test.Harness.unwatch('Done') };
            Test.Harness.watch('Done', function (attr, prev, next) {
                if (next < buffer.Test.Builder.Instances.length) return next;
                runner();
                return 0;
            });
        } else {
            // Damn. We have to set timeouts. :-(
            var pkg;
            var wait = function () {
                // Check Test.Harness.Done. If it's non-zero, then we know
                // that the buffer is fully loaded, because it has incremented
                // Test.Harness.Done. Grrr.. IE 6 SP 2 seems to delete
                // buffer.Test after all the tests have finished running, but
                // before this code executes for the correct number of
                // completed tests. So we cache it in a variable outside of
                // the function on previous calls to the function.
                if (!pkg) pkg = buffer.Test;
                if (Test.Harness.Done > 0
                    && Test.Harness.Done >= pkg.Builder.Instances.length)
                {
                    Test.Harness.Done = 0;
                    // Avoid race condition by resetting the instances, too. I
                    // have no idea why this might remain set from a previous
                    // test, but such can be the case in IE 6 SP 2.
                    pkg.Builder.Instances = [];
                    runner();
                } else {
                    window.setTimeout(wait, harness.interval);
                }
            };
            // We'll just have to set a timeout for the next test.
            runNext = function () {
                window.setTimeout(wait, harness.interval);
            };
            window.setTimeout(wait, this.interval);
        }

        // Now start the first test.
        output.pass(outfiles[ti] + (this.args.verbose ? Test.Harness.LF : ''));
        start = new Date();
        this.runTest(files[ti], buffer);
    };

    Test.Harness.Browser.prototype.runTest = function (file, buffer) {
        if (/\.html$/.test(file)) {
            buffer.location.replace(file);
        } else { // if (/\.js$/.test(file)) {
            if (/MSIE|Opera|Safari|Konqueror/.test(navigator.userAgent)) {
                // These browsers have problems with the DOM solution. It
                // simply doesn't work in Safari, and Opera considers its
                // handling of buffer.document to be a security violation. So
                // have them use the XML hack, instead.
                this.includes[this.includes.length-1] = file;
                buffer.location.replace(location.pathname + "?xml-hack=1");
                return;
            }
            // document.write() simply doesn't work here. Thanks to
            // Pawel Chmielowski for figuring that out!
            var doc = buffer.document;
            doc.open("text/html");
            doc.close();
            var el;

            // XXX Opera chokes on this line. It thinks that using the doc
            // element like this is a security violation, never mind that we
            // were the ones who actually created it. Whatever!
            var body = doc.body || doc.getElementsByTagName("body")[0];
            var head = doc.getElementsByTagName("head")[0];

            // Safari seems to be headless at this point.
            if (!head) {
                head = doc.createElement('head');
                doc.appendChild(head);
            }

            // Add script elements for all includes.
            for (var i = 0; i < this.includes.length - 1; i++) {
                el = doc.createElement("script");
                el.setAttribute("src", this.includes[i]);
                head.appendChild(el);
            }


            // Create the pre and script element for the test file.
            var pre = doc.createElement("pre");
            pre.id = "test";
            el = doc.createElement("script");
            el.type = "text/javascript";
            if (this.encoding()) el.charset = this.encoding();

            // XXX This doesn't work in Safari right now. See
            // http://bugs.webkit.org/show_bug.cgi?id=3748
            el.src = file;
            pre.appendChild(el);

            // Create a script element to finish the tests.
            el = doc.createElement("script");
            el.type = "text/javascript";
            var text = "window.onload(null, Test)";

            // IE doesn't let script elements have children.
            if (null != el.canHaveChildren) el.text = text;
            // But most other browsers do.
            else el.appendChild(document.createTextNode(text));

            pre.appendChild(el);

            // IE 6 SP 2 Requires getting the body element again.
            body = doc.body || doc.getElementsByTagName("body")[0];
            body.appendChild(pre);
        /* Let's just assume that if it's not .html, it's JavaScript.
        } else {
            // Who are you, man??
            alert("I don't know what kind of file '" + file + "' is");
        */
        }
    };

    Test.Harness.Browser.prototype.args = {};
    var pairs = location.search.substring(1).split(/[;&]/);
    for (var i = 0; i < pairs.length; i++) {
        var parts = pairs[i].split('=');
        if (parts[0] == null) continue;
        var key = unescape(parts[0]), val = unescape(parts[1]);
        if (Test.Harness.Browser.prototype.args[key] == null) {
            Test.Harness.Browser.prototype.args[key] = unescape(val);
        } else {
            if (typeof Test.Harness.Browser.prototype.args[key] == 'string') {
                Test.Harness.Browser.prototype.args[key] =
                    [Test.Harness.Browser.prototype.args[key], unescape(val)];
            } else {
                Test.Harness.Browser.prototype.args[key].push(unescape(val));
            }
        }
    }

    Test.Harness.Browser.prototype.formatFailures = function (fn) {
        // XXX Switch to DOM?
        var failedStr = "Failed Test";
        var middleStr = " Total Fail  Failed  ";
        var listStr = "List of Failed";
        var table =
            '<style>table {padding: 0; border-collapse: collapse; }'
          + 'tr { height: 2em; }'
          + 'th { background: lightgrey; }'
          + 'td, th { padding: 2px 5px; text-align: left; border: solid #000000 1px;}'
          + '.odd { background: #e8e8cd }'
          + '</style>'
          + '<table style="padding: 0"><tr><th>Failed Test</th><th>Total</th>'
          + '<th>Fail</th><th>Failed</th></tr>';
        for (var i = 0; i < this.failures.length; i++) {
            var track = this.failures[i];
            var style = i % 2 ? 'even' : 'odd';
            table += '<tr class="' + style + '"><td>' + track.fn + '</td>'
              + '<td>' + track.total + '</td>'
              + '<td>' + (track.total - track.ok) + '</td>'
              + '<td>' + this._failList(track.failList) + '</td></tr>';
        };
        table += '</table>' + Test.Harness.LF;
        var node = document.getElementById('summary');
        node.innerHTML += table;
        window.scrollTo(0, document.body.offsetHeight
                        || document.body.scrollHeight);
    };
}