tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

testharness.js (94608B)


      1 /*global self*/
      2 /*jshint latedef: nofunc*/
      3 /*
      4 Distributed under both the W3C Test Suite License [1] and the W3C
      5 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
      6 policies and contribution forms [3].
      7 
      8 [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
      9 [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
     10 [3] http://www.w3.org/2004/10/27-testcases
     11 */
     12 
     13 /* Documentation is in docs/api.md */
     14 
     15 (function ()
     16 {
     17    var debug = false;
     18    // default timeout is 10 seconds, test can override if needed
     19    var settings = {
     20        output:true,
     21        harness_timeout:{
     22            "normal":10000,
     23            "long":60000
     24        },
     25        test_timeout:null,
     26        message_events: ["start", "test_state", "result", "completion"]
     27    };
     28 
     29    var xhtml_ns = "http://www.w3.org/1999/xhtml";
     30 
     31    /*
     32     * TestEnvironment is an abstraction for the environment in which the test
     33     * harness is used. Each implementation of a test environment has to provide
     34     * the following interface:
     35     *
     36     * interface TestEnvironment {
     37     *   // Invoked after the global 'tests' object has been created and it's
     38     *   // safe to call add_*_callback() to register event handlers.
     39     *   void on_tests_ready();
     40     *
     41     *   // Invoked after setup() has been called to notify the test environment
     42     *   // of changes to the test harness properties.
     43     *   void on_new_harness_properties(object properties);
     44     *
     45     *   // Should return a new unique default test name.
     46     *   DOMString next_default_test_name();
     47     *
     48     *   // Should return the test harness timeout duration in milliseconds.
     49     *   float test_timeout();
     50     *
     51     *   // Should return the global scope object.
     52     *   object global_scope();
     53     * };
     54     */
     55 
     56    /*
     57     * A test environment with a DOM. The global object is 'window'. By default
     58     * test results are displayed in a table. Any parent windows receive
     59     * callbacks or messages via postMessage() when test events occur. See
     60     * apisample11.html and apisample12.html.
     61     */
     62    function WindowTestEnvironment() {
     63        this.name_counter = 0;
     64        this.window_cache = null;
     65        this.output_handler = null;
     66        this.all_loaded = false;
     67        var this_obj = this;
     68        this.message_events = [];
     69 
     70        this.message_functions = {
     71            start: [add_start_callback, remove_start_callback,
     72                    function (properties) {
     73                        this_obj._dispatch("start_callback", [properties],
     74                                           {type: "start", properties: properties});
     75                    }],
     76 
     77            test_state: [add_test_state_callback, remove_test_state_callback,
     78                         function(test) {
     79                             this_obj._dispatch("test_state_callback", [test],
     80                                                {type: "test_state",
     81                                                 test: test.structured_clone()});
     82                         }],
     83            result: [add_result_callback, remove_result_callback,
     84                     function (test) {
     85                         this_obj.output_handler.show_status();
     86                         this_obj._dispatch("result_callback", [test],
     87                                            {type: "result",
     88                                             test: test.structured_clone()});
     89                     }],
     90            completion: [add_completion_callback, remove_completion_callback,
     91                         function (tests, harness_status) {
     92                             var cloned_tests = map(tests, function(test) {
     93                                 return test.structured_clone();
     94                             });
     95                             this_obj._dispatch("completion_callback", [tests, harness_status],
     96                                                {type: "complete",
     97                                                 tests: cloned_tests,
     98                                                 status: harness_status.structured_clone()});
     99                         }]
    100        }
    101 
    102        on_event(window, 'load', function() {
    103            this_obj.all_loaded = true;
    104        });
    105    }
    106 
    107    WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
    108        this._forEach_windows(
    109                function(w, same_origin) {
    110                    if (same_origin) {
    111                        try {
    112                            var has_selector = selector in w;
    113                        } catch(e) {
    114                            // If document.domain was set at some point same_origin can be
    115                            // wrong and the above will fail.
    116                            has_selector = false;
    117                        }
    118                        if (has_selector) {
    119                            try {
    120                                w[selector].apply(undefined, callback_args);
    121                            } catch (e) {
    122                                if (debug) {
    123                                    throw e;
    124                                }
    125                            }
    126                        }
    127                    }
    128                    if (supports_post_message(w) && w !== self) {
    129                        w.postMessage(message_arg, "*");
    130                    }
    131                });
    132    };
    133 
    134    WindowTestEnvironment.prototype._forEach_windows = function(callback) {
    135        // Iterate of the the windows [self ... top, opener]. The callback is passed
    136        // two objects, the first one is the windows object itself, the second one
    137        // is a boolean indicating whether or not its on the same origin as the
    138        // current window.
    139        var cache = this.window_cache;
    140        if (!cache) {
    141            cache = [[self, true]];
    142            var w = self;
    143            var i = 0;
    144            var so;
    145            var origins = location.ancestorOrigins;
    146            while (w != w.parent) {
    147                w = w.parent;
    148                // In WebKit, calls to parent windows' properties that aren't on the same
    149                // origin cause an error message to be displayed in the error console but
    150                // don't throw an exception. This is a deviation from the current HTML5
    151                // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504
    152                // The problem with WebKit's behavior is that it pollutes the error console
    153                // with error messages that can't be caught.
    154                //
    155                // This issue can be mitigated by relying on the (for now) proprietary
    156                // `location.ancestorOrigins` property which returns an ordered list of
    157                // the origins of enclosing windows. See:
    158                // http://trac.webkit.org/changeset/113945.
    159                if (origins) {
    160                    so = (location.origin == origins[i]);
    161                } else {
    162                    so = is_same_origin(w);
    163                }
    164                cache.push([w, so]);
    165                i++;
    166            }
    167            w = window.opener;
    168            if (w) {
    169                // window.opener isn't included in the `location.ancestorOrigins` prop.
    170                // We'll just have to deal with a simple check and an error msg on WebKit
    171                // browsers in this case.
    172                cache.push([w, is_same_origin(w)]);
    173            }
    174            this.window_cache = cache;
    175        }
    176 
    177        forEach(cache,
    178                function(a) {
    179                    callback.apply(null, a);
    180                });
    181    };
    182 
    183    WindowTestEnvironment.prototype.on_tests_ready = function() {
    184        var output = new Output();
    185        this.output_handler = output;
    186 
    187        var this_obj = this;
    188 
    189        add_start_callback(function (properties) {
    190            this_obj.output_handler.init(properties);
    191        });
    192 
    193        add_test_state_callback(function(test) {
    194            this_obj.output_handler.show_status();
    195        });
    196 
    197        add_result_callback(function (test) {
    198            this_obj.output_handler.show_status();
    199        });
    200 
    201        add_completion_callback(function (tests, harness_status) {
    202            this_obj.output_handler.show_results(tests, harness_status);
    203        });
    204        this.setup_messages(settings.message_events);
    205    };
    206 
    207    WindowTestEnvironment.prototype.setup_messages = function(new_events) {
    208        var this_obj = this;
    209        forEach(settings.message_events, function(x) {
    210            var current_dispatch = this_obj.message_events.includes(x);
    211            var new_dispatch = new_events.includes(x);
    212            if (!current_dispatch && new_dispatch) {
    213                this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
    214            } else if (current_dispatch && !new_dispatch) {
    215                this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
    216            }
    217        });
    218        this.message_events = new_events;
    219    }
    220 
    221    WindowTestEnvironment.prototype.next_default_test_name = function() {
    222        //Don't use document.title to work around an Opera bug in XHTML documents
    223        var title = document.getElementsByTagName("title")[0];
    224        var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled";
    225        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
    226        this.name_counter++;
    227        return prefix + suffix;
    228    };
    229 
    230    WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
    231        this.output_handler.setup(properties);
    232        if (properties.hasOwnProperty("message_events")) {
    233            this.setup_messages(properties.message_events);
    234        }
    235    };
    236 
    237    WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
    238        on_event(window, 'load', callback);
    239    };
    240 
    241    WindowTestEnvironment.prototype.test_timeout = function() {
    242        var metas = document.getElementsByTagName("meta");
    243        for (var i = 0; i < metas.length; i++) {
    244            if (metas[i].name == "timeout") {
    245                if (metas[i].content == "long") {
    246                    return settings.harness_timeout.long;
    247                }
    248                break;
    249            }
    250        }
    251        return settings.harness_timeout.normal;
    252    };
    253 
    254    WindowTestEnvironment.prototype.global_scope = function() {
    255        return window;
    256    };
    257 
    258    /*
    259     * Base TestEnvironment implementation for a generic web worker.
    260     *
    261     * Workers accumulate test results. One or more clients can connect and
    262     * retrieve results from a worker at any time.
    263     *
    264     * WorkerTestEnvironment supports communicating with a client via a
    265     * MessagePort.  The mechanism for determining the appropriate MessagePort
    266     * for communicating with a client depends on the type of worker and is
    267     * implemented by the various specializations of WorkerTestEnvironment
    268     * below.
    269     *
    270     * A client document using testharness can use fetch_tests_from_worker() to
    271     * retrieve results from a worker. See apisample16.html.
    272     */
    273    function WorkerTestEnvironment() {
    274        this.name_counter = 0;
    275        this.all_loaded = true;
    276        this.message_list = [];
    277        this.message_ports = [];
    278    }
    279 
    280    WorkerTestEnvironment.prototype._dispatch = function(message) {
    281        this.message_list.push(message);
    282        for (var i = 0; i < this.message_ports.length; ++i)
    283        {
    284            this.message_ports[i].postMessage(message);
    285        }
    286    };
    287 
    288    // The only requirement is that port has a postMessage() method. It doesn't
    289    // have to be an instance of a MessagePort, and often isn't.
    290    WorkerTestEnvironment.prototype._add_message_port = function(port) {
    291        this.message_ports.push(port);
    292        for (var i = 0; i < this.message_list.length; ++i)
    293        {
    294            port.postMessage(this.message_list[i]);
    295        }
    296    };
    297 
    298    WorkerTestEnvironment.prototype.next_default_test_name = function() {
    299        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
    300        this.name_counter++;
    301        return "Untitled" + suffix;
    302    };
    303 
    304    WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
    305 
    306    WorkerTestEnvironment.prototype.on_tests_ready = function() {
    307        var this_obj = this;
    308        add_start_callback(
    309                function(properties) {
    310                    this_obj._dispatch({
    311                        type: "start",
    312                        properties: properties,
    313                    });
    314                });
    315        add_test_state_callback(
    316                function(test) {
    317                    this_obj._dispatch({
    318                        type: "test_state",
    319                        test: test.structured_clone()
    320                    });
    321                });
    322        add_result_callback(
    323                function(test) {
    324                    this_obj._dispatch({
    325                        type: "result",
    326                        test: test.structured_clone()
    327                    });
    328                });
    329        add_completion_callback(
    330                function(tests, harness_status) {
    331                    this_obj._dispatch({
    332                        type: "complete",
    333                        tests: map(tests,
    334                            function(test) {
    335                                return test.structured_clone();
    336                            }),
    337                        status: harness_status.structured_clone()
    338                    });
    339                });
    340    };
    341 
    342    WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
    343 
    344    WorkerTestEnvironment.prototype.test_timeout = function() {
    345        // Tests running in a worker don't have a default timeout. I.e. all
    346        // worker tests behave as if settings.explicit_timeout is true.
    347        return null;
    348    };
    349 
    350    WorkerTestEnvironment.prototype.global_scope = function() {
    351        return self;
    352    };
    353 
    354    /*
    355     * Dedicated web workers.
    356     * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
    357     *
    358     * This class is used as the test_environment when testharness is running
    359     * inside a dedicated worker.
    360     */
    361    function DedicatedWorkerTestEnvironment() {
    362        WorkerTestEnvironment.call(this);
    363        // self is an instance of DedicatedWorkerGlobalScope which exposes
    364        // a postMessage() method for communicating via the message channel
    365        // established when the worker is created.
    366        this._add_message_port(self);
    367    }
    368    DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
    369 
    370    DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
    371        WorkerTestEnvironment.prototype.on_tests_ready.call(this);
    372        // In the absence of an onload notification, we a require dedicated
    373        // workers to explicitly signal when the tests are done.
    374        tests.wait_for_finish = true;
    375    };
    376 
    377    /*
    378     * Shared web workers.
    379     * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
    380     *
    381     * This class is used as the test_environment when testharness is running
    382     * inside a shared web worker.
    383     */
    384    function SharedWorkerTestEnvironment() {
    385        WorkerTestEnvironment.call(this);
    386        var this_obj = this;
    387        // Shared workers receive message ports via the 'onconnect' event for
    388        // each connection.
    389        self.addEventListener("connect",
    390                function(message_event) {
    391                    this_obj._add_message_port(message_event.source);
    392                });
    393    }
    394    SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
    395 
    396    SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
    397        WorkerTestEnvironment.prototype.on_tests_ready.call(this);
    398        // In the absence of an onload notification, we a require shared
    399        // workers to explicitly signal when the tests are done.
    400        tests.wait_for_finish = true;
    401    };
    402 
    403    /*
    404     * Service workers.
    405     * http://www.w3.org/TR/service-workers/
    406     *
    407     * This class is used as the test_environment when testharness is running
    408     * inside a service worker.
    409     */
    410    function ServiceWorkerTestEnvironment() {
    411        WorkerTestEnvironment.call(this);
    412        this.all_loaded = false;
    413        this.on_loaded_callback = null;
    414        var this_obj = this;
    415        self.addEventListener("message",
    416                function(event) {
    417                    if (event.data.type && event.data.type === "connect") {
    418                        if (event.ports && event.ports[0]) {
    419                            // If a MessageChannel was passed, then use it to
    420                            // send results back to the main window.  This
    421                            // allows the tests to work even if the browser
    422                            // does not fully support MessageEvent.source in
    423                            // ServiceWorkers yet.
    424                            this_obj._add_message_port(event.ports[0]);
    425                            event.ports[0].start();
    426                        } else {
    427                            // If there is no MessageChannel, then attempt to
    428                            // use the MessageEvent.source to send results
    429                            // back to the main window.
    430                            this_obj._add_message_port(event.source);
    431                        }
    432                    }
    433                });
    434 
    435        // The oninstall event is received after the service worker script and
    436        // all imported scripts have been fetched and executed. It's the
    437        // equivalent of an onload event for a document. All tests should have
    438        // been added by the time this event is received, thus it's not
    439        // necessary to wait until the onactivate event.
    440        on_event(self, "install",
    441                function(event) {
    442                    this_obj.all_loaded = true;
    443                    if (this_obj.on_loaded_callback) {
    444                        this_obj.on_loaded_callback();
    445                    }
    446                });
    447    }
    448    ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
    449 
    450    ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
    451        if (this.all_loaded) {
    452            callback();
    453        } else {
    454            this.on_loaded_callback = callback;
    455        }
    456    };
    457 
    458    function create_test_environment() {
    459        if ('document' in self) {
    460            return new WindowTestEnvironment();
    461        }
    462        if ('DedicatedWorkerGlobalScope' in self &&
    463            self instanceof DedicatedWorkerGlobalScope) {
    464            return new DedicatedWorkerTestEnvironment();
    465        }
    466        if ('SharedWorkerGlobalScope' in self &&
    467            self instanceof SharedWorkerGlobalScope) {
    468            return new SharedWorkerTestEnvironment();
    469        }
    470        if ('ServiceWorkerGlobalScope' in self &&
    471            self instanceof ServiceWorkerGlobalScope) {
    472            return new ServiceWorkerTestEnvironment();
    473        }
    474        throw new Error("Unsupported test environment");
    475    }
    476 
    477    var test_environment = create_test_environment();
    478 
    479    function is_shared_worker(worker) {
    480        return 'SharedWorker' in self && worker instanceof SharedWorker;
    481    }
    482 
    483    function is_service_worker(worker) {
    484        return 'ServiceWorker' in self && worker instanceof ServiceWorker;
    485    }
    486 
    487    /*
    488     * API functions
    489     */
    490 
    491    function test(func, name, properties)
    492    {
    493        var test_name = name ? name : test_environment.next_default_test_name();
    494        properties = properties ? properties : {};
    495        var test_obj = new Test(test_name, properties);
    496        test_obj.step(func, test_obj, test_obj);
    497        if (test_obj.phase === test_obj.phases.STARTED) {
    498            test_obj.done();
    499        }
    500    }
    501 
    502    function async_test(func, name, properties)
    503    {
    504        if (typeof func !== "function") {
    505            properties = name;
    506            name = func;
    507            func = null;
    508        }
    509        var test_name = name ? name : test_environment.next_default_test_name();
    510        properties = properties ? properties : {};
    511        var test_obj = new Test(test_name, properties);
    512        if (func) {
    513            test_obj.step(func, test_obj, test_obj);
    514        }
    515        return test_obj;
    516    }
    517 
    518    function promise_test(func, name, properties) {
    519        var test = async_test(name, properties);
    520        // If there is no promise tests queue make one.
    521        test.step(function() {
    522            if (!tests.promise_tests) {
    523                tests.promise_tests = Promise.resolve();
    524            }
    525        });
    526        tests.promise_tests = tests.promise_tests.then(function() {
    527            return Promise.resolve(test.step(func, test, test))
    528                .then(
    529                    function() {
    530                        test.done();
    531                    })
    532                .catch(test.step_func(
    533                    function(value) {
    534                        if (value instanceof AssertionError) {
    535                            throw value;
    536                        }
    537                        assert(false, "promise_test", null,
    538                               "Unhandled rejection with value: ${value}", {value:value});
    539                    }));
    540        });
    541    }
    542 
    543    function promise_rejects(test, expected, promise) {
    544        return promise.then(test.unreached_func("Should have rejected.")).catch(function(e) {
    545            assert_throws(expected, function() { throw e });
    546        });
    547    }
    548 
    549    /**
    550     * This constructor helper allows DOM events to be handled using Promises,
    551     * which can make it a lot easier to test a very specific series of events,
    552     * including ensuring that unexpected events are not fired at any point.
    553     */
    554    function EventWatcher(test, watchedNode, eventTypes)
    555    {
    556        if (typeof eventTypes == 'string') {
    557            eventTypes = [eventTypes];
    558        }
    559 
    560        var waitingFor = null;
    561 
    562        var eventHandler = test.step_func(function(evt) {
    563            assert_true(!!waitingFor,
    564                        'Not expecting event, but got ' + evt.type + ' event');
    565            assert_equals(evt.type, waitingFor.types[0],
    566                          'Expected ' + waitingFor.types[0] + ' event, but got ' +
    567                          evt.type + ' event instead');
    568            if (waitingFor.types.length > 1) {
    569                // Pop first event from array
    570                waitingFor.types.shift();
    571                return;
    572            }
    573            // We need to null out waitingFor before calling the resolve function
    574            // since the Promise's resolve handlers may call wait_for() which will
    575            // need to set waitingFor.
    576            var resolveFunc = waitingFor.resolve;
    577            waitingFor = null;
    578            resolveFunc(evt);
    579        });
    580 
    581        for (var i = 0; i < eventTypes.length; i++) {
    582            watchedNode.addEventListener(eventTypes[i], eventHandler);
    583        }
    584 
    585        /**
    586         * Returns a Promise that will resolve after the specified event or
    587         * series of events has occured.
    588         */
    589        this.wait_for = function(types) {
    590            if (waitingFor) {
    591                return Promise.reject('Already waiting for an event or events');
    592            }
    593            if (typeof types == 'string') {
    594                types = [types];
    595            }
    596            return new Promise(function(resolve, reject) {
    597                waitingFor = {
    598                    types: types,
    599                    resolve: resolve,
    600                    reject: reject
    601                };
    602            });
    603        };
    604 
    605        function stop_watching() {
    606            for (var i = 0; i < eventTypes.length; i++) {
    607                watchedNode.removeEventListener(eventTypes[i], eventHandler);
    608            }
    609        };
    610 
    611        test.add_cleanup(stop_watching);
    612 
    613        return this;
    614    }
    615    expose(EventWatcher, 'EventWatcher');
    616 
    617    function setup(func_or_properties, maybe_properties)
    618    {
    619        var func = null;
    620        var properties = {};
    621        if (arguments.length === 2) {
    622            func = func_or_properties;
    623            properties = maybe_properties;
    624        } else if (func_or_properties instanceof Function) {
    625            func = func_or_properties;
    626        } else {
    627            properties = func_or_properties;
    628        }
    629        tests.setup(func, properties);
    630        test_environment.on_new_harness_properties(properties);
    631    }
    632 
    633    function done() {
    634        if (tests.tests.length === 0) {
    635            tests.set_file_is_test();
    636        }
    637        if (tests.file_is_test) {
    638            tests.tests[0].done();
    639        }
    640        tests.end_wait();
    641    }
    642 
    643    function generate_tests(func, args, properties) {
    644        forEach(args, function(x, i)
    645                {
    646                    var name = x[0];
    647                    test(function()
    648                         {
    649                             func.apply(this, x.slice(1));
    650                         },
    651                         name,
    652                         Array.isArray(properties) ? properties[i] : properties);
    653                });
    654    }
    655 
    656    function on_event(object, event, callback)
    657    {
    658        object.addEventListener(event, callback);
    659    }
    660 
    661    function step_timeout(f, t) {
    662        var outer_this = this;
    663        var args = Array.prototype.slice.call(arguments, 2);
    664        return setTimeout(function() {
    665            f.apply(outer_this, args);
    666        }, t * tests.timeout_multiplier);
    667    }
    668 
    669    expose(test, 'test');
    670    expose(async_test, 'async_test');
    671    expose(promise_test, 'promise_test');
    672    expose(promise_rejects, 'promise_rejects');
    673    expose(generate_tests, 'generate_tests');
    674    expose(setup, 'setup');
    675    expose(done, 'done');
    676    expose(on_event, 'on_event');
    677    expose(step_timeout, 'step_timeout');
    678 
    679    /*
    680     * Return a string truncated to the given length, with ... added at the end
    681     * if it was longer.
    682     */
    683    function truncate(s, len)
    684    {
    685        if (s.length > len) {
    686            return s.substring(0, len - 3) + "...";
    687        }
    688        return s;
    689    }
    690 
    691    /*
    692     * Return true if object is probably a Node object.
    693     */
    694    function is_node(object)
    695    {
    696        // I use duck-typing instead of instanceof, because
    697        // instanceof doesn't work if the node is from another window (like an
    698        // iframe's contentWindow):
    699        // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
    700        if ("nodeType" in object &&
    701            "nodeName" in object &&
    702            "nodeValue" in object &&
    703            "childNodes" in object) {
    704            try {
    705                object.nodeType;
    706            } catch (e) {
    707                // The object is probably Node.prototype or another prototype
    708                // object that inherits from it, and not a Node instance.
    709                return false;
    710            }
    711            return true;
    712        }
    713        return false;
    714    }
    715 
    716    /*
    717     * Convert a value to a nice, human-readable string
    718     */
    719    function format_value(val, seen)
    720    {
    721        if (!seen) {
    722            seen = [];
    723        }
    724        if (typeof val === "object" && val !== null) {
    725            if (seen.includes(val)) {
    726                return "[...]";
    727            }
    728            seen.push(val);
    729        }
    730        if (Array.isArray(val)) {
    731            return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]";
    732        }
    733 
    734        switch (typeof val) {
    735        case "string":
    736            val = val.replace("\\", "\\\\");
    737            for (var i = 0; i < 32; i++) {
    738                var replace = "\\";
    739                switch (i) {
    740                case 0: replace += "0"; break;
    741                case 1: replace += "x01"; break;
    742                case 2: replace += "x02"; break;
    743                case 3: replace += "x03"; break;
    744                case 4: replace += "x04"; break;
    745                case 5: replace += "x05"; break;
    746                case 6: replace += "x06"; break;
    747                case 7: replace += "x07"; break;
    748                case 8: replace += "b"; break;
    749                case 9: replace += "t"; break;
    750                case 10: replace += "n"; break;
    751                case 11: replace += "v"; break;
    752                case 12: replace += "f"; break;
    753                case 13: replace += "r"; break;
    754                case 14: replace += "x0e"; break;
    755                case 15: replace += "x0f"; break;
    756                case 16: replace += "x10"; break;
    757                case 17: replace += "x11"; break;
    758                case 18: replace += "x12"; break;
    759                case 19: replace += "x13"; break;
    760                case 20: replace += "x14"; break;
    761                case 21: replace += "x15"; break;
    762                case 22: replace += "x16"; break;
    763                case 23: replace += "x17"; break;
    764                case 24: replace += "x18"; break;
    765                case 25: replace += "x19"; break;
    766                case 26: replace += "x1a"; break;
    767                case 27: replace += "x1b"; break;
    768                case 28: replace += "x1c"; break;
    769                case 29: replace += "x1d"; break;
    770                case 30: replace += "x1e"; break;
    771                case 31: replace += "x1f"; break;
    772                }
    773                val = val.replace(RegExp(String.fromCharCode(i), "g"), replace);
    774            }
    775            return '"' + val.replace(/"/g, '\\"') + '"';
    776        case "boolean":
    777        case "undefined":
    778            return String(val);
    779        case "number":
    780            // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
    781            // special-case.
    782            if (val === -0 && 1/val === -Infinity) {
    783                return "-0";
    784            }
    785            return String(val);
    786        case "object":
    787            if (val === null) {
    788                return "null";
    789            }
    790 
    791            // Special-case Node objects, since those come up a lot in my tests.  I
    792            // ignore namespaces.
    793            if (is_node(val)) {
    794                switch (val.nodeType) {
    795                case Node.ELEMENT_NODE:
    796                    var ret = "<" + val.localName;
    797                    for (var i = 0; i < val.attributes.length; i++) {
    798                        ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
    799                    }
    800                    ret += ">" + val.innerHTML + "</" + val.localName + ">";
    801                    return "Element node " + truncate(ret, 60);
    802                case Node.TEXT_NODE:
    803                    return 'Text node "' + truncate(val.data, 60) + '"';
    804                case Node.PROCESSING_INSTRUCTION_NODE:
    805                    return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
    806                case Node.COMMENT_NODE:
    807                    return "Comment node <!--" + truncate(val.data, 60) + "-->";
    808                case Node.DOCUMENT_NODE:
    809                    return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
    810                case Node.DOCUMENT_TYPE_NODE:
    811                    return "DocumentType node";
    812                case Node.DOCUMENT_FRAGMENT_NODE:
    813                    return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
    814                default:
    815                    return "Node object of unknown type";
    816                }
    817            }
    818 
    819        /* falls through */
    820        default:
    821            return typeof val + ' "' + truncate(String(val), 60) + '"';
    822        }
    823    }
    824    expose(format_value, "format_value");
    825 
    826    /*
    827     * Assertions
    828     */
    829 
    830    function assert_true(actual, description)
    831    {
    832        assert(actual === true, "assert_true", description,
    833                                "expected true got ${actual}", {actual:actual});
    834    }
    835    expose(assert_true, "assert_true");
    836 
    837    function assert_false(actual, description)
    838    {
    839        assert(actual === false, "assert_false", description,
    840                                 "expected false got ${actual}", {actual:actual});
    841    }
    842    expose(assert_false, "assert_false");
    843 
    844    function same_value(x, y) {
    845        if (y !== y) {
    846            //NaN case
    847            return x !== x;
    848        }
    849        if (x === 0 && y === 0) {
    850            //Distinguish +0 and -0
    851            return 1/x === 1/y;
    852        }
    853        return x === y;
    854    }
    855 
    856    function assert_equals(actual, expected, description)
    857    {
    858         /*
    859          * Test if two primitives are equal or two objects
    860          * are the same object
    861          */
    862        if (typeof actual != typeof expected) {
    863            assert(false, "assert_equals", description,
    864                          "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
    865                          {expected:expected, actual:actual});
    866            return;
    867        }
    868        assert(same_value(actual, expected), "assert_equals", description,
    869                                             "expected ${expected} but got ${actual}",
    870                                             {expected:expected, actual:actual});
    871    }
    872    expose(assert_equals, "assert_equals");
    873 
    874    function assert_not_equals(actual, expected, description)
    875    {
    876         /*
    877          * Test if two primitives are unequal or two objects
    878          * are different objects
    879          */
    880        assert(!same_value(actual, expected), "assert_not_equals", description,
    881                                              "got disallowed value ${actual}",
    882                                              {actual:actual});
    883    }
    884    expose(assert_not_equals, "assert_not_equals");
    885 
    886    function assert_in_array(actual, expected, description)
    887    {
    888        assert(expected.includes(actual), "assert_in_array", description,
    889                                               "value ${actual} not in array ${expected}",
    890                                               {actual:actual, expected:expected});
    891    }
    892    expose(assert_in_array, "assert_in_array");
    893 
    894    function assert_object_equals(actual, expected, description)
    895    {
    896         //This needs to be improved a great deal
    897         function check_equal(actual, expected, stack)
    898         {
    899             stack.push(actual);
    900 
    901             var p;
    902             for (p in actual) {
    903                 assert(expected.hasOwnProperty(p), "assert_object_equals", description,
    904                                                    "unexpected property ${p}", {p:p});
    905 
    906                 if (typeof actual[p] === "object" && actual[p] !== null) {
    907                     if (!stack.includes(actual[p])) {
    908                         check_equal(actual[p], expected[p], stack);
    909                     }
    910                 } else {
    911                     assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
    912                                                       "property ${p} expected ${expected} got ${actual}",
    913                                                       {p:p, expected:expected, actual:actual});
    914                 }
    915             }
    916             for (p in expected) {
    917                 assert(actual.hasOwnProperty(p),
    918                        "assert_object_equals", description,
    919                        "expected property ${p} missing", {p:p});
    920             }
    921             stack.pop();
    922         }
    923         check_equal(actual, expected, []);
    924    }
    925    expose(assert_object_equals, "assert_object_equals");
    926 
    927    function assert_array_equals(actual, expected, description)
    928    {
    929        assert(actual.length === expected.length,
    930               "assert_array_equals", description,
    931               "lengths differ, expected ${expected} got ${actual}",
    932               {expected:expected.length, actual:actual.length});
    933 
    934        for (var i = 0; i < actual.length; i++) {
    935            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
    936                   "assert_array_equals", description,
    937                   "property ${i}, property expected to be ${expected} but was ${actual}",
    938                   {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
    939                   actual:actual.hasOwnProperty(i) ? "present" : "missing"});
    940            assert(same_value(expected[i], actual[i]),
    941                   "assert_array_equals", description,
    942                   "property ${i}, expected ${expected} but got ${actual}",
    943                   {i:i, expected:expected[i], actual:actual[i]});
    944        }
    945    }
    946    expose(assert_array_equals, "assert_array_equals");
    947 
    948    function assert_approx_equals(actual, expected, epsilon, description)
    949    {
    950        /*
    951         * Test if two primitive numbers are equal withing +/- epsilon
    952         */
    953        assert(typeof actual === "number",
    954               "assert_approx_equals", description,
    955               "expected a number but got a ${type_actual}",
    956               {type_actual:typeof actual});
    957 
    958        assert(Math.abs(actual - expected) <= epsilon,
    959               "assert_approx_equals", description,
    960               "expected ${expected} +/- ${epsilon} but got ${actual}",
    961               {expected:expected, actual:actual, epsilon:epsilon});
    962    }
    963    expose(assert_approx_equals, "assert_approx_equals");
    964 
    965    function assert_less_than(actual, expected, description)
    966    {
    967        /*
    968         * Test if a primitive number is less than another
    969         */
    970        assert(typeof actual === "number",
    971               "assert_less_than", description,
    972               "expected a number but got a ${type_actual}",
    973               {type_actual:typeof actual});
    974 
    975        assert(actual < expected,
    976               "assert_less_than", description,
    977               "expected a number less than ${expected} but got ${actual}",
    978               {expected:expected, actual:actual});
    979    }
    980    expose(assert_less_than, "assert_less_than");
    981 
    982    function assert_greater_than(actual, expected, description)
    983    {
    984        /*
    985         * Test if a primitive number is greater than another
    986         */
    987        assert(typeof actual === "number",
    988               "assert_greater_than", description,
    989               "expected a number but got a ${type_actual}",
    990               {type_actual:typeof actual});
    991 
    992        assert(actual > expected,
    993               "assert_greater_than", description,
    994               "expected a number greater than ${expected} but got ${actual}",
    995               {expected:expected, actual:actual});
    996    }
    997    expose(assert_greater_than, "assert_greater_than");
    998 
    999    function assert_between_exclusive(actual, lower, upper, description)
   1000    {
   1001        /*
   1002         * Test if a primitive number is between two others
   1003         */
   1004        assert(typeof actual === "number",
   1005               "assert_between_exclusive", description,
   1006               "expected a number but got a ${type_actual}",
   1007               {type_actual:typeof actual});
   1008 
   1009        assert(actual > lower && actual < upper,
   1010               "assert_between_exclusive", description,
   1011               "expected a number greater than ${lower} " +
   1012               "and less than ${upper} but got ${actual}",
   1013               {lower:lower, upper:upper, actual:actual});
   1014    }
   1015    expose(assert_between_exclusive, "assert_between_exclusive");
   1016 
   1017    function assert_less_than_equal(actual, expected, description)
   1018    {
   1019        /*
   1020         * Test if a primitive number is less than or equal to another
   1021         */
   1022        assert(typeof actual === "number",
   1023               "assert_less_than_equal", description,
   1024               "expected a number but got a ${type_actual}",
   1025               {type_actual:typeof actual});
   1026 
   1027        assert(actual <= expected,
   1028               "assert_less_than_equal", description,
   1029               "expected a number less than or equal to ${expected} but got ${actual}",
   1030               {expected:expected, actual:actual});
   1031    }
   1032    expose(assert_less_than_equal, "assert_less_than_equal");
   1033 
   1034    function assert_greater_than_equal(actual, expected, description)
   1035    {
   1036        /*
   1037         * Test if a primitive number is greater than or equal to another
   1038         */
   1039        assert(typeof actual === "number",
   1040               "assert_greater_than_equal", description,
   1041               "expected a number but got a ${type_actual}",
   1042               {type_actual:typeof actual});
   1043 
   1044        assert(actual >= expected,
   1045               "assert_greater_than_equal", description,
   1046               "expected a number greater than or equal to ${expected} but got ${actual}",
   1047               {expected:expected, actual:actual});
   1048    }
   1049    expose(assert_greater_than_equal, "assert_greater_than_equal");
   1050 
   1051    function assert_between_inclusive(actual, lower, upper, description)
   1052    {
   1053        /*
   1054         * Test if a primitive number is between to two others or equal to either of them
   1055         */
   1056        assert(typeof actual === "number",
   1057               "assert_between_inclusive", description,
   1058               "expected a number but got a ${type_actual}",
   1059               {type_actual:typeof actual});
   1060 
   1061        assert(actual >= lower && actual <= upper,
   1062               "assert_between_inclusive", description,
   1063               "expected a number greater than or equal to ${lower} " +
   1064               "and less than or equal to ${upper} but got ${actual}",
   1065               {lower:lower, upper:upper, actual:actual});
   1066    }
   1067    expose(assert_between_inclusive, "assert_between_inclusive");
   1068 
   1069    function assert_regexp_match(actual, expected, description) {
   1070        /*
   1071         * Test if a string (actual) matches a regexp (expected)
   1072         */
   1073        assert(expected.test(actual),
   1074               "assert_regexp_match", description,
   1075               "expected ${expected} but got ${actual}",
   1076               {expected:expected, actual:actual});
   1077    }
   1078    expose(assert_regexp_match, "assert_regexp_match");
   1079 
   1080    function assert_class_string(object, class_string, description) {
   1081        assert_equals({}.toString.call(object), "[object " + class_string + "]",
   1082                      description);
   1083    }
   1084    expose(assert_class_string, "assert_class_string");
   1085 
   1086 
   1087    function _assert_own_property(name) {
   1088        return function(object, property_name, description)
   1089        {
   1090            assert(object.hasOwnProperty(property_name),
   1091                   name, description,
   1092                   "expected property ${p} missing", {p:property_name});
   1093        };
   1094    }
   1095    expose(_assert_own_property("assert_exists"), "assert_exists");
   1096    expose(_assert_own_property("assert_own_property"), "assert_own_property");
   1097 
   1098    function assert_not_exists(object, property_name, description)
   1099    {
   1100        assert(!object.hasOwnProperty(property_name),
   1101               "assert_not_exists", description,
   1102               "unexpected property ${p} found", {p:property_name});
   1103    }
   1104    expose(assert_not_exists, "assert_not_exists");
   1105 
   1106    function _assert_inherits(name) {
   1107        return function (object, property_name, description)
   1108        {
   1109            assert(typeof object === "object",
   1110                   name, description,
   1111                   "provided value is not an object");
   1112 
   1113            assert("hasOwnProperty" in object,
   1114                   name, description,
   1115                   "provided value is an object but has no hasOwnProperty method");
   1116 
   1117            assert(!object.hasOwnProperty(property_name),
   1118                   name, description,
   1119                   "property ${p} found on object expected in prototype chain",
   1120                   {p:property_name});
   1121 
   1122            assert(property_name in object,
   1123                   name, description,
   1124                   "property ${p} not found in prototype chain",
   1125                   {p:property_name});
   1126        };
   1127    }
   1128    expose(_assert_inherits("assert_inherits"), "assert_inherits");
   1129    expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
   1130 
   1131    function assert_readonly(object, property_name, description)
   1132    {
   1133         var initial_value = object[property_name];
   1134         try {
   1135             //Note that this can have side effects in the case where
   1136             //the property has PutForwards
   1137             object[property_name] = initial_value + "a"; //XXX use some other value here?
   1138             assert(same_value(object[property_name], initial_value),
   1139                    "assert_readonly", description,
   1140                    "changing property ${p} succeeded",
   1141                    {p:property_name});
   1142         } finally {
   1143             object[property_name] = initial_value;
   1144         }
   1145    }
   1146    expose(assert_readonly, "assert_readonly");
   1147 
   1148    function assert_throws(code, func, description)
   1149    {
   1150        try {
   1151            func.call(this);
   1152            assert(false, "assert_throws", description,
   1153                   "${func} did not throw", {func:func});
   1154        } catch (e) {
   1155            if (e instanceof AssertionError) {
   1156                throw e;
   1157            }
   1158            if (code === null) {
   1159                return;
   1160            }
   1161            if (typeof code === "object") {
   1162                assert(typeof e == "object" && "name" in e && e.name == code.name,
   1163                       "assert_throws", description,
   1164                       "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
   1165                                    {func:func, actual:e, actual_name:e.name,
   1166                                     expected:code,
   1167                                     expected_name:code.name});
   1168                return;
   1169            }
   1170 
   1171            var code_name_map = {
   1172                INDEX_SIZE_ERR: 'IndexSizeError',
   1173                HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
   1174                WRONG_DOCUMENT_ERR: 'WrongDocumentError',
   1175                INVALID_CHARACTER_ERR: 'InvalidCharacterError',
   1176                NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
   1177                NOT_FOUND_ERR: 'NotFoundError',
   1178                NOT_SUPPORTED_ERR: 'NotSupportedError',
   1179                INVALID_STATE_ERR: 'InvalidStateError',
   1180                SYNTAX_ERR: 'SyntaxError',
   1181                INVALID_MODIFICATION_ERR: 'InvalidModificationError',
   1182                NAMESPACE_ERR: 'NamespaceError',
   1183                INVALID_ACCESS_ERR: 'InvalidAccessError',
   1184                TYPE_MISMATCH_ERR: 'TypeMismatchError',
   1185                SECURITY_ERR: 'SecurityError',
   1186                NETWORK_ERR: 'NetworkError',
   1187                ABORT_ERR: 'AbortError',
   1188                URL_MISMATCH_ERR: 'URLMismatchError',
   1189                QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
   1190                TIMEOUT_ERR: 'TimeoutError',
   1191                INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
   1192                DATA_CLONE_ERR: 'DataCloneError'
   1193            };
   1194 
   1195            var name = code in code_name_map ? code_name_map[code] : code;
   1196 
   1197            var name_code_map = {
   1198                IndexSizeError: 1,
   1199                HierarchyRequestError: 3,
   1200                WrongDocumentError: 4,
   1201                InvalidCharacterError: 5,
   1202                NoModificationAllowedError: 7,
   1203                NotFoundError: 8,
   1204                NotSupportedError: 9,
   1205                InvalidStateError: 11,
   1206                SyntaxError: 12,
   1207                InvalidModificationError: 13,
   1208                NamespaceError: 14,
   1209                InvalidAccessError: 15,
   1210                TypeMismatchError: 17,
   1211                SecurityError: 18,
   1212                NetworkError: 19,
   1213                AbortError: 20,
   1214                URLMismatchError: 21,
   1215                QuotaExceededError: 22,
   1216                TimeoutError: 23,
   1217                InvalidNodeTypeError: 24,
   1218                DataCloneError: 25,
   1219 
   1220                EncodingError: 0,
   1221                NotReadableError: 0,
   1222                UnknownError: 0,
   1223                ConstraintError: 0,
   1224                DataError: 0,
   1225                TransactionInactiveError: 0,
   1226                ReadOnlyError: 0,
   1227                VersionError: 0,
   1228                OperationError: 0,
   1229            };
   1230 
   1231            if (!(name in name_code_map)) {
   1232                throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
   1233            }
   1234 
   1235            var required_props = { code: name_code_map[name] };
   1236 
   1237            if (required_props.code === 0 ||
   1238               (typeof e == "object" &&
   1239                "name" in e &&
   1240                e.name !== e.name.toUpperCase() &&
   1241                e.name !== "DOMException")) {
   1242                // New style exception: also test the name property.
   1243                required_props.name = name;
   1244            }
   1245 
   1246            //We'd like to test that e instanceof the appropriate interface,
   1247            //but we can't, because we don't know what window it was created
   1248            //in.  It might be an instanceof the appropriate interface on some
   1249            //unknown other window.  TODO: Work around this somehow?
   1250 
   1251            assert(typeof e == "object",
   1252                   "assert_throws", description,
   1253                   "${func} threw ${e} with type ${type}, not an object",
   1254                   {func:func, e:e, type:typeof e});
   1255 
   1256            for (var prop in required_props) {
   1257                assert(typeof e == "object" && prop in e && e[prop] == required_props[prop],
   1258                       "assert_throws", description,
   1259                       "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}",
   1260                       {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
   1261            }
   1262        }
   1263    }
   1264    expose(assert_throws, "assert_throws");
   1265 
   1266    function assert_unreached(description) {
   1267         assert(false, "assert_unreached", description,
   1268                "Reached unreachable code");
   1269    }
   1270    expose(assert_unreached, "assert_unreached");
   1271 
   1272    function assert_any(assert_func, actual, expected_array)
   1273    {
   1274        var args = [].slice.call(arguments, 3);
   1275        var errors = [];
   1276        var passed = false;
   1277        forEach(expected_array,
   1278                function(expected)
   1279                {
   1280                    try {
   1281                        assert_func.apply(this, [actual, expected].concat(args));
   1282                        passed = true;
   1283                    } catch (e) {
   1284                        errors.push(e.message);
   1285                    }
   1286                });
   1287        if (!passed) {
   1288            throw new AssertionError(errors.join("\n\n"));
   1289        }
   1290    }
   1291    expose(assert_any, "assert_any");
   1292 
   1293    function Test(name, properties)
   1294    {
   1295        if (tests.file_is_test && tests.tests.length) {
   1296            throw new Error("Tried to create a test with file_is_test");
   1297        }
   1298        this.name = name;
   1299 
   1300        this.phase = this.phases.INITIAL;
   1301 
   1302        this.status = this.NOTRUN;
   1303        this.timeout_id = null;
   1304        this.index = null;
   1305 
   1306        this.properties = properties;
   1307        var timeout = properties.timeout ? properties.timeout : settings.test_timeout;
   1308        if (timeout !== null) {
   1309            this.timeout_length = timeout * tests.timeout_multiplier;
   1310        } else {
   1311            this.timeout_length = null;
   1312        }
   1313 
   1314        this.message = null;
   1315        this.stack = null;
   1316 
   1317        this.steps = [];
   1318 
   1319        this.cleanup_callbacks = [];
   1320 
   1321        tests.push(this);
   1322    }
   1323 
   1324    Test.statuses = {
   1325        PASS:0,
   1326        FAIL:1,
   1327        TIMEOUT:2,
   1328        NOTRUN:3
   1329    };
   1330 
   1331    Test.prototype = merge({}, Test.statuses);
   1332 
   1333    Test.prototype.phases = {
   1334        INITIAL:0,
   1335        STARTED:1,
   1336        HAS_RESULT:2,
   1337        COMPLETE:3
   1338    };
   1339 
   1340    Test.prototype.structured_clone = function()
   1341    {
   1342        if (!this._structured_clone) {
   1343            var msg = this.message;
   1344            msg = msg ? String(msg) : msg;
   1345            this._structured_clone = merge({
   1346                name:String(this.name),
   1347                properties:merge({}, this.properties),
   1348            }, Test.statuses);
   1349        }
   1350        this._structured_clone.status = this.status;
   1351        this._structured_clone.message = this.message;
   1352        this._structured_clone.stack = this.stack;
   1353        this._structured_clone.index = this.index;
   1354        return this._structured_clone;
   1355    };
   1356 
   1357    Test.prototype.step = function(func, this_obj)
   1358    {
   1359        if (this.phase > this.phases.STARTED) {
   1360            return;
   1361        }
   1362        this.phase = this.phases.STARTED;
   1363        //If we don't get a result before the harness times out that will be a test timout
   1364        this.set_status(this.TIMEOUT, "Test timed out");
   1365 
   1366        tests.started = true;
   1367        tests.notify_test_state(this);
   1368 
   1369        if (this.timeout_id === null) {
   1370            this.set_timeout();
   1371        }
   1372 
   1373        this.steps.push(func);
   1374 
   1375        if (arguments.length === 1) {
   1376            this_obj = this;
   1377        }
   1378 
   1379        try {
   1380            return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
   1381        } catch (e) {
   1382            if (this.phase >= this.phases.HAS_RESULT) {
   1383                return;
   1384            }
   1385            var message = String((typeof e === "object" && e !== null) ? e.message : e);
   1386            var stack = e.stack ? e.stack : null;
   1387 
   1388            this.set_status(this.FAIL, message, stack);
   1389            this.phase = this.phases.HAS_RESULT;
   1390            this.done();
   1391        }
   1392    };
   1393 
   1394    Test.prototype.step_func = function(func, this_obj)
   1395    {
   1396        var test_this = this;
   1397 
   1398        if (arguments.length === 1) {
   1399            this_obj = test_this;
   1400        }
   1401 
   1402        return function()
   1403        {
   1404            return test_this.step.apply(test_this, [func, this_obj].concat(
   1405                Array.prototype.slice.call(arguments)));
   1406        };
   1407    };
   1408 
   1409    Test.prototype.step_func_done = function(func, this_obj)
   1410    {
   1411        var test_this = this;
   1412 
   1413        if (arguments.length === 1) {
   1414            this_obj = test_this;
   1415        }
   1416 
   1417        return function()
   1418        {
   1419            if (func) {
   1420                test_this.step.apply(test_this, [func, this_obj].concat(
   1421                    Array.prototype.slice.call(arguments)));
   1422            }
   1423            test_this.done();
   1424        };
   1425    };
   1426 
   1427    Test.prototype.unreached_func = function(description)
   1428    {
   1429        return this.step_func(function() {
   1430            assert_unreached(description);
   1431        });
   1432    };
   1433 
   1434    Test.prototype.step_timeout = function(f, timeout) {
   1435        var test_this = this;
   1436        var args = Array.prototype.slice.call(arguments, 2);
   1437        return setTimeout(this.step_func(function() {
   1438            return f.apply(test_this, args);
   1439        }, timeout * tests.timeout_multiplier));
   1440    }
   1441 
   1442    Test.prototype.add_cleanup = function(callback) {
   1443        this.cleanup_callbacks.push(callback);
   1444    };
   1445 
   1446    Test.prototype.force_timeout = function() {
   1447        this.set_status(this.TIMEOUT);
   1448        this.phase = this.phases.HAS_RESULT;
   1449    };
   1450 
   1451    Test.prototype.set_timeout = function()
   1452    {
   1453        if (this.timeout_length !== null) {
   1454            var this_obj = this;
   1455            this.timeout_id = setTimeout(function()
   1456                                         {
   1457                                             this_obj.timeout();
   1458                                         }, this.timeout_length);
   1459        }
   1460    };
   1461 
   1462    Test.prototype.set_status = function(status, message, stack)
   1463    {
   1464        this.status = status;
   1465        this.message = message;
   1466        this.stack = stack ? stack : null;
   1467    };
   1468 
   1469    Test.prototype.timeout = function()
   1470    {
   1471        this.timeout_id = null;
   1472        this.set_status(this.TIMEOUT, "Test timed out");
   1473        this.phase = this.phases.HAS_RESULT;
   1474        this.done();
   1475    };
   1476 
   1477    Test.prototype.done = function()
   1478    {
   1479        if (this.phase == this.phases.COMPLETE) {
   1480            return;
   1481        }
   1482 
   1483        if (this.phase <= this.phases.STARTED) {
   1484            this.set_status(this.PASS, null);
   1485        }
   1486 
   1487        this.phase = this.phases.COMPLETE;
   1488 
   1489        clearTimeout(this.timeout_id);
   1490        tests.result(this);
   1491        this.cleanup();
   1492    };
   1493 
   1494    Test.prototype.cleanup = function() {
   1495        forEach(this.cleanup_callbacks,
   1496                function(cleanup_callback) {
   1497                    cleanup_callback();
   1498                });
   1499    };
   1500 
   1501    /*
   1502     * A RemoteTest object mirrors a Test object on a remote worker. The
   1503     * associated RemoteWorker updates the RemoteTest object in response to
   1504     * received events. In turn, the RemoteTest object replicates these events
   1505     * on the local document. This allows listeners (test result reporting
   1506     * etc..) to transparently handle local and remote events.
   1507     */
   1508    function RemoteTest(clone) {
   1509        var this_obj = this;
   1510        Object.keys(clone).forEach(
   1511                function(key) {
   1512                    this_obj[key] = clone[key];
   1513                });
   1514        this.index = null;
   1515        this.phase = this.phases.INITIAL;
   1516        this.update_state_from(clone);
   1517        tests.push(this);
   1518    }
   1519 
   1520    RemoteTest.prototype.structured_clone = function() {
   1521        var clone = {};
   1522        Object.keys(this).forEach(
   1523                key => {
   1524                    if (typeof(this[key]) === "object") {
   1525                        clone[key] = merge({}, this[key]);
   1526                    } else {
   1527                        clone[key] = this[key];
   1528                    }
   1529                });
   1530        clone.phases = merge({}, this.phases);
   1531        return clone;
   1532    };
   1533 
   1534    RemoteTest.prototype.cleanup = function() {};
   1535    RemoteTest.prototype.phases = Test.prototype.phases;
   1536    RemoteTest.prototype.update_state_from = function(clone) {
   1537        this.status = clone.status;
   1538        this.message = clone.message;
   1539        this.stack = clone.stack;
   1540        if (this.phase === this.phases.INITIAL) {
   1541            this.phase = this.phases.STARTED;
   1542        }
   1543    };
   1544    RemoteTest.prototype.done = function() {
   1545        this.phase = this.phases.COMPLETE;
   1546    }
   1547 
   1548    /*
   1549     * A RemoteWorker listens for test events from a worker. These events are
   1550     * then used to construct and maintain RemoteTest objects that mirror the
   1551     * tests running on the remote worker.
   1552     */
   1553    function RemoteWorker(worker) {
   1554        this.running = true;
   1555        this.tests = new Array();
   1556 
   1557        var this_obj = this;
   1558        worker.onerror = function(error) { this_obj.worker_error(error); };
   1559 
   1560        var message_port;
   1561 
   1562        if (is_service_worker(worker)) {
   1563            if (window.MessageChannel) {
   1564                // The ServiceWorker's implicit MessagePort is currently not
   1565                // reliably accessible from the ServiceWorkerGlobalScope due to
   1566                // Blink setting MessageEvent.source to null for messages sent
   1567                // via ServiceWorker.postMessage(). Until that's resolved,
   1568                // create an explicit MessageChannel and pass one end to the
   1569                // worker.
   1570                var message_channel = new MessageChannel();
   1571                message_port = message_channel.port1;
   1572                message_port.start();
   1573                worker.postMessage({type: "connect"}, [message_channel.port2]);
   1574            } else {
   1575                // If MessageChannel is not available, then try the
   1576                // ServiceWorker.postMessage() approach using MessageEvent.source
   1577                // on the other end.
   1578                message_port = navigator.serviceWorker;
   1579                worker.postMessage({type: "connect"});
   1580            }
   1581        } else if (is_shared_worker(worker)) {
   1582            message_port = worker.port;
   1583        } else {
   1584            message_port = worker;
   1585        }
   1586 
   1587        // Keeping a reference to the worker until worker_done() is seen
   1588        // prevents the Worker object and its MessageChannel from going away
   1589        // before all the messages are dispatched.
   1590        this.worker = worker;
   1591 
   1592        message_port.onmessage =
   1593            function(message) {
   1594                if (this_obj.running && (message.data.type in this_obj.message_handlers)) {
   1595                    this_obj.message_handlers[message.data.type].call(this_obj, message.data);
   1596                }
   1597            };
   1598    }
   1599 
   1600    RemoteWorker.prototype.worker_error = function(error) {
   1601        var message = error.message || String(error);
   1602        var filename = (error.filename ? " " + error.filename: "");
   1603        // FIXME: Display worker error states separately from main document
   1604        // error state.
   1605        this.worker_done({
   1606            status: {
   1607                status: tests.status.ERROR,
   1608                message: "Error in worker" + filename + ": " + message,
   1609                stack: error.stack
   1610            }
   1611        });
   1612        error.preventDefault();
   1613    };
   1614 
   1615    RemoteWorker.prototype.test_state = function(data) {
   1616        var remote_test = this.tests[data.test.index];
   1617        if (!remote_test) {
   1618            remote_test = new RemoteTest(data.test);
   1619            this.tests[data.test.index] = remote_test;
   1620        }
   1621        remote_test.update_state_from(data.test);
   1622        tests.notify_test_state(remote_test);
   1623    };
   1624 
   1625    RemoteWorker.prototype.test_done = function(data) {
   1626        var remote_test = this.tests[data.test.index];
   1627        remote_test.update_state_from(data.test);
   1628        remote_test.done();
   1629        tests.result(remote_test);
   1630    };
   1631 
   1632    RemoteWorker.prototype.worker_done = function(data) {
   1633        if (tests.status.status === null &&
   1634            data.status.status !== data.status.OK) {
   1635            tests.status.status = data.status.status;
   1636            tests.status.message = data.status.message;
   1637            tests.status.stack = data.status.stack;
   1638        }
   1639        this.running = false;
   1640        this.worker = null;
   1641        if (tests.all_done()) {
   1642            tests.complete();
   1643        }
   1644    };
   1645 
   1646    RemoteWorker.prototype.message_handlers = {
   1647        test_state: RemoteWorker.prototype.test_state,
   1648        result: RemoteWorker.prototype.test_done,
   1649        complete: RemoteWorker.prototype.worker_done
   1650    };
   1651 
   1652    /*
   1653     * Harness
   1654     */
   1655 
   1656    function TestsStatus()
   1657    {
   1658        this.status = null;
   1659        this.message = null;
   1660        this.stack = null;
   1661    }
   1662 
   1663    TestsStatus.statuses = {
   1664        OK:0,
   1665        ERROR:1,
   1666        TIMEOUT:2
   1667    };
   1668 
   1669    TestsStatus.prototype = merge({}, TestsStatus.statuses);
   1670 
   1671    TestsStatus.prototype.structured_clone = function()
   1672    {
   1673        if (!this._structured_clone) {
   1674            var msg = this.message;
   1675            msg = msg ? String(msg) : msg;
   1676            this._structured_clone = merge({
   1677                status:this.status,
   1678                message:msg,
   1679                stack:this.stack
   1680            }, TestsStatus.statuses);
   1681        }
   1682        return this._structured_clone;
   1683    };
   1684 
   1685    function Tests()
   1686    {
   1687        this.tests = [];
   1688        this.num_pending = 0;
   1689 
   1690        this.phases = {
   1691            INITIAL:0,
   1692            SETUP:1,
   1693            HAVE_TESTS:2,
   1694            HAVE_RESULTS:3,
   1695            COMPLETE:4
   1696        };
   1697        this.phase = this.phases.INITIAL;
   1698 
   1699        this.properties = {};
   1700 
   1701        this.wait_for_finish = false;
   1702        this.processing_callbacks = false;
   1703 
   1704        this.allow_uncaught_exception = false;
   1705 
   1706        this.file_is_test = false;
   1707 
   1708        this.timeout_multiplier = 1;
   1709        this.timeout_length = test_environment.test_timeout();
   1710        this.timeout_id = null;
   1711 
   1712        this.start_callbacks = [];
   1713        this.test_state_callbacks = [];
   1714        this.test_done_callbacks = [];
   1715        this.all_done_callbacks = [];
   1716 
   1717        this.pending_workers = [];
   1718 
   1719        this.status = new TestsStatus();
   1720 
   1721        var this_obj = this;
   1722 
   1723        test_environment.add_on_loaded_callback(function() {
   1724            if (this_obj.all_done()) {
   1725                this_obj.complete();
   1726            }
   1727        });
   1728 
   1729        this.set_timeout();
   1730    }
   1731 
   1732    Tests.prototype.setup = function(func, properties)
   1733    {
   1734        if (this.phase >= this.phases.HAVE_RESULTS) {
   1735            return;
   1736        }
   1737 
   1738        if (this.phase < this.phases.SETUP) {
   1739            this.phase = this.phases.SETUP;
   1740        }
   1741 
   1742        this.properties = properties;
   1743 
   1744        for (var p in properties) {
   1745            if (properties.hasOwnProperty(p)) {
   1746                var value = properties[p];
   1747                if (p == "allow_uncaught_exception") {
   1748                    this.allow_uncaught_exception = value;
   1749                } else if (p == "explicit_done" && value) {
   1750                    this.wait_for_finish = true;
   1751                } else if (p == "explicit_timeout" && value) {
   1752                    this.timeout_length = null;
   1753                    if (this.timeout_id)
   1754                    {
   1755                        clearTimeout(this.timeout_id);
   1756                    }
   1757                } else if (p == "timeout_multiplier") {
   1758                    this.timeout_multiplier = value;
   1759                }
   1760            }
   1761        }
   1762 
   1763        if (func) {
   1764            try {
   1765                func();
   1766            } catch (e) {
   1767                this.status.status = this.status.ERROR;
   1768                this.status.message = String(e);
   1769                this.status.stack = e.stack ? e.stack : null;
   1770            }
   1771        }
   1772        this.set_timeout();
   1773    };
   1774 
   1775    Tests.prototype.set_file_is_test = function() {
   1776        if (this.tests.length > 0) {
   1777            throw new Error("Tried to set file as test after creating a test");
   1778        }
   1779        this.wait_for_finish = true;
   1780        this.file_is_test = true;
   1781        // Create the test, which will add it to the list of tests
   1782        async_test();
   1783    };
   1784 
   1785    Tests.prototype.set_timeout = function() {
   1786        var this_obj = this;
   1787        clearTimeout(this.timeout_id);
   1788        if (this.timeout_length !== null) {
   1789            this.timeout_id = setTimeout(function() {
   1790                                             this_obj.timeout();
   1791                                         }, this.timeout_length);
   1792        }
   1793    };
   1794 
   1795    Tests.prototype.timeout = function() {
   1796        if (this.status.status === null) {
   1797            this.status.status = this.status.TIMEOUT;
   1798        }
   1799        this.complete();
   1800    };
   1801 
   1802    Tests.prototype.end_wait = function()
   1803    {
   1804        this.wait_for_finish = false;
   1805        if (this.all_done()) {
   1806            this.complete();
   1807        }
   1808    };
   1809 
   1810    Tests.prototype.push = function(test)
   1811    {
   1812        if (this.phase < this.phases.HAVE_TESTS) {
   1813            this.start();
   1814        }
   1815        this.num_pending++;
   1816        test.index = this.tests.push(test);
   1817        this.notify_test_state(test);
   1818    };
   1819 
   1820    Tests.prototype.notify_test_state = function(test) {
   1821        var this_obj = this;
   1822        forEach(this.test_state_callbacks,
   1823                function(callback) {
   1824                    callback(test, this_obj);
   1825                });
   1826    };
   1827 
   1828    Tests.prototype.all_done = function() {
   1829        return (this.tests.length > 0 && test_environment.all_loaded &&
   1830                this.num_pending === 0 && !this.wait_for_finish &&
   1831                !this.processing_callbacks &&
   1832                !this.pending_workers.some(function(w) { return w.running; }));
   1833    };
   1834 
   1835    Tests.prototype.start = function() {
   1836        this.phase = this.phases.HAVE_TESTS;
   1837        this.notify_start();
   1838    };
   1839 
   1840    Tests.prototype.notify_start = function() {
   1841        var this_obj = this;
   1842        forEach (this.start_callbacks,
   1843                 function(callback)
   1844                 {
   1845                     callback(this_obj.properties);
   1846                 });
   1847    };
   1848 
   1849    Tests.prototype.result = function(test)
   1850    {
   1851        if (this.phase > this.phases.HAVE_RESULTS) {
   1852            return;
   1853        }
   1854        this.phase = this.phases.HAVE_RESULTS;
   1855        this.num_pending--;
   1856        this.notify_result(test);
   1857    };
   1858 
   1859    Tests.prototype.notify_result = function(test) {
   1860        var this_obj = this;
   1861        this.processing_callbacks = true;
   1862        forEach(this.test_done_callbacks,
   1863                function(callback)
   1864                {
   1865                    callback(test, this_obj);
   1866                });
   1867        this.processing_callbacks = false;
   1868        if (this_obj.all_done()) {
   1869            this_obj.complete();
   1870        }
   1871    };
   1872 
   1873    Tests.prototype.complete = function() {
   1874        if (this.phase === this.phases.COMPLETE) {
   1875            return;
   1876        }
   1877        this.phase = this.phases.COMPLETE;
   1878        var this_obj = this;
   1879        this.tests.forEach(
   1880            function(x)
   1881            {
   1882                if (x.phase < x.phases.COMPLETE) {
   1883                    this_obj.notify_result(x);
   1884                    x.cleanup();
   1885                    x.phase = x.phases.COMPLETE;
   1886                }
   1887            }
   1888        );
   1889        this.notify_complete();
   1890    };
   1891 
   1892    Tests.prototype.notify_complete = function() {
   1893        var this_obj = this;
   1894        if (this.status.status === null) {
   1895            this.status.status = this.status.OK;
   1896        }
   1897 
   1898        forEach (this.all_done_callbacks,
   1899                 function(callback)
   1900                 {
   1901                     callback(this_obj.tests, this_obj.status);
   1902                 });
   1903    };
   1904 
   1905    Tests.prototype.fetch_tests_from_worker = function(worker) {
   1906        if (this.phase >= this.phases.COMPLETE) {
   1907            return;
   1908        }
   1909 
   1910        this.pending_workers.push(new RemoteWorker(worker));
   1911    };
   1912 
   1913    function fetch_tests_from_worker(port) {
   1914        tests.fetch_tests_from_worker(port);
   1915    }
   1916    expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
   1917 
   1918    function timeout() {
   1919        if (tests.timeout_length === null) {
   1920            tests.timeout();
   1921        }
   1922    }
   1923    expose(timeout, 'timeout');
   1924 
   1925    function add_start_callback(callback) {
   1926        tests.start_callbacks.push(callback);
   1927    }
   1928 
   1929    function add_test_state_callback(callback) {
   1930        tests.test_state_callbacks.push(callback);
   1931    }
   1932 
   1933    function add_result_callback(callback) {
   1934        tests.test_done_callbacks.push(callback);
   1935    }
   1936 
   1937    function add_completion_callback(callback) {
   1938        tests.all_done_callbacks.push(callback);
   1939    }
   1940 
   1941    expose(add_start_callback, 'add_start_callback');
   1942    expose(add_test_state_callback, 'add_test_state_callback');
   1943    expose(add_result_callback, 'add_result_callback');
   1944    expose(add_completion_callback, 'add_completion_callback');
   1945 
   1946    function remove(array, item) {
   1947        var index = array.indexOf(item);
   1948        if (index > -1) {
   1949            array.splice(index, 1);
   1950        }
   1951    }
   1952 
   1953    function remove_start_callback(callback) {
   1954        remove(tests.start_callbacks, callback);
   1955    }
   1956 
   1957    function remove_test_state_callback(callback) {
   1958        remove(tests.test_state_callbacks, callback);
   1959    }
   1960 
   1961    function remove_result_callback(callback) {
   1962        remove(tests.test_done_callbacks, callback);
   1963    }
   1964 
   1965    function remove_completion_callback(callback) {
   1966       remove(tests.all_done_callbacks, callback);
   1967    }
   1968 
   1969    /*
   1970     * Output listener
   1971    */
   1972 
   1973    function Output() {
   1974        this.output_document = document;
   1975        this.output_node = null;
   1976        this.enabled = settings.output;
   1977        this.phase = this.INITIAL;
   1978    }
   1979 
   1980    Output.prototype.INITIAL = 0;
   1981    Output.prototype.STARTED = 1;
   1982    Output.prototype.HAVE_RESULTS = 2;
   1983    Output.prototype.COMPLETE = 3;
   1984 
   1985    Output.prototype.setup = function(properties) {
   1986        if (this.phase > this.INITIAL) {
   1987            return;
   1988        }
   1989 
   1990        //If output is disabled in testharnessreport.js the test shouldn't be
   1991        //able to override that
   1992        this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
   1993                                        properties.output : settings.output);
   1994    };
   1995 
   1996    Output.prototype.init = function(properties) {
   1997        if (this.phase >= this.STARTED) {
   1998            return;
   1999        }
   2000        if (properties.output_document) {
   2001            this.output_document = properties.output_document;
   2002        } else {
   2003            this.output_document = document;
   2004        }
   2005        this.phase = this.STARTED;
   2006    };
   2007 
   2008    Output.prototype.resolve_log = function() {
   2009        var output_document;
   2010        if (typeof this.output_document === "function") {
   2011            output_document = this.output_document.apply(undefined);
   2012        } else {
   2013            output_document = this.output_document;
   2014        }
   2015        if (!output_document) {
   2016            return;
   2017        }
   2018        var node = output_document.getElementById("log");
   2019        if (!node) {
   2020            if (!document.body || document.readyState == "loading") {
   2021                return;
   2022            }
   2023            node = output_document.createElement("div");
   2024            node.id = "log";
   2025            output_document.body.appendChild(node);
   2026        }
   2027        this.output_document = output_document;
   2028        this.output_node = node;
   2029    };
   2030 
   2031    Output.prototype.show_status = function() {
   2032        if (this.phase < this.STARTED) {
   2033            this.init();
   2034        }
   2035        if (!this.enabled) {
   2036            return;
   2037        }
   2038        if (this.phase < this.HAVE_RESULTS) {
   2039            this.resolve_log();
   2040            this.phase = this.HAVE_RESULTS;
   2041        }
   2042        var done_count = tests.tests.length - tests.num_pending;
   2043        if (this.output_node) {
   2044            if (done_count < 100 ||
   2045                (done_count < 1000 && done_count % 100 === 0) ||
   2046                done_count % 1000 === 0) {
   2047                this.output_node.textContent = "Running, " +
   2048                    done_count + " complete, " +
   2049                    tests.num_pending + " remain";
   2050            }
   2051        }
   2052    };
   2053 
   2054    Output.prototype.show_results = function (tests, harness_status) {
   2055        if (this.phase >= this.COMPLETE) {
   2056            return;
   2057        }
   2058        if (!this.enabled) {
   2059            return;
   2060        }
   2061        if (!this.output_node) {
   2062            this.resolve_log();
   2063        }
   2064        this.phase = this.COMPLETE;
   2065 
   2066        var log = this.output_node;
   2067        if (!log) {
   2068            return;
   2069        }
   2070        var output_document = this.output_document;
   2071 
   2072        while (log.lastChild) {
   2073            log.removeChild(log.lastChild);
   2074        }
   2075 
   2076        var harness_url = get_harness_url();
   2077        if (harness_url !== null) {
   2078            var stylesheet = output_document.createElementNS(xhtml_ns, "link");
   2079            stylesheet.setAttribute("rel", "stylesheet");
   2080            stylesheet.setAttribute("href", harness_url + "testharness.css");
   2081            var heads = output_document.getElementsByTagName("head");
   2082            if (heads.length) {
   2083                heads[0].appendChild(stylesheet);
   2084            }
   2085        }
   2086 
   2087        var status_text_harness = {};
   2088        status_text_harness[harness_status.OK] = "OK";
   2089        status_text_harness[harness_status.ERROR] = "Error";
   2090        status_text_harness[harness_status.TIMEOUT] = "Timeout";
   2091 
   2092        var status_text = {};
   2093        status_text[Test.prototype.PASS] = "Pass";
   2094        status_text[Test.prototype.FAIL] = "Fail";
   2095        status_text[Test.prototype.TIMEOUT] = "Timeout";
   2096        status_text[Test.prototype.NOTRUN] = "Not Run";
   2097 
   2098        var status_number = {};
   2099        forEach(tests,
   2100                function(test) {
   2101                    var status = status_text[test.status];
   2102                    if (status_number.hasOwnProperty(status)) {
   2103                        status_number[status] += 1;
   2104                    } else {
   2105                        status_number[status] = 1;
   2106                    }
   2107                });
   2108 
   2109        function status_class(status)
   2110        {
   2111            return status.replace(/\s/g, '').toLowerCase();
   2112        }
   2113 
   2114        var summary_template = ["section", {"id":"summary"},
   2115                                ["h2", {}, "Summary"],
   2116                                function()
   2117                                {
   2118 
   2119                                    var status = status_text_harness[harness_status.status];
   2120                                    var rv = [["section", {},
   2121                                               ["p", {},
   2122                                                "Harness status: ",
   2123                                                ["span", {"class":status_class(status)},
   2124                                                 status
   2125                                                ],
   2126                                               ]
   2127                                              ]];
   2128 
   2129                                    if (harness_status.status === harness_status.ERROR) {
   2130                                        rv[0].push(["pre", {}, harness_status.message]);
   2131                                        if (harness_status.stack) {
   2132                                            rv[0].push(["pre", {}, harness_status.stack]);
   2133                                        }
   2134                                    }
   2135                                    return rv;
   2136                                },
   2137                                ["p", {}, "Found ${num_tests} tests"],
   2138                                function() {
   2139                                    var rv = [["div", {}]];
   2140                                    var i = 0;
   2141                                    while (status_text.hasOwnProperty(i)) {
   2142                                        if (status_number.hasOwnProperty(status_text[i])) {
   2143                                            var status = status_text[i];
   2144                                            rv[0].push(["div", {"class":status_class(status)},
   2145                                                        ["label", {},
   2146                                                         ["input", {type:"checkbox", checked:"checked"}],
   2147                                                         status_number[status] + " " + status]]);
   2148                                        }
   2149                                        i++;
   2150                                    }
   2151                                    return rv;
   2152                                },
   2153                               ];
   2154 
   2155        log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
   2156 
   2157        forEach(output_document.querySelectorAll("section#summary label"),
   2158                function(element)
   2159                {
   2160                    on_event(element, "click",
   2161                             function(e)
   2162                             {
   2163                                 if (output_document.getElementById("results") === null) {
   2164                                     e.preventDefault();
   2165                                     return;
   2166                                 }
   2167                                 var result_class = element.parentNode.getAttribute("class");
   2168                                 var style_element = output_document.querySelector("style#hide-" + result_class);
   2169                                 var input_element = element.querySelector("input");
   2170                                 if (!style_element && !input_element.checked) {
   2171                                     style_element = output_document.createElementNS(xhtml_ns, "style");
   2172                                     style_element.id = "hide-" + result_class;
   2173                                     style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}";
   2174                                     output_document.body.appendChild(style_element);
   2175                                 } else if (style_element && input_element.checked) {
   2176                                     style_element.remove();
   2177                                 }
   2178                             });
   2179                });
   2180 
   2181        // This use of innerHTML plus manual escaping is not recommended in
   2182        // general, but is necessary here for performance.  Using textContent
   2183        // on each individual <td> adds tens of seconds of execution time for
   2184        // large test suites (tens of thousands of tests).
   2185        function escape_html(s)
   2186        {
   2187            return s.replace(/\&/g, "&amp;")
   2188                .replace(/</g, "&lt;")
   2189                .replace(/"/g, "&quot;")
   2190                .replace(/'/g, "&#39;");
   2191        }
   2192 
   2193        function has_assertions()
   2194        {
   2195            for (var i = 0; i < tests.length; i++) {
   2196                if (tests[i].properties.hasOwnProperty("assert")) {
   2197                    return true;
   2198                }
   2199            }
   2200            return false;
   2201        }
   2202 
   2203        function get_assertion(test)
   2204        {
   2205            if (test.properties.hasOwnProperty("assert")) {
   2206                if (Array.isArray(test.properties.assert)) {
   2207                    return test.properties.assert.join(' ');
   2208                }
   2209                return test.properties.assert;
   2210            }
   2211            return '';
   2212        }
   2213 
   2214        log.appendChild(document.createElementNS(xhtml_ns, "section"));
   2215        var assertions = has_assertions();
   2216        var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
   2217            "<thead><tr><th>Result</th><th>Test Name</th>" +
   2218            (assertions ? "<th>Assertion</th>" : "") +
   2219            "<th>Message</th></tr></thead>" +
   2220            "<tbody>";
   2221        for (var i = 0; i < tests.length; i++) {
   2222            html += '<tr class="' +
   2223                escape_html(status_class(status_text[tests[i].status])) +
   2224                '"><td>' +
   2225                escape_html(status_text[tests[i].status]) +
   2226                "</td><td>" +
   2227                escape_html(tests[i].name) +
   2228                "</td><td>" +
   2229                (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") +
   2230                escape_html(tests[i].message ? tests[i].message : " ") +
   2231                (tests[i].stack ? "<pre>" +
   2232                 escape_html(tests[i].stack) +
   2233                 "</pre>": "") +
   2234                "</td></tr>";
   2235        }
   2236        html += "</tbody></table>";
   2237        try {
   2238            log.lastChild.innerHTML = html;
   2239        } catch (e) {
   2240            log.appendChild(document.createElementNS(xhtml_ns, "p"))
   2241               .textContent = "Setting innerHTML for the log threw an exception.";
   2242            log.appendChild(document.createElementNS(xhtml_ns, "pre"))
   2243               .textContent = html;
   2244        }
   2245    };
   2246 
   2247    /*
   2248     * Template code
   2249     *
   2250     * A template is just a javascript structure. An element is represented as:
   2251     *
   2252     * [tag_name, {attr_name:attr_value}, child1, child2]
   2253     *
   2254     * the children can either be strings (which act like text nodes), other templates or
   2255     * functions (see below)
   2256     *
   2257     * A text node is represented as
   2258     *
   2259     * ["{text}", value]
   2260     *
   2261     * String values have a simple substitution syntax; ${foo} represents a variable foo.
   2262     *
   2263     * It is possible to embed logic in templates by using a function in a place where a
   2264     * node would usually go. The function must either return part of a template or null.
   2265     *
   2266     * In cases where a set of nodes are required as output rather than a single node
   2267     * with children it is possible to just use a list
   2268     * [node1, node2, node3]
   2269     *
   2270     * Usage:
   2271     *
   2272     * render(template, substitutions) - take a template and an object mapping
   2273     * variable names to parameters and return either a DOM node or a list of DOM nodes
   2274     *
   2275     * substitute(template, substitutions) - take a template and variable mapping object,
   2276     * make the variable substitutions and return the substituted template
   2277     *
   2278     */
   2279 
   2280    function is_single_node(template)
   2281    {
   2282        return typeof template[0] === "string";
   2283    }
   2284 
   2285    function substitute(template, substitutions)
   2286    {
   2287        if (typeof template === "function") {
   2288            var replacement = template(substitutions);
   2289            if (!replacement) {
   2290                return null;
   2291            }
   2292 
   2293            return substitute(replacement, substitutions);
   2294        }
   2295 
   2296        if (is_single_node(template)) {
   2297            return substitute_single(template, substitutions);
   2298        }
   2299 
   2300        return filter(map(template, function(x) {
   2301                              return substitute(x, substitutions);
   2302                          }), function(x) {return x !== null;});
   2303    }
   2304 
   2305    function substitute_single(template, substitutions)
   2306    {
   2307        var substitution_re = /\$\{([^ }]*)\}/g;
   2308 
   2309        function do_substitution(input) {
   2310            var components = input.split(substitution_re);
   2311            var rv = [];
   2312            for (var i = 0; i < components.length; i += 2) {
   2313                rv.push(components[i]);
   2314                if (components[i + 1]) {
   2315                    rv.push(String(substitutions[components[i + 1]]));
   2316                }
   2317            }
   2318            return rv;
   2319        }
   2320 
   2321        function substitute_attrs(attrs, rv)
   2322        {
   2323            rv[1] = {};
   2324            for (var name in template[1]) {
   2325                if (attrs.hasOwnProperty(name)) {
   2326                    var new_name = do_substitution(name).join("");
   2327                    var new_value = do_substitution(attrs[name]).join("");
   2328                    rv[1][new_name] = new_value;
   2329                }
   2330            }
   2331        }
   2332 
   2333        function substitute_children(children, rv)
   2334        {
   2335            for (var i = 0; i < children.length; i++) {
   2336                if (children[i] instanceof Object) {
   2337                    var replacement = substitute(children[i], substitutions);
   2338                    if (replacement !== null) {
   2339                        if (is_single_node(replacement)) {
   2340                            rv.push(replacement);
   2341                        } else {
   2342                            extend(rv, replacement);
   2343                        }
   2344                    }
   2345                } else {
   2346                    extend(rv, do_substitution(String(children[i])));
   2347                }
   2348            }
   2349            return rv;
   2350        }
   2351 
   2352        var rv = [];
   2353        rv.push(do_substitution(String(template[0])).join(""));
   2354 
   2355        if (template[0] === "{text}") {
   2356            substitute_children(template.slice(1), rv);
   2357        } else {
   2358            substitute_attrs(template[1], rv);
   2359            substitute_children(template.slice(2), rv);
   2360        }
   2361 
   2362        return rv;
   2363    }
   2364 
   2365    function make_dom_single(template, doc)
   2366    {
   2367        var output_document = doc || document;
   2368        var element;
   2369        if (template[0] === "{text}") {
   2370            element = output_document.createTextNode("");
   2371            for (var i = 1; i < template.length; i++) {
   2372                element.data += template[i];
   2373            }
   2374        } else {
   2375            element = output_document.createElementNS(xhtml_ns, template[0]);
   2376            for (var name in template[1]) {
   2377                if (template[1].hasOwnProperty(name)) {
   2378                    element.setAttribute(name, template[1][name]);
   2379                }
   2380            }
   2381            for (var i = 2; i < template.length; i++) {
   2382                if (template[i] instanceof Object) {
   2383                    var sub_element = make_dom(template[i]);
   2384                    element.appendChild(sub_element);
   2385                } else {
   2386                    var text_node = output_document.createTextNode(template[i]);
   2387                    element.appendChild(text_node);
   2388                }
   2389            }
   2390        }
   2391 
   2392        return element;
   2393    }
   2394 
   2395    function make_dom(template, substitutions, output_document)
   2396    {
   2397        if (is_single_node(template)) {
   2398            return make_dom_single(template, output_document);
   2399        }
   2400 
   2401        return map(template, function(x) {
   2402                       return make_dom_single(x, output_document);
   2403                   });
   2404    }
   2405 
   2406    function render(template, substitutions, output_document)
   2407    {
   2408        return make_dom(substitute(template, substitutions), output_document);
   2409    }
   2410 
   2411    /*
   2412     * Utility funcions
   2413     */
   2414    function assert(expected_true, function_name, description, error, substitutions)
   2415    {
   2416        if (tests.tests.length === 0) {
   2417            tests.set_file_is_test();
   2418        }
   2419        if (expected_true !== true) {
   2420            var msg = make_message(function_name, description,
   2421                                   error, substitutions);
   2422            throw new AssertionError(msg);
   2423        }
   2424    }
   2425 
   2426    function AssertionError(message)
   2427    {
   2428        this.message = message;
   2429        this.stack = this.get_stack();
   2430    }
   2431 
   2432    AssertionError.prototype = Object.create(Error.prototype);
   2433 
   2434    AssertionError.prototype.get_stack = function() {
   2435        var stack = new Error().stack;
   2436        // IE11 does not initialize 'Error.stack' until the object is thrown.
   2437        if (!stack) {
   2438            try {
   2439                throw new Error();
   2440            } catch (e) {
   2441                stack = e.stack;
   2442            }
   2443        }
   2444 
   2445        var lines = stack.split("\n");
   2446 
   2447        // Create a pattern to match stack frames originating within testharness.js.  These include the
   2448        // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
   2449        var re = new RegExp((get_script_url() || "\\btestharness.js") + ":\\d+:\\d+");
   2450 
   2451        // Some browsers include a preamble that specifies the type of the error object.  Skip this by
   2452        // advancing until we find the first stack frame originating from testharness.js.
   2453        var i = 0;
   2454        while (!re.test(lines[i]) && i < lines.length) {
   2455            i++;
   2456        }
   2457 
   2458        // Then skip the top frames originating from testharness.js to begin the stack at the test code.
   2459        while (re.test(lines[i]) && i < lines.length) {
   2460            i++;
   2461        }
   2462 
   2463        // Paranoid check that we didn't skip all frames.  If so, return the original stack unmodified.
   2464        if (i >= lines.length) {
   2465            return stack;
   2466        }
   2467 
   2468        return lines.slice(i).join("\n");
   2469    }
   2470 
   2471    function make_message(function_name, description, error, substitutions)
   2472    {
   2473        for (var p in substitutions) {
   2474            if (substitutions.hasOwnProperty(p)) {
   2475                substitutions[p] = format_value(substitutions[p]);
   2476            }
   2477        }
   2478        var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
   2479                                   merge({function_name:function_name,
   2480                                          description:(description?description + " ":"")},
   2481                                          substitutions));
   2482        return node_form.slice(1).join("");
   2483    }
   2484 
   2485    function filter(array, callable, thisObj) {
   2486        var rv = [];
   2487        for (var i = 0; i < array.length; i++) {
   2488            if (array.hasOwnProperty(i)) {
   2489                var pass = callable.call(thisObj, array[i], i, array);
   2490                if (pass) {
   2491                    rv.push(array[i]);
   2492                }
   2493            }
   2494        }
   2495        return rv;
   2496    }
   2497 
   2498    function map(array, callable, thisObj)
   2499    {
   2500        var rv = [];
   2501        rv.length = array.length;
   2502        for (var i = 0; i < array.length; i++) {
   2503            if (array.hasOwnProperty(i)) {
   2504                rv[i] = callable.call(thisObj, array[i], i, array);
   2505            }
   2506        }
   2507        return rv;
   2508    }
   2509 
   2510    function extend(array, items)
   2511    {
   2512        Array.prototype.push.apply(array, items);
   2513    }
   2514 
   2515    function forEach(array, callback, thisObj)
   2516    {
   2517        for (var i = 0; i < array.length; i++) {
   2518            if (array.hasOwnProperty(i)) {
   2519                callback.call(thisObj, array[i], i, array);
   2520            }
   2521        }
   2522    }
   2523 
   2524    function merge(a,b)
   2525    {
   2526        var rv = {};
   2527        var p;
   2528        for (p in a) {
   2529            rv[p] = a[p];
   2530        }
   2531        for (p in b) {
   2532            rv[p] = b[p];
   2533        }
   2534        return rv;
   2535    }
   2536 
   2537    function expose(object, name)
   2538    {
   2539        var components = name.split(".");
   2540        var target = test_environment.global_scope();
   2541        for (var i = 0; i < components.length - 1; i++) {
   2542            if (!(components[i] in target)) {
   2543                target[components[i]] = {};
   2544            }
   2545            target = target[components[i]];
   2546        }
   2547        target[components[components.length - 1]] = object;
   2548    }
   2549 
   2550    function is_same_origin(w) {
   2551        try {
   2552            'random_prop' in w;
   2553            return true;
   2554        } catch (e) {
   2555            return false;
   2556        }
   2557    }
   2558 
   2559    /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
   2560    function get_script_url()
   2561    {
   2562        if (!('document' in self)) {
   2563            return undefined;
   2564        }
   2565 
   2566        var scripts = document.getElementsByTagName("script");
   2567        for (var i = 0; i < scripts.length; i++) {
   2568            var src;
   2569            if (scripts[i].src) {
   2570                src = scripts[i].src;
   2571            } else if (scripts[i].href) {
   2572                //SVG case
   2573                src = scripts[i].href.baseVal;
   2574            }
   2575 
   2576            var matches = src && src.match(/^(.*\/|)testharness\.js$/);
   2577            if (matches) {
   2578                return src;
   2579            }
   2580        }
   2581        return undefined;
   2582    }
   2583 
   2584    /** Returns the URL path at which the files for testharness.js are assumed to reside (e.g., '/resources/').
   2585        The path is derived from inspecting the 'src' of the <script> tag that included 'testharness.js'. */
   2586    function get_harness_url()
   2587    {
   2588        var script_url = get_script_url();
   2589 
   2590        // Exclude the 'testharness.js' file from the returned path, but '+ 1' to include the trailing slash.
   2591        return script_url ? script_url.slice(0, script_url.lastIndexOf('/') + 1) : undefined;
   2592    }
   2593 
   2594    function supports_post_message(w)
   2595    {
   2596        var supports;
   2597        var type;
   2598        // Given IE implements postMessage across nested iframes but not across
   2599        // windows or tabs, you can't infer cross-origin communication from the presence
   2600        // of postMessage on the current window object only.
   2601        //
   2602        // Touching the postMessage prop on a window can throw if the window is
   2603        // not from the same origin AND post message is not supported in that
   2604        // browser. So just doing an existence test here won't do, you also need
   2605        // to wrap it in a try..cacth block.
   2606        try {
   2607            type = typeof w.postMessage;
   2608            if (type === "function") {
   2609                supports = true;
   2610            }
   2611 
   2612            // IE8 supports postMessage, but implements it as a host object which
   2613            // returns "object" as its `typeof`.
   2614            else if (type === "object") {
   2615                supports = true;
   2616            }
   2617 
   2618            // This is the case where postMessage isn't supported AND accessing a
   2619            // window property across origins does NOT throw (e.g. old Safari browser).
   2620            else {
   2621                supports = false;
   2622            }
   2623        } catch (e) {
   2624            // This is the case where postMessage isn't supported AND accessing a
   2625            // window property across origins throws (e.g. old Firefox browser).
   2626            supports = false;
   2627        }
   2628        return supports;
   2629    }
   2630 
   2631    /**
   2632     * Setup globals
   2633     */
   2634 
   2635    var tests = new Tests();
   2636 
   2637    addEventListener("error", function(e) {
   2638        if (tests.file_is_test) {
   2639            var test = tests.tests[0];
   2640            if (test.phase >= test.phases.HAS_RESULT) {
   2641                return;
   2642            }
   2643            test.set_status(test.FAIL, e.message, e.stack);
   2644            test.phase = test.phases.HAS_RESULT;
   2645            test.done();
   2646            done();
   2647        } else if (!tests.allow_uncaught_exception) {
   2648            tests.status.status = tests.status.ERROR;
   2649            tests.status.message = e.message;
   2650            tests.status.stack = e.stack;
   2651        }
   2652    });
   2653 
   2654    test_environment.on_tests_ready();
   2655 
   2656 })();
   2657 // vim: set expandtab shiftwidth=4 tabstop=4: