• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*global self*/
2/*jshint latedef: nofunc*/
3/*
4Distributed under both the W3C Test Suite License [1] and the W3C
53-clause BSD License [2]. To contribute to a W3C Test Suite, see the
6policies 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: https://web-platform-tests.org/writing-tests/testharness-api.html
14 * (../docs/_writing-tests/testharness-api.md) */
15
16(function (global_scope)
17{
18    var debug = false;
19    // default timeout is 10 seconds, test can override if needed
20    var settings = {
21        output:true,
22        harness_timeout:{
23            "normal":10000,
24            "long":60000
25        },
26        test_timeout:null,
27        message_events: ["start", "test_state", "result", "completion"]
28    };
29
30    var xhtml_ns = "http://www.w3.org/1999/xhtml";
31
32    /*
33     * TestEnvironment is an abstraction for the environment in which the test
34     * harness is used. Each implementation of a test environment has to provide
35     * the following interface:
36     *
37     * interface TestEnvironment {
38     *   // Invoked after the global 'tests' object has been created and it's
39     *   // safe to call add_*_callback() to register event handlers.
40     *   void on_tests_ready();
41     *
42     *   // Invoked after setup() has been called to notify the test environment
43     *   // of changes to the test harness properties.
44     *   void on_new_harness_properties(object properties);
45     *
46     *   // Should return a new unique default test name.
47     *   DOMString next_default_test_name();
48     *
49     *   // Should return the test harness timeout duration in milliseconds.
50     *   float test_timeout();
51     * };
52     */
53
54    /*
55     * A test environment with a DOM. The global object is 'window'. By default
56     * test results are displayed in a table. Any parent windows receive
57     * callbacks or messages via postMessage() when test events occur. See
58     * apisample11.html and apisample12.html.
59     */
60    function WindowTestEnvironment() {
61        this.name_counter = 0;
62        this.window_cache = null;
63        this.output_handler = null;
64        this.all_loaded = false;
65        var this_obj = this;
66        this.message_events = [];
67        this.dispatched_messages = [];
68
69        this.message_functions = {
70            start: [add_start_callback, remove_start_callback,
71                    function (properties) {
72                        this_obj._dispatch("start_callback", [properties],
73                                           {type: "start", properties: properties});
74                    }],
75
76            test_state: [add_test_state_callback, remove_test_state_callback,
77                         function(test) {
78                             this_obj._dispatch("test_state_callback", [test],
79                                                {type: "test_state",
80                                                 test: test.structured_clone()});
81                         }],
82            result: [add_result_callback, remove_result_callback,
83                     function (test) {
84                         this_obj.output_handler.show_status();
85                         this_obj._dispatch("result_callback", [test],
86                                            {type: "result",
87                                             test: test.structured_clone()});
88                     }],
89            completion: [add_completion_callback, remove_completion_callback,
90                         function (tests, harness_status) {
91                             var cloned_tests = map(tests, function(test) {
92                                 return test.structured_clone();
93                             });
94                             this_obj._dispatch("completion_callback", [tests, harness_status],
95                                                {type: "complete",
96                                                 tests: cloned_tests,
97                                                 status: harness_status.structured_clone()});
98                         }]
99        }
100
101        on_event(window, 'load', function() {
102            this_obj.all_loaded = true;
103        });
104
105        on_event(window, 'message', function(event) {
106            if (event.data && event.data.type === "getmessages" && event.source) {
107                // A window can post "getmessages" to receive a duplicate of every
108                // message posted by this environment so far. This allows subscribers
109                // from fetch_tests_from_window to 'catch up' to the current state of
110                // this environment.
111                for (var i = 0; i < this_obj.dispatched_messages.length; ++i)
112                {
113                    event.source.postMessage(this_obj.dispatched_messages[i], "*");
114                }
115            }
116        });
117    }
118
119    WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
120        this.dispatched_messages.push(message_arg);
121        this._forEach_windows(
122                function(w, same_origin) {
123                    if (same_origin) {
124                        try {
125                            var has_selector = selector in w;
126                        } catch(e) {
127                            // If document.domain was set at some point same_origin can be
128                            // wrong and the above will fail.
129                            has_selector = false;
130                        }
131                        if (has_selector) {
132                            try {
133                                w[selector].apply(undefined, callback_args);
134                            } catch (e) {
135                                if (debug) {
136                                    throw e;
137                                }
138                            }
139                        }
140                    }
141                    if (supports_post_message(w) && w !== self) {
142                        w.postMessage(message_arg, "*");
143                    }
144                });
145    };
146
147    WindowTestEnvironment.prototype._forEach_windows = function(callback) {
148        // Iterate over the windows [self ... top, opener]. The callback is passed
149        // two objects, the first one is the window object itself, the second one
150        // is a boolean indicating whether or not it's on the same origin as the
151        // current window.
152        var cache = this.window_cache;
153        if (!cache) {
154            cache = [[self, true]];
155            var w = self;
156            var i = 0;
157            var so;
158            while (w != w.parent) {
159                w = w.parent;
160                so = is_same_origin(w);
161                cache.push([w, so]);
162                i++;
163            }
164            w = window.opener;
165            if (w) {
166                cache.push([w, is_same_origin(w)]);
167            }
168            this.window_cache = cache;
169        }
170
171        forEach(cache,
172                function(a) {
173                    callback.apply(null, a);
174                });
175    };
176
177    WindowTestEnvironment.prototype.on_tests_ready = function() {
178        var output = new Output();
179        this.output_handler = output;
180
181        var this_obj = this;
182
183        add_start_callback(function (properties) {
184            this_obj.output_handler.init(properties);
185        });
186
187        add_test_state_callback(function(test) {
188            this_obj.output_handler.show_status();
189        });
190
191        add_result_callback(function (test) {
192            this_obj.output_handler.show_status();
193        });
194
195        add_completion_callback(function (tests, harness_status) {
196            this_obj.output_handler.show_results(tests, harness_status);
197        });
198        this.setup_messages(settings.message_events);
199    };
200
201    WindowTestEnvironment.prototype.setup_messages = function(new_events) {
202        var this_obj = this;
203        forEach(settings.message_events, function(x) {
204            var current_dispatch = this_obj.message_events.indexOf(x) !== -1;
205            var new_dispatch = new_events.indexOf(x) !== -1;
206            if (!current_dispatch && new_dispatch) {
207                this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
208            } else if (current_dispatch && !new_dispatch) {
209                this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
210            }
211        });
212        this.message_events = new_events;
213    }
214
215    WindowTestEnvironment.prototype.next_default_test_name = function() {
216        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
217        this.name_counter++;
218        return get_title() + suffix;
219    };
220
221    WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
222        this.output_handler.setup(properties);
223        if (properties.hasOwnProperty("message_events")) {
224            this.setup_messages(properties.message_events);
225        }
226    };
227
228    WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
229        on_event(window, 'load', callback);
230    };
231
232    WindowTestEnvironment.prototype.test_timeout = function() {
233        var metas = document.getElementsByTagName("meta");
234        for (var i = 0; i < metas.length; i++) {
235            if (metas[i].name == "timeout") {
236                if (metas[i].content == "long") {
237                    return settings.harness_timeout.long;
238                }
239                break;
240            }
241        }
242        return settings.harness_timeout.normal;
243    };
244
245    /*
246     * Base TestEnvironment implementation for a generic web worker.
247     *
248     * Workers accumulate test results. One or more clients can connect and
249     * retrieve results from a worker at any time.
250     *
251     * WorkerTestEnvironment supports communicating with a client via a
252     * MessagePort.  The mechanism for determining the appropriate MessagePort
253     * for communicating with a client depends on the type of worker and is
254     * implemented by the various specializations of WorkerTestEnvironment
255     * below.
256     *
257     * A client document using testharness can use fetch_tests_from_worker() to
258     * retrieve results from a worker. See apisample16.html.
259     */
260    function WorkerTestEnvironment() {
261        this.name_counter = 0;
262        this.all_loaded = true;
263        this.message_list = [];
264        this.message_ports = [];
265    }
266
267    WorkerTestEnvironment.prototype._dispatch = function(message) {
268        this.message_list.push(message);
269        for (var i = 0; i < this.message_ports.length; ++i)
270        {
271            this.message_ports[i].postMessage(message);
272        }
273    };
274
275    // The only requirement is that port has a postMessage() method. It doesn't
276    // have to be an instance of a MessagePort, and often isn't.
277    WorkerTestEnvironment.prototype._add_message_port = function(port) {
278        this.message_ports.push(port);
279        for (var i = 0; i < this.message_list.length; ++i)
280        {
281            port.postMessage(this.message_list[i]);
282        }
283    };
284
285    WorkerTestEnvironment.prototype.next_default_test_name = function() {
286        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
287        this.name_counter++;
288        return get_title() + suffix;
289    };
290
291    WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
292
293    WorkerTestEnvironment.prototype.on_tests_ready = function() {
294        var this_obj = this;
295        add_start_callback(
296                function(properties) {
297                    this_obj._dispatch({
298                        type: "start",
299                        properties: properties,
300                    });
301                });
302        add_test_state_callback(
303                function(test) {
304                    this_obj._dispatch({
305                        type: "test_state",
306                        test: test.structured_clone()
307                    });
308                });
309        add_result_callback(
310                function(test) {
311                    this_obj._dispatch({
312                        type: "result",
313                        test: test.structured_clone()
314                    });
315                });
316        add_completion_callback(
317                function(tests, harness_status) {
318                    this_obj._dispatch({
319                        type: "complete",
320                        tests: map(tests,
321                            function(test) {
322                                return test.structured_clone();
323                            }),
324                        status: harness_status.structured_clone()
325                    });
326                });
327    };
328
329    WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
330
331    WorkerTestEnvironment.prototype.test_timeout = function() {
332        // Tests running in a worker don't have a default timeout. I.e. all
333        // worker tests behave as if settings.explicit_timeout is true.
334        return null;
335    };
336
337    /*
338     * Dedicated web workers.
339     * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
340     *
341     * This class is used as the test_environment when testharness is running
342     * inside a dedicated worker.
343     */
344    function DedicatedWorkerTestEnvironment() {
345        WorkerTestEnvironment.call(this);
346        // self is an instance of DedicatedWorkerGlobalScope which exposes
347        // a postMessage() method for communicating via the message channel
348        // established when the worker is created.
349        this._add_message_port(self);
350    }
351    DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
352
353    DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
354        WorkerTestEnvironment.prototype.on_tests_ready.call(this);
355        // In the absence of an onload notification, we a require dedicated
356        // workers to explicitly signal when the tests are done.
357        tests.wait_for_finish = true;
358    };
359
360    /*
361     * Shared web workers.
362     * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
363     *
364     * This class is used as the test_environment when testharness is running
365     * inside a shared web worker.
366     */
367    function SharedWorkerTestEnvironment() {
368        WorkerTestEnvironment.call(this);
369        var this_obj = this;
370        // Shared workers receive message ports via the 'onconnect' event for
371        // each connection.
372        self.addEventListener("connect",
373                function(message_event) {
374                    this_obj._add_message_port(message_event.source);
375                }, false);
376    }
377    SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
378
379    SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
380        WorkerTestEnvironment.prototype.on_tests_ready.call(this);
381        // In the absence of an onload notification, we a require shared
382        // workers to explicitly signal when the tests are done.
383        tests.wait_for_finish = true;
384    };
385
386    /*
387     * Service workers.
388     * http://www.w3.org/TR/service-workers/
389     *
390     * This class is used as the test_environment when testharness is running
391     * inside a service worker.
392     */
393    function ServiceWorkerTestEnvironment() {
394        WorkerTestEnvironment.call(this);
395        this.all_loaded = false;
396        this.on_loaded_callback = null;
397        var this_obj = this;
398        self.addEventListener("message",
399                function(event) {
400                    if (event.data && event.data.type && event.data.type === "connect") {
401                        if (event.ports && event.ports[0]) {
402                            // If a MessageChannel was passed, then use it to
403                            // send results back to the main window.  This
404                            // allows the tests to work even if the browser
405                            // does not fully support MessageEvent.source in
406                            // ServiceWorkers yet.
407                            this_obj._add_message_port(event.ports[0]);
408                            event.ports[0].start();
409                        } else {
410                            // If there is no MessageChannel, then attempt to
411                            // use the MessageEvent.source to send results
412                            // back to the main window.
413                            this_obj._add_message_port(event.source);
414                        }
415                    }
416                }, false);
417
418        // The oninstall event is received after the service worker script and
419        // all imported scripts have been fetched and executed. It's the
420        // equivalent of an onload event for a document. All tests should have
421        // been added by the time this event is received, thus it's not
422        // necessary to wait until the onactivate event. However, tests for
423        // installed service workers need another event which is equivalent to
424        // the onload event because oninstall is fired only on installation. The
425        // onmessage event is used for that purpose since tests using
426        // testharness.js should ask the result to its service worker by
427        // PostMessage. If the onmessage event is triggered on the service
428        // worker's context, that means the worker's script has been evaluated.
429        on_event(self, "install", on_all_loaded);
430        on_event(self, "message", on_all_loaded);
431        function on_all_loaded() {
432            if (this_obj.all_loaded)
433                return;
434            this_obj.all_loaded = true;
435            if (this_obj.on_loaded_callback) {
436              this_obj.on_loaded_callback();
437            }
438        }
439    }
440
441    ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
442
443    ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
444        if (this.all_loaded) {
445            callback();
446        } else {
447            this.on_loaded_callback = callback;
448        }
449    };
450
451    /*
452     * JavaScript shells.
453     *
454     * This class is used as the test_environment when testharness is running
455     * inside a JavaScript shell.
456     */
457    function ShellTestEnvironment() {
458        this.name_counter = 0;
459        this.all_loaded = false;
460        this.on_loaded_callback = null;
461        Promise.resolve().then(function() {
462            this.all_loaded = true
463            if (this.on_loaded_callback) {
464                this.on_loaded_callback();
465            }
466        }.bind(this));
467        this.message_list = [];
468        this.message_ports = [];
469    }
470
471    ShellTestEnvironment.prototype.next_default_test_name = function() {
472        var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
473        this.name_counter++;
474        return "Untitled" + suffix;
475    };
476
477    ShellTestEnvironment.prototype.on_new_harness_properties = function() {};
478
479    ShellTestEnvironment.prototype.on_tests_ready = function() {};
480
481    ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
482        if (this.all_loaded) {
483            callback();
484        } else {
485            this.on_loaded_callback = callback;
486        }
487    };
488
489    ShellTestEnvironment.prototype.test_timeout = function() {
490        // Tests running in a shell don't have a default timeout, so behave as
491        // if settings.explicit_timeout is true.
492        return null;
493    };
494
495    function create_test_environment() {
496        if ('document' in global_scope) {
497            return new WindowTestEnvironment();
498        }
499        if ('DedicatedWorkerGlobalScope' in global_scope &&
500            global_scope instanceof DedicatedWorkerGlobalScope) {
501            return new DedicatedWorkerTestEnvironment();
502        }
503        if ('SharedWorkerGlobalScope' in global_scope &&
504            global_scope instanceof SharedWorkerGlobalScope) {
505            return new SharedWorkerTestEnvironment();
506        }
507        if ('ServiceWorkerGlobalScope' in global_scope &&
508            global_scope instanceof ServiceWorkerGlobalScope) {
509            return new ServiceWorkerTestEnvironment();
510        }
511        if ('WorkerGlobalScope' in global_scope &&
512            global_scope instanceof WorkerGlobalScope) {
513            return new DedicatedWorkerTestEnvironment();
514        }
515
516        if (!('location' in global_scope)) {
517            return new ShellTestEnvironment();
518        }
519
520        throw new Error("Unsupported test environment");
521    }
522
523    var test_environment = create_test_environment();
524
525    function is_shared_worker(worker) {
526        return 'SharedWorker' in global_scope && worker instanceof SharedWorker;
527    }
528
529    function is_service_worker(worker) {
530        // The worker object may be from another execution context,
531        // so do not use instanceof here.
532        return 'ServiceWorker' in global_scope &&
533            Object.prototype.toString.call(worker) == '[object ServiceWorker]';
534    }
535
536    /*
537     * API functions
538     */
539    function test(func, name, properties)
540    {
541        var test_name = name ? name : test_environment.next_default_test_name();
542        properties = properties ? properties : {};
543        var test_obj = new Test(test_name, properties);
544        var value = test_obj.step(func, test_obj, test_obj);
545
546        if (value !== undefined) {
547            var msg = "test named \"" + test_name +
548                "\" inappropriately returned a value";
549
550            try {
551                if (value && value.hasOwnProperty("then")) {
552                    msg += ", consider using `promise_test` instead";
553                }
554            } catch (err) {}
555
556            tests.status.status = tests.status.ERROR;
557            tests.status.message = msg;
558        }
559
560        if (test_obj.phase === test_obj.phases.STARTED) {
561            test_obj.done();
562        }
563    }
564
565    function async_test(func, name, properties)
566    {
567        if (typeof func !== "function") {
568            properties = name;
569            name = func;
570            func = null;
571        }
572        var test_name = name ? name : test_environment.next_default_test_name();
573        properties = properties ? properties : {};
574        var test_obj = new Test(test_name, properties);
575        if (func) {
576            test_obj.step(func, test_obj, test_obj);
577        }
578        return test_obj;
579    }
580
581    function promise_test(func, name, properties) {
582        var test = async_test(name, properties);
583        test._is_promise_test = true;
584
585        // If there is no promise tests queue make one.
586        if (!tests.promise_tests) {
587            tests.promise_tests = Promise.resolve();
588        }
589        tests.promise_tests = tests.promise_tests.then(function() {
590            return new Promise(function(resolve) {
591                var promise = test.step(func, test, test);
592
593                test.step(function() {
594                    assert(!!promise, "promise_test", null,
595                           "test body must return a 'thenable' object (received ${value})",
596                           {value:promise});
597                    assert(typeof promise.then === "function", "promise_test", null,
598                           "test body must return a 'thenable' object (received an object with no `then` method)",
599                           null);
600                });
601
602                // Test authors may use the `step` method within a
603                // `promise_test` even though this reflects a mixture of
604                // asynchronous control flow paradigms. The "done" callback
605                // should be registered prior to the resolution of the
606                // user-provided Promise to avoid timeouts in cases where the
607                // Promise does not settle but a `step` function has thrown an
608                // error.
609                add_test_done_callback(test, resolve);
610
611                Promise.resolve(promise)
612                    .catch(test.step_func(
613                        function(value) {
614                            if (value instanceof AssertionError) {
615                                throw value;
616                            }
617                            assert(false, "promise_test", null,
618                                   "Unhandled rejection with value: ${value}", {value:value});
619                        }))
620                    .then(function() {
621                        test.done();
622                    });
623                });
624        });
625    }
626
627    function promise_rejects(test, expected, promise, description) {
628        return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) {
629            assert_throws(expected, function() { throw e }, description);
630        });
631    }
632
633    /**
634     * This constructor helper allows DOM events to be handled using Promises,
635     * which can make it a lot easier to test a very specific series of events,
636     * including ensuring that unexpected events are not fired at any point.
637     */
638    function EventWatcher(test, watchedNode, eventTypes, timeoutPromise)
639    {
640        if (typeof eventTypes == 'string') {
641            eventTypes = [eventTypes];
642        }
643
644        var waitingFor = null;
645
646        // This is null unless we are recording all events, in which case it
647        // will be an Array object.
648        var recordedEvents = null;
649
650        var eventHandler = test.step_func(function(evt) {
651            assert_true(!!waitingFor,
652                        'Not expecting event, but got ' + evt.type + ' event');
653            assert_equals(evt.type, waitingFor.types[0],
654                          'Expected ' + waitingFor.types[0] + ' event, but got ' +
655                          evt.type + ' event instead');
656
657            if (Array.isArray(recordedEvents)) {
658                recordedEvents.push(evt);
659            }
660
661            if (waitingFor.types.length > 1) {
662                // Pop first event from array
663                waitingFor.types.shift();
664                return;
665            }
666            // We need to null out waitingFor before calling the resolve function
667            // since the Promise's resolve handlers may call wait_for() which will
668            // need to set waitingFor.
669            var resolveFunc = waitingFor.resolve;
670            waitingFor = null;
671            // Likewise, we should reset the state of recordedEvents.
672            var result = recordedEvents || evt;
673            recordedEvents = null;
674            resolveFunc(result);
675        });
676
677        for (var i = 0; i < eventTypes.length; i++) {
678            watchedNode.addEventListener(eventTypes[i], eventHandler, false);
679        }
680
681        /**
682         * Returns a Promise that will resolve after the specified event or
683         * series of events has occurred.
684         *
685         * @param options An optional options object. If the 'record' property
686         *                on this object has the value 'all', when the Promise
687         *                returned by this function is resolved,  *all* Event
688         *                objects that were waited for will be returned as an
689         *                array.
690         *
691         * For example,
692         *
693         * ```js
694         * const watcher = new EventWatcher(t, div, [ 'animationstart',
695         *                                            'animationiteration',
696         *                                            'animationend' ]);
697         * return watcher.wait_for([ 'animationstart', 'animationend' ],
698         *                         { record: 'all' }).then(evts => {
699         *   assert_equals(evts[0].elapsedTime, 0.0);
700         *   assert_equals(evts[1].elapsedTime, 2.0);
701         * });
702         * ```
703         */
704        this.wait_for = function(types, options) {
705            if (waitingFor) {
706                return Promise.reject('Already waiting for an event or events');
707            }
708            if (typeof types == 'string') {
709                types = [types];
710            }
711            if (options && options.record && options.record === 'all') {
712                recordedEvents = [];
713            }
714            return new Promise(function(resolve, reject) {
715                var timeout = test.step_func(function() {
716                    // If the timeout fires after the events have been received
717                    // or during a subsequent call to wait_for, ignore it.
718                    if (!waitingFor || waitingFor.resolve !== resolve)
719                        return;
720
721                    // This should always fail, otherwise we should have
722                    // resolved the promise.
723                    assert_true(waitingFor.types.length == 0,
724                                'Timed out waiting for ' + waitingFor.types.join(', '));
725                    var result = recordedEvents;
726                    recordedEvents = null;
727                    var resolveFunc = waitingFor.resolve;
728                    waitingFor = null;
729                    resolveFunc(result);
730                });
731
732                if (timeoutPromise) {
733                    timeoutPromise().then(timeout);
734                }
735
736                waitingFor = {
737                    types: types,
738                    resolve: resolve,
739                    reject: reject
740                };
741            });
742        };
743
744        function stop_watching() {
745            for (var i = 0; i < eventTypes.length; i++) {
746                watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
747            }
748        };
749
750        test._add_cleanup(stop_watching);
751
752        return this;
753    }
754    expose(EventWatcher, 'EventWatcher');
755
756    function setup(func_or_properties, maybe_properties)
757    {
758        var func = null;
759        var properties = {};
760        if (arguments.length === 2) {
761            func = func_or_properties;
762            properties = maybe_properties;
763        } else if (func_or_properties instanceof Function) {
764            func = func_or_properties;
765        } else {
766            properties = func_or_properties;
767        }
768        tests.setup(func, properties);
769        test_environment.on_new_harness_properties(properties);
770    }
771
772    function done() {
773        if (tests.tests.length === 0) {
774            tests.set_file_is_test();
775        }
776        if (tests.file_is_test) {
777            // file is test files never have asynchronous cleanup logic,
778            // meaning the fully-synchronous `done` function can be used here.
779            tests.tests[0].done();
780        }
781        tests.end_wait();
782    }
783
784    function generate_tests(func, args, properties) {
785        forEach(args, function(x, i)
786                {
787                    var name = x[0];
788                    test(function()
789                         {
790                             func.apply(this, x.slice(1));
791                         },
792                         name,
793                         Array.isArray(properties) ? properties[i] : properties);
794                });
795    }
796
797    function on_event(object, event, callback)
798    {
799        object.addEventListener(event, callback, false);
800    }
801
802    function step_timeout(f, t) {
803        var outer_this = this;
804        var args = Array.prototype.slice.call(arguments, 2);
805        return setTimeout(function() {
806            f.apply(outer_this, args);
807        }, t * tests.timeout_multiplier);
808    }
809
810    expose(test, 'test');
811    expose(async_test, 'async_test');
812    expose(promise_test, 'promise_test');
813    expose(promise_rejects, 'promise_rejects');
814    expose(generate_tests, 'generate_tests');
815    expose(setup, 'setup');
816    expose(done, 'done');
817    expose(on_event, 'on_event');
818    expose(step_timeout, 'step_timeout');
819
820    /*
821     * Return a string truncated to the given length, with ... added at the end
822     * if it was longer.
823     */
824    function truncate(s, len)
825    {
826        if (s.length > len) {
827            return s.substring(0, len - 3) + "...";
828        }
829        return s;
830    }
831
832    /*
833     * Return true if object is probably a Node object.
834     */
835    function is_node(object)
836    {
837        // I use duck-typing instead of instanceof, because
838        // instanceof doesn't work if the node is from another window (like an
839        // iframe's contentWindow):
840        // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
841        try {
842            var has_node_properties = ("nodeType" in object &&
843                                       "nodeName" in object &&
844                                       "nodeValue" in object &&
845                                       "childNodes" in object);
846        } catch (e) {
847            // We're probably cross-origin, which means we aren't a node
848            return false;
849        }
850
851        if (has_node_properties) {
852            try {
853                object.nodeType;
854            } catch (e) {
855                // The object is probably Node.prototype or another prototype
856                // object that inherits from it, and not a Node instance.
857                return false;
858            }
859            return true;
860        }
861        return false;
862    }
863
864    var replacements = {
865        "0": "0",
866        "1": "x01",
867        "2": "x02",
868        "3": "x03",
869        "4": "x04",
870        "5": "x05",
871        "6": "x06",
872        "7": "x07",
873        "8": "b",
874        "9": "t",
875        "10": "n",
876        "11": "v",
877        "12": "f",
878        "13": "r",
879        "14": "x0e",
880        "15": "x0f",
881        "16": "x10",
882        "17": "x11",
883        "18": "x12",
884        "19": "x13",
885        "20": "x14",
886        "21": "x15",
887        "22": "x16",
888        "23": "x17",
889        "24": "x18",
890        "25": "x19",
891        "26": "x1a",
892        "27": "x1b",
893        "28": "x1c",
894        "29": "x1d",
895        "30": "x1e",
896        "31": "x1f",
897        "0xfffd": "ufffd",
898        "0xfffe": "ufffe",
899        "0xffff": "uffff",
900    };
901
902    /*
903     * Convert a value to a nice, human-readable string
904     */
905    function format_value(val, seen)
906    {
907        if (!seen) {
908            seen = [];
909        }
910        if (typeof val === "object" && val !== null) {
911            if (seen.indexOf(val) >= 0) {
912                return "[...]";
913            }
914            seen.push(val);
915        }
916        if (Array.isArray(val)) {
917            return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]";
918        }
919
920        switch (typeof val) {
921        case "string":
922            val = val.replace("\\", "\\\\");
923            for (var p in replacements) {
924                var replace = "\\" + replacements[p];
925                val = val.replace(RegExp(String.fromCharCode(p), "g"), replace);
926            }
927            return '"' + val.replace(/"/g, '\\"') + '"';
928        case "boolean":
929        case "undefined":
930            return String(val);
931        case "number":
932            // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
933            // special-case.
934            if (val === -0 && 1/val === -Infinity) {
935                return "-0";
936            }
937            return String(val);
938        case "object":
939            if (val === null) {
940                return "null";
941            }
942
943            // Special-case Node objects, since those come up a lot in my tests.  I
944            // ignore namespaces.
945            if (is_node(val)) {
946                switch (val.nodeType) {
947                case Node.ELEMENT_NODE:
948                    var ret = "<" + val.localName;
949                    for (var i = 0; i < val.attributes.length; i++) {
950                        ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
951                    }
952                    ret += ">" + val.innerHTML + "</" + val.localName + ">";
953                    return "Element node " + truncate(ret, 60);
954                case Node.TEXT_NODE:
955                    return 'Text node "' + truncate(val.data, 60) + '"';
956                case Node.PROCESSING_INSTRUCTION_NODE:
957                    return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
958                case Node.COMMENT_NODE:
959                    return "Comment node <!--" + truncate(val.data, 60) + "-->";
960                case Node.DOCUMENT_NODE:
961                    return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
962                case Node.DOCUMENT_TYPE_NODE:
963                    return "DocumentType node";
964                case Node.DOCUMENT_FRAGMENT_NODE:
965                    return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
966                default:
967                    return "Node object of unknown type";
968                }
969            }
970
971        /* falls through */
972        default:
973            try {
974                return typeof val + ' "' + truncate(String(val), 1000) + '"';
975            } catch(e) {
976                return ("[stringifying object threw " + String(e) +
977                        " with type " + String(typeof e) + "]");
978            }
979        }
980    }
981    expose(format_value, "format_value");
982
983    /*
984     * Assertions
985     */
986
987    function assert_true(actual, description)
988    {
989        assert(actual === true, "assert_true", description,
990                                "expected true got ${actual}", {actual:actual});
991    }
992    expose(assert_true, "assert_true");
993
994    function assert_false(actual, description)
995    {
996        assert(actual === false, "assert_false", description,
997                                 "expected false got ${actual}", {actual:actual});
998    }
999    expose(assert_false, "assert_false");
1000
1001    function same_value(x, y) {
1002        if (y !== y) {
1003            //NaN case
1004            return x !== x;
1005        }
1006        if (x === 0 && y === 0) {
1007            //Distinguish +0 and -0
1008            return 1/x === 1/y;
1009        }
1010        return x === y;
1011    }
1012
1013    function assert_equals(actual, expected, description)
1014    {
1015         /*
1016          * Test if two primitives are equal or two objects
1017          * are the same object
1018          */
1019        if (typeof actual != typeof expected) {
1020            assert(false, "assert_equals", description,
1021                          "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
1022                          {expected:expected, actual:actual});
1023            return;
1024        }
1025        assert(same_value(actual, expected), "assert_equals", description,
1026                                             "expected ${expected} but got ${actual}",
1027                                             {expected:expected, actual:actual});
1028    }
1029    expose(assert_equals, "assert_equals");
1030
1031    function assert_not_equals(actual, expected, description)
1032    {
1033         /*
1034          * Test if two primitives are unequal or two objects
1035          * are different objects
1036          */
1037        assert(!same_value(actual, expected), "assert_not_equals", description,
1038                                              "got disallowed value ${actual}",
1039                                              {actual:actual});
1040    }
1041    expose(assert_not_equals, "assert_not_equals");
1042
1043    function assert_in_array(actual, expected, description)
1044    {
1045        assert(expected.indexOf(actual) != -1, "assert_in_array", description,
1046                                               "value ${actual} not in array ${expected}",
1047                                               {actual:actual, expected:expected});
1048    }
1049    expose(assert_in_array, "assert_in_array");
1050
1051    function assert_object_equals(actual, expected, description)
1052    {
1053         assert(typeof actual === "object" && actual !== null, "assert_object_equals", description,
1054                                                               "value is ${actual}, expected object",
1055                                                               {actual: actual});
1056         //This needs to be improved a great deal
1057         function check_equal(actual, expected, stack)
1058         {
1059             stack.push(actual);
1060
1061             var p;
1062             for (p in actual) {
1063                 assert(expected.hasOwnProperty(p), "assert_object_equals", description,
1064                                                    "unexpected property ${p}", {p:p});
1065
1066                 if (typeof actual[p] === "object" && actual[p] !== null) {
1067                     if (stack.indexOf(actual[p]) === -1) {
1068                         check_equal(actual[p], expected[p], stack);
1069                     }
1070                 } else {
1071                     assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
1072                                                       "property ${p} expected ${expected} got ${actual}",
1073                                                       {p:p, expected:expected, actual:actual});
1074                 }
1075             }
1076             for (p in expected) {
1077                 assert(actual.hasOwnProperty(p),
1078                        "assert_object_equals", description,
1079                        "expected property ${p} missing", {p:p});
1080             }
1081             stack.pop();
1082         }
1083         check_equal(actual, expected, []);
1084    }
1085    expose(assert_object_equals, "assert_object_equals");
1086
1087    function assert_array_equals(actual, expected, description)
1088    {
1089        assert(typeof actual === "object" && actual !== null && "length" in actual,
1090               "assert_array_equals", description,
1091               "value is ${actual}, expected array",
1092               {actual:actual});
1093        assert(actual.length === expected.length,
1094               "assert_array_equals", description,
1095               "lengths differ, expected ${expected} got ${actual}",
1096               {expected:expected.length, actual:actual.length});
1097
1098        for (var i = 0; i < actual.length; i++) {
1099            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
1100                   "assert_array_equals", description,
1101                   "property ${i}, property expected to be ${expected} but was ${actual}",
1102                   {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
1103                   actual:actual.hasOwnProperty(i) ? "present" : "missing"});
1104            assert(same_value(expected[i], actual[i]),
1105                   "assert_array_equals", description,
1106                   "property ${i}, expected ${expected} but got ${actual}",
1107                   {i:i, expected:expected[i], actual:actual[i]});
1108        }
1109    }
1110    expose(assert_array_equals, "assert_array_equals");
1111
1112    function assert_array_approx_equals(actual, expected, epsilon, description)
1113    {
1114        /*
1115         * Test if two primitive arrays are equal within +/- epsilon
1116         */
1117        assert(actual.length === expected.length,
1118               "assert_array_approx_equals", description,
1119               "lengths differ, expected ${expected} got ${actual}",
1120               {expected:expected.length, actual:actual.length});
1121
1122        for (var i = 0; i < actual.length; i++) {
1123            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
1124                   "assert_array_approx_equals", description,
1125                   "property ${i}, property expected to be ${expected} but was ${actual}",
1126                   {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
1127                   actual:actual.hasOwnProperty(i) ? "present" : "missing"});
1128            assert(typeof actual[i] === "number",
1129                   "assert_array_approx_equals", description,
1130                   "property ${i}, expected a number but got a ${type_actual}",
1131                   {i:i, type_actual:typeof actual[i]});
1132            assert(Math.abs(actual[i] - expected[i]) <= epsilon,
1133                   "assert_array_approx_equals", description,
1134                   "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}",
1135                   {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon});
1136        }
1137    }
1138    expose(assert_array_approx_equals, "assert_array_approx_equals");
1139
1140    function assert_approx_equals(actual, expected, epsilon, description)
1141    {
1142        /*
1143         * Test if two primitive numbers are equal within +/- epsilon
1144         */
1145        assert(typeof actual === "number",
1146               "assert_approx_equals", description,
1147               "expected a number but got a ${type_actual}",
1148               {type_actual:typeof actual});
1149
1150        assert(Math.abs(actual - expected) <= epsilon,
1151               "assert_approx_equals", description,
1152               "expected ${expected} +/- ${epsilon} but got ${actual}",
1153               {expected:expected, actual:actual, epsilon:epsilon});
1154    }
1155    expose(assert_approx_equals, "assert_approx_equals");
1156
1157    function assert_less_than(actual, expected, description)
1158    {
1159        /*
1160         * Test if a primitive number is less than another
1161         */
1162        assert(typeof actual === "number",
1163               "assert_less_than", description,
1164               "expected a number but got a ${type_actual}",
1165               {type_actual:typeof actual});
1166
1167        assert(actual < expected,
1168               "assert_less_than", description,
1169               "expected a number less than ${expected} but got ${actual}",
1170               {expected:expected, actual:actual});
1171    }
1172    expose(assert_less_than, "assert_less_than");
1173
1174    function assert_greater_than(actual, expected, description)
1175    {
1176        /*
1177         * Test if a primitive number is greater than another
1178         */
1179        assert(typeof actual === "number",
1180               "assert_greater_than", description,
1181               "expected a number but got a ${type_actual}",
1182               {type_actual:typeof actual});
1183
1184        assert(actual > expected,
1185               "assert_greater_than", description,
1186               "expected a number greater than ${expected} but got ${actual}",
1187               {expected:expected, actual:actual});
1188    }
1189    expose(assert_greater_than, "assert_greater_than");
1190
1191    function assert_between_exclusive(actual, lower, upper, description)
1192    {
1193        /*
1194         * Test if a primitive number is between two others
1195         */
1196        assert(typeof actual === "number",
1197               "assert_between_exclusive", description,
1198               "expected a number but got a ${type_actual}",
1199               {type_actual:typeof actual});
1200
1201        assert(actual > lower && actual < upper,
1202               "assert_between_exclusive", description,
1203               "expected a number greater than ${lower} " +
1204               "and less than ${upper} but got ${actual}",
1205               {lower:lower, upper:upper, actual:actual});
1206    }
1207    expose(assert_between_exclusive, "assert_between_exclusive");
1208
1209    function assert_less_than_equal(actual, expected, description)
1210    {
1211        /*
1212         * Test if a primitive number is less than or equal to another
1213         */
1214        assert(typeof actual === "number",
1215               "assert_less_than_equal", description,
1216               "expected a number but got a ${type_actual}",
1217               {type_actual:typeof actual});
1218
1219        assert(actual <= expected,
1220               "assert_less_than_equal", description,
1221               "expected a number less than or equal to ${expected} but got ${actual}",
1222               {expected:expected, actual:actual});
1223    }
1224    expose(assert_less_than_equal, "assert_less_than_equal");
1225
1226    function assert_greater_than_equal(actual, expected, description)
1227    {
1228        /*
1229         * Test if a primitive number is greater than or equal to another
1230         */
1231        assert(typeof actual === "number",
1232               "assert_greater_than_equal", description,
1233               "expected a number but got a ${type_actual}",
1234               {type_actual:typeof actual});
1235
1236        assert(actual >= expected,
1237               "assert_greater_than_equal", description,
1238               "expected a number greater than or equal to ${expected} but got ${actual}",
1239               {expected:expected, actual:actual});
1240    }
1241    expose(assert_greater_than_equal, "assert_greater_than_equal");
1242
1243    function assert_between_inclusive(actual, lower, upper, description)
1244    {
1245        /*
1246         * Test if a primitive number is between to two others or equal to either of them
1247         */
1248        assert(typeof actual === "number",
1249               "assert_between_inclusive", description,
1250               "expected a number but got a ${type_actual}",
1251               {type_actual:typeof actual});
1252
1253        assert(actual >= lower && actual <= upper,
1254               "assert_between_inclusive", description,
1255               "expected a number greater than or equal to ${lower} " +
1256               "and less than or equal to ${upper} but got ${actual}",
1257               {lower:lower, upper:upper, actual:actual});
1258    }
1259    expose(assert_between_inclusive, "assert_between_inclusive");
1260
1261    function assert_regexp_match(actual, expected, description) {
1262        /*
1263         * Test if a string (actual) matches a regexp (expected)
1264         */
1265        assert(expected.test(actual),
1266               "assert_regexp_match", description,
1267               "expected ${expected} but got ${actual}",
1268               {expected:expected, actual:actual});
1269    }
1270    expose(assert_regexp_match, "assert_regexp_match");
1271
1272    function assert_class_string(object, class_string, description) {
1273        assert_equals({}.toString.call(object), "[object " + class_string + "]",
1274                      description);
1275    }
1276    expose(assert_class_string, "assert_class_string");
1277
1278
1279    function assert_own_property(object, property_name, description) {
1280        assert(object.hasOwnProperty(property_name),
1281               "assert_own_property", description,
1282               "expected property ${p} missing", {p:property_name});
1283    }
1284    expose(assert_own_property, "assert_own_property");
1285
1286    function assert_not_own_property(object, property_name, description) {
1287        assert(!object.hasOwnProperty(property_name),
1288               "assert_not_own_property", description,
1289               "unexpected property ${p} is found on object", {p:property_name});
1290    }
1291    expose(assert_not_own_property, "assert_not_own_property");
1292
1293    function _assert_inherits(name) {
1294        return function (object, property_name, description)
1295        {
1296            assert(typeof object === "object" || typeof object === "function",
1297                   name, description,
1298                   "provided value is not an object");
1299
1300            assert("hasOwnProperty" in object,
1301                   name, description,
1302                   "provided value is an object but has no hasOwnProperty method");
1303
1304            assert(!object.hasOwnProperty(property_name),
1305                   name, description,
1306                   "property ${p} found on object expected in prototype chain",
1307                   {p:property_name});
1308
1309            assert(property_name in object,
1310                   name, description,
1311                   "property ${p} not found in prototype chain",
1312                   {p:property_name});
1313        };
1314    }
1315    expose(_assert_inherits("assert_inherits"), "assert_inherits");
1316    expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
1317
1318    function assert_readonly(object, property_name, description)
1319    {
1320         var initial_value = object[property_name];
1321         try {
1322             //Note that this can have side effects in the case where
1323             //the property has PutForwards
1324             object[property_name] = initial_value + "a"; //XXX use some other value here?
1325             assert(same_value(object[property_name], initial_value),
1326                    "assert_readonly", description,
1327                    "changing property ${p} succeeded",
1328                    {p:property_name});
1329         } finally {
1330             object[property_name] = initial_value;
1331         }
1332    }
1333    expose(assert_readonly, "assert_readonly");
1334
1335    /**
1336     * Assert an Exception with the expected code is thrown.
1337     *
1338     * @param {object|number|string} code The expected exception code.
1339     * @param {Function} func Function which should throw.
1340     * @param {string} description Error description for the case that the error is not thrown.
1341     */
1342    function assert_throws(code, func, description)
1343    {
1344        try {
1345            func.call(this);
1346            assert(false, "assert_throws", description,
1347                   "${func} did not throw", {func:func});
1348        } catch (e) {
1349            if (e instanceof AssertionError) {
1350                throw e;
1351            }
1352
1353            assert(typeof e === "object",
1354                   "assert_throws", description,
1355                   "${func} threw ${e} with type ${type}, not an object",
1356                   {func:func, e:e, type:typeof e});
1357
1358            assert(e !== null,
1359                   "assert_throws", description,
1360                   "${func} threw null, not an object",
1361                   {func:func});
1362
1363            if (code === null) {
1364                throw new AssertionError('Test bug: need to pass exception to assert_throws()');
1365            }
1366            if (typeof code === "object") {
1367                assert("name" in e && e.name == code.name,
1368                       "assert_throws", description,
1369                       "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
1370                                    {func:func, actual:e, actual_name:e.name,
1371                                     expected:code,
1372                                     expected_name:code.name});
1373                return;
1374            }
1375
1376            var code_name_map = {
1377                INDEX_SIZE_ERR: 'IndexSizeError',
1378                HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
1379                WRONG_DOCUMENT_ERR: 'WrongDocumentError',
1380                INVALID_CHARACTER_ERR: 'InvalidCharacterError',
1381                NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
1382                NOT_FOUND_ERR: 'NotFoundError',
1383                NOT_SUPPORTED_ERR: 'NotSupportedError',
1384                INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
1385                INVALID_STATE_ERR: 'InvalidStateError',
1386                SYNTAX_ERR: 'SyntaxError',
1387                INVALID_MODIFICATION_ERR: 'InvalidModificationError',
1388                NAMESPACE_ERR: 'NamespaceError',
1389                INVALID_ACCESS_ERR: 'InvalidAccessError',
1390                TYPE_MISMATCH_ERR: 'TypeMismatchError',
1391                SECURITY_ERR: 'SecurityError',
1392                NETWORK_ERR: 'NetworkError',
1393                ABORT_ERR: 'AbortError',
1394                URL_MISMATCH_ERR: 'URLMismatchError',
1395                QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
1396                TIMEOUT_ERR: 'TimeoutError',
1397                INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
1398                DATA_CLONE_ERR: 'DataCloneError'
1399            };
1400
1401            var name = code in code_name_map ? code_name_map[code] : code;
1402
1403            var name_code_map = {
1404                IndexSizeError: 1,
1405                HierarchyRequestError: 3,
1406                WrongDocumentError: 4,
1407                InvalidCharacterError: 5,
1408                NoModificationAllowedError: 7,
1409                NotFoundError: 8,
1410                NotSupportedError: 9,
1411                InUseAttributeError: 10,
1412                InvalidStateError: 11,
1413                SyntaxError: 12,
1414                InvalidModificationError: 13,
1415                NamespaceError: 14,
1416                InvalidAccessError: 15,
1417                TypeMismatchError: 17,
1418                SecurityError: 18,
1419                NetworkError: 19,
1420                AbortError: 20,
1421                URLMismatchError: 21,
1422                QuotaExceededError: 22,
1423                TimeoutError: 23,
1424                InvalidNodeTypeError: 24,
1425                DataCloneError: 25,
1426
1427                EncodingError: 0,
1428                NotReadableError: 0,
1429                UnknownError: 0,
1430                ConstraintError: 0,
1431                DataError: 0,
1432                TransactionInactiveError: 0,
1433                ReadOnlyError: 0,
1434                VersionError: 0,
1435                OperationError: 0,
1436                NotAllowedError: 0
1437            };
1438
1439            if (!(name in name_code_map)) {
1440                throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
1441            }
1442
1443            var required_props = { code: name_code_map[name] };
1444
1445            if (required_props.code === 0 ||
1446               ("name" in e &&
1447                e.name !== e.name.toUpperCase() &&
1448                e.name !== "DOMException")) {
1449                // New style exception: also test the name property.
1450                required_props.name = name;
1451            }
1452
1453            //We'd like to test that e instanceof the appropriate interface,
1454            //but we can't, because we don't know what window it was created
1455            //in.  It might be an instanceof the appropriate interface on some
1456            //unknown other window.  TODO: Work around this somehow?
1457
1458            for (var prop in required_props) {
1459                assert(prop in e && e[prop] == required_props[prop],
1460                       "assert_throws", description,
1461                       "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}",
1462                       {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
1463            }
1464        }
1465    }
1466    expose(assert_throws, "assert_throws");
1467
1468    function assert_unreached(description) {
1469         assert(false, "assert_unreached", description,
1470                "Reached unreachable code");
1471    }
1472    expose(assert_unreached, "assert_unreached");
1473
1474    function assert_any(assert_func, actual, expected_array)
1475    {
1476        var args = [].slice.call(arguments, 3);
1477        var errors = [];
1478        var passed = false;
1479        forEach(expected_array,
1480                function(expected)
1481                {
1482                    try {
1483                        assert_func.apply(this, [actual, expected].concat(args));
1484                        passed = true;
1485                    } catch (e) {
1486                        errors.push(e.message);
1487                    }
1488                });
1489        if (!passed) {
1490            throw new AssertionError(errors.join("\n\n"));
1491        }
1492    }
1493    expose(assert_any, "assert_any");
1494
1495    function Test(name, properties)
1496    {
1497        if (tests.file_is_test && tests.tests.length) {
1498            throw new Error("Tried to create a test with file_is_test");
1499        }
1500        this.name = name;
1501
1502        this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ?
1503            this.phases.COMPLETE : this.phases.INITIAL;
1504
1505        this.status = this.NOTRUN;
1506        this.timeout_id = null;
1507        this.index = null;
1508
1509        this.properties = properties;
1510        this.timeout_length = settings.test_timeout;
1511        if (this.timeout_length !== null) {
1512            this.timeout_length *= tests.timeout_multiplier;
1513        }
1514
1515        this.message = null;
1516        this.stack = null;
1517
1518        this.steps = [];
1519        this._is_promise_test = false;
1520
1521        this.cleanup_callbacks = [];
1522        this._user_defined_cleanup_count = 0;
1523        this._done_callbacks = [];
1524
1525        // Tests declared following harness completion are likely an indication
1526        // of a programming error, but they cannot be reported
1527        // deterministically.
1528        if (tests.phase === tests.phases.COMPLETE) {
1529            return;
1530        }
1531
1532        tests.push(this);
1533    }
1534
1535    Test.statuses = {
1536        PASS:0,
1537        FAIL:1,
1538        TIMEOUT:2,
1539        NOTRUN:3
1540    };
1541
1542    Test.prototype = merge({}, Test.statuses);
1543
1544    Test.prototype.phases = {
1545        INITIAL:0,
1546        STARTED:1,
1547        HAS_RESULT:2,
1548        CLEANING:3,
1549        COMPLETE:4
1550    };
1551
1552    Test.prototype.structured_clone = function()
1553    {
1554        if (!this._structured_clone) {
1555            var msg = this.message;
1556            msg = msg ? String(msg) : msg;
1557            this._structured_clone = merge({
1558                name:String(this.name),
1559                properties:merge({}, this.properties),
1560                phases:merge({}, this.phases)
1561            }, Test.statuses);
1562        }
1563        this._structured_clone.status = this.status;
1564        this._structured_clone.message = this.message;
1565        this._structured_clone.stack = this.stack;
1566        this._structured_clone.index = this.index;
1567        this._structured_clone.phase = this.phase;
1568        return this._structured_clone;
1569    };
1570
1571    Test.prototype.step = function(func, this_obj)
1572    {
1573        if (this.phase > this.phases.STARTED) {
1574            return;
1575        }
1576        this.phase = this.phases.STARTED;
1577        //If we don't get a result before the harness times out that will be a test timeout
1578        this.set_status(this.TIMEOUT, "Test timed out");
1579
1580        tests.started = true;
1581        tests.notify_test_state(this);
1582
1583        if (this.timeout_id === null) {
1584            this.set_timeout();
1585        }
1586
1587        this.steps.push(func);
1588
1589        if (arguments.length === 1) {
1590            this_obj = this;
1591        }
1592
1593        try {
1594            return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
1595        } catch (e) {
1596            if (this.phase >= this.phases.HAS_RESULT) {
1597                return;
1598            }
1599            var message = String((typeof e === "object" && e !== null) ? e.message : e);
1600            var stack = e.stack ? e.stack : null;
1601
1602            this.set_status(this.FAIL, message, stack);
1603            this.phase = this.phases.HAS_RESULT;
1604            this.done();
1605        }
1606    };
1607
1608    Test.prototype.step_func = function(func, this_obj)
1609    {
1610        var test_this = this;
1611
1612        if (arguments.length === 1) {
1613            this_obj = test_this;
1614        }
1615
1616        return function()
1617        {
1618            return test_this.step.apply(test_this, [func, this_obj].concat(
1619                Array.prototype.slice.call(arguments)));
1620        };
1621    };
1622
1623    Test.prototype.step_func_done = function(func, this_obj)
1624    {
1625        var test_this = this;
1626
1627        if (arguments.length === 1) {
1628            this_obj = test_this;
1629        }
1630
1631        return function()
1632        {
1633            if (func) {
1634                test_this.step.apply(test_this, [func, this_obj].concat(
1635                    Array.prototype.slice.call(arguments)));
1636            }
1637            test_this.done();
1638        };
1639    };
1640
1641    Test.prototype.unreached_func = function(description)
1642    {
1643        return this.step_func(function() {
1644            assert_unreached(description);
1645        });
1646    };
1647
1648    Test.prototype.step_timeout = function(f, timeout) {
1649        var test_this = this;
1650        var args = Array.prototype.slice.call(arguments, 2);
1651        return setTimeout(this.step_func(function() {
1652            return f.apply(test_this, args);
1653        }), timeout * tests.timeout_multiplier);
1654    }
1655
1656    /*
1657     * Private method for registering cleanup functions. `testharness.js`
1658     * internals should use this method instead of the public `add_cleanup`
1659     * method in order to hide implementation details from the harness status
1660     * message in the case errors.
1661     */
1662    Test.prototype._add_cleanup = function(callback) {
1663        this.cleanup_callbacks.push(callback);
1664    };
1665
1666    /*
1667     * Schedule a function to be run after the test result is known, regardless
1668     * of passing or failing state. The behavior of this function will not
1669     * influence the result of the test, but if an exception is thrown, the
1670     * test harness will report an error.
1671     */
1672    Test.prototype.add_cleanup = function(callback) {
1673        this._user_defined_cleanup_count += 1;
1674        this._add_cleanup(callback);
1675    };
1676
1677    Test.prototype.set_timeout = function()
1678    {
1679        if (this.timeout_length !== null) {
1680            var this_obj = this;
1681            this.timeout_id = setTimeout(function()
1682                                         {
1683                                             this_obj.timeout();
1684                                         }, this.timeout_length);
1685        }
1686    };
1687
1688    Test.prototype.set_status = function(status, message, stack)
1689    {
1690        this.status = status;
1691        this.message = message;
1692        this.stack = stack ? stack : null;
1693    };
1694
1695    Test.prototype.timeout = function()
1696    {
1697        this.timeout_id = null;
1698        this.set_status(this.TIMEOUT, "Test timed out");
1699        this.phase = this.phases.HAS_RESULT;
1700        this.done();
1701    };
1702
1703    Test.prototype.force_timeout = Test.prototype.timeout;
1704
1705    /**
1706     * Update the test status, initiate "cleanup" functions, and signal test
1707     * completion.
1708     */
1709    Test.prototype.done = function()
1710    {
1711        if (this.phase >= this.phases.CLEANING) {
1712            return;
1713        }
1714
1715        if (this.phase <= this.phases.STARTED) {
1716            this.set_status(this.PASS, null);
1717        }
1718
1719        if (global_scope.clearTimeout) {
1720            clearTimeout(this.timeout_id);
1721        }
1722
1723        this.cleanup();
1724    };
1725
1726    function add_test_done_callback(test, callback)
1727    {
1728        if (test.phase === test.phases.COMPLETE) {
1729            callback();
1730            return;
1731        }
1732
1733        test._done_callbacks.push(callback);
1734    }
1735
1736    /*
1737     * Invoke all specified cleanup functions. If one or more produce an error,
1738     * the context is in an unpredictable state, so all further testing should
1739     * be cancelled.
1740     */
1741    Test.prototype.cleanup = function() {
1742        var error_count = 0;
1743        var bad_value_count = 0;
1744        function on_error() {
1745            error_count += 1;
1746            // Abort tests immediately so that tests declared within subsequent
1747            // cleanup functions are not run.
1748            tests.abort();
1749        }
1750        var this_obj = this;
1751        var results = [];
1752
1753        this.phase = this.phases.CLEANING;
1754
1755        forEach(this.cleanup_callbacks,
1756                function(cleanup_callback) {
1757                    var result;
1758
1759                    try {
1760                        result = cleanup_callback();
1761                    } catch (e) {
1762                        on_error();
1763                        return;
1764                    }
1765
1766                    if (!is_valid_cleanup_result(this_obj, result)) {
1767                        bad_value_count += 1;
1768                        // Abort tests immediately so that tests declared
1769                        // within subsequent cleanup functions are not run.
1770                        tests.abort();
1771                    }
1772
1773                    results.push(result);
1774                });
1775
1776        if (!this._is_promise_test) {
1777            cleanup_done(this_obj, error_count, bad_value_count);
1778        } else {
1779            all_async(results,
1780                      function(result, done) {
1781                          if (result && typeof result.then === "function") {
1782                              result
1783                                  .then(null, on_error)
1784                                  .then(done);
1785                          } else {
1786                              done();
1787                          }
1788                      },
1789                      function() {
1790                          cleanup_done(this_obj, error_count, bad_value_count);
1791                      });
1792        }
1793    };
1794
1795    /**
1796     * Determine if the return value of a cleanup function is valid for a given
1797     * test. Any test may return the value `undefined`. Tests created with
1798     * `promise_test` may alternatively return "thenable" object values.
1799     */
1800    function is_valid_cleanup_result(test, result) {
1801        if (result === undefined) {
1802            return true;
1803        }
1804
1805        if (test._is_promise_test) {
1806            return result && typeof result.then === "function";
1807        }
1808
1809        return false;
1810    }
1811
1812    function cleanup_done(test, error_count, bad_value_count) {
1813        if (error_count || bad_value_count) {
1814            var total = test._user_defined_cleanup_count;
1815
1816            tests.status.status = tests.status.ERROR;
1817            tests.status.message = "Test named '" + test.name +
1818                "' specified " + total +
1819                " 'cleanup' function" + (total > 1 ? "s" : "");
1820
1821            if (error_count) {
1822                tests.status.message += ", and " + error_count + " failed";
1823            }
1824
1825            if (bad_value_count) {
1826                var type = test._is_promise_test ?
1827                   "non-thenable" : "non-undefined";
1828                tests.status.message += ", and " + bad_value_count +
1829                    " returned a " + type + " value";
1830            }
1831
1832            tests.status.message += ".";
1833
1834            tests.status.stack = null;
1835        }
1836
1837        test.phase = test.phases.COMPLETE;
1838        tests.result(test);
1839        forEach(test._done_callbacks,
1840                function(callback) {
1841                    callback();
1842                });
1843        test._done_callbacks.length = 0;
1844    }
1845
1846    /*
1847     * A RemoteTest object mirrors a Test object on a remote worker. The
1848     * associated RemoteWorker updates the RemoteTest object in response to
1849     * received events. In turn, the RemoteTest object replicates these events
1850     * on the local document. This allows listeners (test result reporting
1851     * etc..) to transparently handle local and remote events.
1852     */
1853    function RemoteTest(clone) {
1854        var this_obj = this;
1855        Object.keys(clone).forEach(
1856                function(key) {
1857                    this_obj[key] = clone[key];
1858                });
1859        this.index = null;
1860        this.phase = this.phases.INITIAL;
1861        this.update_state_from(clone);
1862        this._done_callbacks = [];
1863        tests.push(this);
1864    }
1865
1866    RemoteTest.prototype.structured_clone = function() {
1867        var clone = {};
1868        Object.keys(this).forEach(
1869                (function(key) {
1870                    var value = this[key];
1871                    // `RemoteTest` instances are responsible for managing
1872                    // their own "done" callback functions, so those functions
1873                    // are not relevant in other execution contexts. Because of
1874                    // this (and because Function values cannot be serialized
1875                    // for cross-realm transmittance), the property should not
1876                    // be considered when cloning instances.
1877                    if (key === '_done_callbacks' ) {
1878                        return;
1879                    }
1880
1881                    if (typeof value === "object" && value !== null) {
1882                        clone[key] = merge({}, value);
1883                    } else {
1884                        clone[key] = value;
1885                    }
1886                }).bind(this));
1887        clone.phases = merge({}, this.phases);
1888        return clone;
1889    };
1890
1891    /**
1892     * `RemoteTest` instances are objects which represent tests running in
1893     * another realm. They do not define "cleanup" functions (if necessary,
1894     * such functions are defined on the associated `Test` instance within the
1895     * external realm). However, `RemoteTests` may have "done" callbacks (e.g.
1896     * as attached by the `Tests` instance responsible for tracking the overall
1897     * test status in the parent realm). The `cleanup` method delegates to
1898     * `done` in order to ensure that such callbacks are invoked following the
1899     * completion of the `RemoteTest`.
1900     */
1901    RemoteTest.prototype.cleanup = function() {
1902        this.done();
1903    };
1904    RemoteTest.prototype.phases = Test.prototype.phases;
1905    RemoteTest.prototype.update_state_from = function(clone) {
1906        this.status = clone.status;
1907        this.message = clone.message;
1908        this.stack = clone.stack;
1909        if (this.phase === this.phases.INITIAL) {
1910            this.phase = this.phases.STARTED;
1911        }
1912    };
1913    RemoteTest.prototype.done = function() {
1914        this.phase = this.phases.COMPLETE;
1915
1916        forEach(this._done_callbacks,
1917                function(callback) {
1918                    callback();
1919                });
1920    }
1921
1922    /*
1923     * A RemoteContext listens for test events from a remote test context, such
1924     * as another window or a worker. These events are then used to construct
1925     * and maintain RemoteTest objects that mirror the tests running in the
1926     * remote context.
1927     *
1928     * An optional third parameter can be used as a predicate to filter incoming
1929     * MessageEvents.
1930     */
1931    function RemoteContext(remote, message_target, message_filter) {
1932        this.running = true;
1933        this.started = false;
1934        this.tests = new Array();
1935        this.early_exception = null;
1936
1937        var this_obj = this;
1938        // If remote context is cross origin assigning to onerror is not
1939        // possible, so silently catch those errors.
1940        try {
1941          remote.onerror = function(error) { this_obj.remote_error(error); };
1942        } catch (e) {
1943          // Ignore.
1944        }
1945
1946        // Keeping a reference to the remote object and the message handler until
1947        // remote_done() is seen prevents the remote object and its message channel
1948        // from going away before all the messages are dispatched.
1949        this.remote = remote;
1950        this.message_target = message_target;
1951        this.message_handler = function(message) {
1952            var passesFilter = !message_filter || message_filter(message);
1953            // The reference to the `running` property in the following
1954            // condition is unnecessary because that value is only set to
1955            // `false` after the `message_handler` function has been
1956            // unsubscribed.
1957            // TODO: Simplify the condition by removing the reference.
1958            if (this_obj.running && message.data && passesFilter &&
1959                (message.data.type in this_obj.message_handlers)) {
1960                this_obj.message_handlers[message.data.type].call(this_obj, message.data);
1961            }
1962        };
1963
1964        if (self.Promise) {
1965            this.done = new Promise(function(resolve) {
1966                this_obj.doneResolve = resolve;
1967            });
1968        }
1969
1970        this.message_target.addEventListener("message", this.message_handler);
1971    }
1972
1973    RemoteContext.prototype.remote_error = function(error) {
1974        if (error.preventDefault) {
1975            error.preventDefault();
1976        }
1977
1978        // Defer interpretation of errors until the testing protocol has
1979        // started and the remote test's `allow_uncaught_exception` property
1980        // is available.
1981        if (!this.started) {
1982            this.early_exception = error;
1983        } else if (!this.allow_uncaught_exception) {
1984            this.report_uncaught(error);
1985        }
1986    };
1987
1988    RemoteContext.prototype.report_uncaught = function(error) {
1989        var message = error.message || String(error);
1990        var filename = (error.filename ? " " + error.filename: "");
1991        // FIXME: Display remote error states separately from main document
1992        // error state.
1993        tests.set_status(tests.status.ERROR,
1994                         "Error in remote" + filename + ": " + message,
1995                         error.stack);
1996    };
1997
1998    RemoteContext.prototype.start = function(data) {
1999        this.started = true;
2000        this.allow_uncaught_exception = data.properties.allow_uncaught_exception;
2001
2002        if (this.early_exception && !this.allow_uncaught_exception) {
2003            this.report_uncaught(this.early_exception);
2004        }
2005    };
2006
2007    RemoteContext.prototype.test_state = function(data) {
2008        var remote_test = this.tests[data.test.index];
2009        if (!remote_test) {
2010            remote_test = new RemoteTest(data.test);
2011            this.tests[data.test.index] = remote_test;
2012        }
2013        remote_test.update_state_from(data.test);
2014        tests.notify_test_state(remote_test);
2015    };
2016
2017    RemoteContext.prototype.test_done = function(data) {
2018        var remote_test = this.tests[data.test.index];
2019        remote_test.update_state_from(data.test);
2020        remote_test.done();
2021        tests.result(remote_test);
2022    };
2023
2024    RemoteContext.prototype.remote_done = function(data) {
2025        if (tests.status.status === null &&
2026            data.status.status !== data.status.OK) {
2027            tests.set_status(data.status.status, data.status.message, data.status.sack);
2028        }
2029
2030        this.message_target.removeEventListener("message", this.message_handler);
2031        this.running = false;
2032
2033        // If remote context is cross origin assigning to onerror is not
2034        // possible, so silently catch those errors.
2035        try {
2036          this.remote.onerror = null;
2037        } catch (e) {
2038          // Ignore.
2039        }
2040
2041        this.remote = null;
2042        this.message_target = null;
2043        if (this.doneResolve) {
2044            this.doneResolve();
2045        }
2046
2047        if (tests.all_done()) {
2048            tests.complete();
2049        }
2050    };
2051
2052    RemoteContext.prototype.message_handlers = {
2053        start: RemoteContext.prototype.start,
2054        test_state: RemoteContext.prototype.test_state,
2055        result: RemoteContext.prototype.test_done,
2056        complete: RemoteContext.prototype.remote_done
2057    };
2058
2059    /*
2060     * Harness
2061     */
2062
2063    function TestsStatus()
2064    {
2065        this.status = null;
2066        this.message = null;
2067        this.stack = null;
2068    }
2069
2070    TestsStatus.statuses = {
2071        OK:0,
2072        ERROR:1,
2073        TIMEOUT:2
2074    };
2075
2076    TestsStatus.prototype = merge({}, TestsStatus.statuses);
2077
2078    TestsStatus.prototype.structured_clone = function()
2079    {
2080        if (!this._structured_clone) {
2081            var msg = this.message;
2082            msg = msg ? String(msg) : msg;
2083            this._structured_clone = merge({
2084                status:this.status,
2085                message:msg,
2086                stack:this.stack
2087            }, TestsStatus.statuses);
2088        }
2089        return this._structured_clone;
2090    };
2091
2092    function Tests()
2093    {
2094        this.tests = [];
2095        this.num_pending = 0;
2096
2097        this.phases = {
2098            INITIAL:0,
2099            SETUP:1,
2100            HAVE_TESTS:2,
2101            HAVE_RESULTS:3,
2102            COMPLETE:4
2103        };
2104        this.phase = this.phases.INITIAL;
2105
2106        this.properties = {};
2107
2108        this.wait_for_finish = false;
2109        this.processing_callbacks = false;
2110
2111        this.allow_uncaught_exception = false;
2112
2113        this.file_is_test = false;
2114
2115        this.timeout_multiplier = 1;
2116        this.timeout_length = test_environment.test_timeout();
2117        this.timeout_id = null;
2118
2119        this.start_callbacks = [];
2120        this.test_state_callbacks = [];
2121        this.test_done_callbacks = [];
2122        this.all_done_callbacks = [];
2123
2124        this.pending_remotes = [];
2125
2126        this.status = new TestsStatus();
2127
2128        var this_obj = this;
2129
2130        test_environment.add_on_loaded_callback(function() {
2131            if (this_obj.all_done()) {
2132                this_obj.complete();
2133            }
2134        });
2135
2136        this.set_timeout();
2137    }
2138
2139    Tests.prototype.setup = function(func, properties)
2140    {
2141        if (this.phase >= this.phases.HAVE_RESULTS) {
2142            return;
2143        }
2144
2145        if (this.phase < this.phases.SETUP) {
2146            this.phase = this.phases.SETUP;
2147        }
2148
2149        this.properties = properties;
2150
2151        for (var p in properties) {
2152            if (properties.hasOwnProperty(p)) {
2153                var value = properties[p];
2154                if (p == "allow_uncaught_exception") {
2155                    this.allow_uncaught_exception = value;
2156                } else if (p == "explicit_done" && value) {
2157                    this.wait_for_finish = true;
2158                } else if (p == "explicit_timeout" && value) {
2159                    this.timeout_length = null;
2160                    if (this.timeout_id)
2161                    {
2162                        clearTimeout(this.timeout_id);
2163                    }
2164                } else if (p == "timeout_multiplier") {
2165                    this.timeout_multiplier = value;
2166                    if (this.timeout_length) {
2167                         this.timeout_length *= this.timeout_multiplier;
2168                    }
2169                }
2170            }
2171        }
2172
2173        if (func) {
2174            try {
2175                func();
2176            } catch (e) {
2177                this.status.status = this.status.ERROR;
2178                this.status.message = String(e);
2179                this.status.stack = e.stack ? e.stack : null;
2180            }
2181        }
2182        this.set_timeout();
2183    };
2184
2185    Tests.prototype.set_file_is_test = function() {
2186        if (this.tests.length > 0) {
2187            throw new Error("Tried to set file as test after creating a test");
2188        }
2189        this.wait_for_finish = true;
2190        this.file_is_test = true;
2191        // Create the test, which will add it to the list of tests
2192        async_test();
2193    };
2194
2195    Tests.prototype.set_status = function(status, message, stack)
2196    {
2197        this.status.status = status;
2198        this.status.message = message;
2199        this.status.stack = stack ? stack : null;
2200    };
2201
2202    Tests.prototype.set_timeout = function() {
2203        if (global_scope.clearTimeout) {
2204            var this_obj = this;
2205            clearTimeout(this.timeout_id);
2206            if (this.timeout_length !== null) {
2207                this.timeout_id = setTimeout(function() {
2208                                                 this_obj.timeout();
2209                                             }, this.timeout_length);
2210            }
2211        }
2212    };
2213
2214    Tests.prototype.timeout = function() {
2215        var test_in_cleanup = null;
2216
2217        if (this.status.status === null) {
2218            forEach(this.tests,
2219                    function(test) {
2220                        // No more than one test is expected to be in the
2221                        // "CLEANUP" phase at any time
2222                        if (test.phase === test.phases.CLEANING) {
2223                            test_in_cleanup = test;
2224                        }
2225
2226                        test.phase = test.phases.COMPLETE;
2227                    });
2228
2229            // Timeouts that occur while a test is in the "cleanup" phase
2230            // indicate that some global state was not properly reverted. This
2231            // invalidates the overall test execution, so the timeout should be
2232            // reported as an error and cancel the execution of any remaining
2233            // tests.
2234            if (test_in_cleanup) {
2235                this.status.status = this.status.ERROR;
2236                this.status.message = "Timeout while running cleanup for " +
2237                    "test named \"" + test_in_cleanup.name + "\".";
2238                tests.status.stack = null;
2239            } else {
2240                this.status.status = this.status.TIMEOUT;
2241            }
2242        }
2243
2244        this.complete();
2245    };
2246
2247    Tests.prototype.end_wait = function()
2248    {
2249        this.wait_for_finish = false;
2250        if (this.all_done()) {
2251            this.complete();
2252        }
2253    };
2254
2255    Tests.prototype.push = function(test)
2256    {
2257        if (this.phase < this.phases.HAVE_TESTS) {
2258            this.start();
2259        }
2260        this.num_pending++;
2261        test.index = this.tests.push(test);
2262        this.notify_test_state(test);
2263    };
2264
2265    Tests.prototype.notify_test_state = function(test) {
2266        var this_obj = this;
2267        forEach(this.test_state_callbacks,
2268                function(callback) {
2269                    callback(test, this_obj);
2270                });
2271    };
2272
2273    Tests.prototype.all_done = function() {
2274        return this.tests.length > 0 && test_environment.all_loaded &&
2275                (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish &&
2276                !this.processing_callbacks &&
2277                !this.pending_remotes.some(function(w) { return w.running; });
2278    };
2279
2280    Tests.prototype.start = function() {
2281        this.phase = this.phases.HAVE_TESTS;
2282        this.notify_start();
2283    };
2284
2285    Tests.prototype.notify_start = function() {
2286        var this_obj = this;
2287        forEach (this.start_callbacks,
2288                 function(callback)
2289                 {
2290                     callback(this_obj.properties);
2291                 });
2292    };
2293
2294    Tests.prototype.result = function(test)
2295    {
2296        // If the harness has already transitioned beyond the `HAVE_RESULTS`
2297        // phase, subsequent tests should not cause it to revert.
2298        if (this.phase <= this.phases.HAVE_RESULTS) {
2299            this.phase = this.phases.HAVE_RESULTS;
2300        }
2301        this.num_pending--;
2302        this.notify_result(test);
2303    };
2304
2305    Tests.prototype.notify_result = function(test) {
2306        var this_obj = this;
2307        this.processing_callbacks = true;
2308        forEach(this.test_done_callbacks,
2309                function(callback)
2310                {
2311                    callback(test, this_obj);
2312                });
2313        this.processing_callbacks = false;
2314        if (this_obj.all_done()) {
2315            this_obj.complete();
2316        }
2317    };
2318
2319    Tests.prototype.complete = function() {
2320        if (this.phase === this.phases.COMPLETE) {
2321            return;
2322        }
2323        var this_obj = this;
2324        var all_complete = function() {
2325            this_obj.phase = this_obj.phases.COMPLETE;
2326            this_obj.notify_complete();
2327        };
2328        var incomplete = filter(this.tests,
2329                                function(test) {
2330                                    return test.phase < test.phases.COMPLETE;
2331                                });
2332
2333        /**
2334         * To preserve legacy behavior, overall test completion must be
2335         * signaled synchronously.
2336         */
2337        if (incomplete.length === 0) {
2338            all_complete();
2339            return;
2340        }
2341
2342        all_async(incomplete,
2343                  function(test, testDone)
2344                  {
2345                      if (test.phase === test.phases.INITIAL) {
2346                          test.phase = test.phases.COMPLETE;
2347                          testDone();
2348                      } else {
2349                          add_test_done_callback(test, testDone);
2350                          test.cleanup();
2351                      }
2352                  },
2353                  all_complete);
2354    };
2355
2356    /**
2357     * Update the harness status to reflect an unrecoverable harness error that
2358     * should cancel all further testing. Update all previously-defined tests
2359     * which have not yet started to indicate that they will not be executed.
2360     */
2361    Tests.prototype.abort = function() {
2362        this.status.status = this.status.ERROR;
2363        this.is_aborted = true;
2364
2365        forEach(this.tests,
2366                function(test) {
2367                    if (test.phase === test.phases.INITIAL) {
2368                        test.phase = test.phases.COMPLETE;
2369                    }
2370                });
2371    };
2372
2373    /*
2374     * Determine if any tests share the same `name` property. Return an array
2375     * containing the names of any such duplicates.
2376     */
2377    Tests.prototype.find_duplicates = function() {
2378        var names = Object.create(null);
2379        var duplicates = [];
2380
2381        forEach (this.tests,
2382                 function(test)
2383                 {
2384                     if (test.name in names && duplicates.indexOf(test.name) === -1) {
2385                        duplicates.push(test.name);
2386                     }
2387                     names[test.name] = true;
2388                 });
2389
2390        return duplicates;
2391    };
2392
2393    function code_unit_str(char) {
2394        return 'U+' + char.charCodeAt(0).toString(16);
2395    }
2396
2397    function sanitize_unpaired_surrogates(str) {
2398        return str.replace(/([\ud800-\udbff])(?![\udc00-\udfff])/g,
2399                           function(_, unpaired)
2400                           {
2401                               return code_unit_str(unpaired);
2402                           })
2403                  // This replacement is intentionally implemented without an
2404                  // ES2018 negative lookbehind assertion to support runtimes
2405                  // which do not yet implement that language feature.
2406                  .replace(/(^|[^\ud800-\udbff])([\udc00-\udfff])/g,
2407                           function(_, previous, unpaired) {
2408                              if (/[\udc00-\udfff]/.test(previous)) {
2409                                  previous = code_unit_str(previous);
2410                              }
2411
2412                              return previous + code_unit_str(unpaired);
2413                           });
2414    }
2415
2416    function sanitize_all_unpaired_surrogates(tests) {
2417        forEach (tests,
2418                 function (test)
2419                 {
2420                     var sanitized = sanitize_unpaired_surrogates(test.name);
2421
2422                     if (test.name !== sanitized) {
2423                         test.name = sanitized;
2424                         delete test._structured_clone;
2425                     }
2426                 });
2427    }
2428
2429    Tests.prototype.notify_complete = function() {
2430        var this_obj = this;
2431        var duplicates;
2432
2433        if (this.status.status === null) {
2434            duplicates = this.find_duplicates();
2435
2436            // Some transports adhere to UTF-8's restriction on unpaired
2437            // surrogates. Sanitize the titles so that the results can be
2438            // consistently sent via all transports.
2439            sanitize_all_unpaired_surrogates(this.tests);
2440
2441            // Test names are presumed to be unique within test files--this
2442            // allows consumers to use them for identification purposes.
2443            // Duplicated names violate this expectation and should therefore
2444            // be reported as an error.
2445            if (duplicates.length) {
2446                this.status.status = this.status.ERROR;
2447                this.status.message =
2448                   duplicates.length + ' duplicate test name' +
2449                   (duplicates.length > 1 ? 's' : '') + ': "' +
2450                   duplicates.join('", "') + '"';
2451            } else {
2452                this.status.status = this.status.OK;
2453            }
2454        }
2455
2456        forEach (this.all_done_callbacks,
2457                 function(callback)
2458                 {
2459                     callback(this_obj.tests, this_obj.status);
2460                 });
2461    };
2462
2463    /*
2464     * Constructs a RemoteContext that tracks tests from a specific worker.
2465     */
2466    Tests.prototype.create_remote_worker = function(worker) {
2467        var message_port;
2468
2469        if (is_service_worker(worker)) {
2470            if (window.MessageChannel) {
2471                // The ServiceWorker's implicit MessagePort is currently not
2472                // reliably accessible from the ServiceWorkerGlobalScope due to
2473                // Blink setting MessageEvent.source to null for messages sent
2474                // via ServiceWorker.postMessage(). Until that's resolved,
2475                // create an explicit MessageChannel and pass one end to the
2476                // worker.
2477                var message_channel = new MessageChannel();
2478                message_port = message_channel.port1;
2479                message_port.start();
2480                worker.postMessage({type: "connect"}, [message_channel.port2]);
2481            } else {
2482                // If MessageChannel is not available, then try the
2483                // ServiceWorker.postMessage() approach using MessageEvent.source
2484                // on the other end.
2485                message_port = navigator.serviceWorker;
2486                worker.postMessage({type: "connect"});
2487            }
2488        } else if (is_shared_worker(worker)) {
2489            message_port = worker.port;
2490            message_port.start();
2491        } else {
2492            message_port = worker;
2493        }
2494
2495        return new RemoteContext(worker, message_port);
2496    };
2497
2498    /*
2499     * Constructs a RemoteContext that tracks tests from a specific window.
2500     */
2501    Tests.prototype.create_remote_window = function(remote) {
2502        remote.postMessage({type: "getmessages"}, "*");
2503        return new RemoteContext(
2504            remote,
2505            window,
2506            function(msg) {
2507                return msg.source === remote;
2508            }
2509        );
2510    };
2511
2512    Tests.prototype.fetch_tests_from_worker = function(worker) {
2513        if (this.phase >= this.phases.COMPLETE) {
2514            return;
2515        }
2516
2517        var remoteContext = this.create_remote_worker(worker);
2518        this.pending_remotes.push(remoteContext);
2519        return remoteContext.done;
2520    };
2521
2522    function fetch_tests_from_worker(port) {
2523        return tests.fetch_tests_from_worker(port);
2524    }
2525    expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
2526
2527    Tests.prototype.fetch_tests_from_window = function(remote) {
2528        if (this.phase >= this.phases.COMPLETE) {
2529            return;
2530        }
2531
2532        this.pending_remotes.push(this.create_remote_window(remote));
2533    };
2534
2535    function fetch_tests_from_window(window) {
2536        tests.fetch_tests_from_window(window);
2537    }
2538    expose(fetch_tests_from_window, 'fetch_tests_from_window');
2539
2540    function timeout() {
2541        if (tests.timeout_length === null) {
2542            tests.timeout();
2543        }
2544    }
2545    expose(timeout, 'timeout');
2546
2547    function add_start_callback(callback) {
2548        tests.start_callbacks.push(callback);
2549    }
2550
2551    function add_test_state_callback(callback) {
2552        tests.test_state_callbacks.push(callback);
2553    }
2554
2555    function add_result_callback(callback) {
2556        tests.test_done_callbacks.push(callback);
2557    }
2558
2559    function add_completion_callback(callback) {
2560        tests.all_done_callbacks.push(callback);
2561    }
2562
2563    expose(add_start_callback, 'add_start_callback');
2564    expose(add_test_state_callback, 'add_test_state_callback');
2565    expose(add_result_callback, 'add_result_callback');
2566    expose(add_completion_callback, 'add_completion_callback');
2567
2568    function remove(array, item) {
2569        var index = array.indexOf(item);
2570        if (index > -1) {
2571            array.splice(index, 1);
2572        }
2573    }
2574
2575    function remove_start_callback(callback) {
2576        remove(tests.start_callbacks, callback);
2577    }
2578
2579    function remove_test_state_callback(callback) {
2580        remove(tests.test_state_callbacks, callback);
2581    }
2582
2583    function remove_result_callback(callback) {
2584        remove(tests.test_done_callbacks, callback);
2585    }
2586
2587    function remove_completion_callback(callback) {
2588       remove(tests.all_done_callbacks, callback);
2589    }
2590
2591    /*
2592     * Output listener
2593    */
2594
2595    function Output() {
2596        this.output_document = document;
2597        this.output_node = null;
2598        this.enabled = settings.output;
2599        this.phase = this.INITIAL;
2600    }
2601
2602    Output.prototype.INITIAL = 0;
2603    Output.prototype.STARTED = 1;
2604    Output.prototype.HAVE_RESULTS = 2;
2605    Output.prototype.COMPLETE = 3;
2606
2607    Output.prototype.setup = function(properties) {
2608        if (this.phase > this.INITIAL) {
2609            return;
2610        }
2611
2612        //If output is disabled in testharnessreport.js the test shouldn't be
2613        //able to override that
2614        this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
2615                                        properties.output : settings.output);
2616    };
2617
2618    Output.prototype.init = function(properties) {
2619        if (this.phase >= this.STARTED) {
2620            return;
2621        }
2622        if (properties.output_document) {
2623            this.output_document = properties.output_document;
2624        } else {
2625            this.output_document = document;
2626        }
2627        this.phase = this.STARTED;
2628    };
2629
2630    Output.prototype.resolve_log = function() {
2631        var output_document;
2632        if (this.output_node) {
2633            return;
2634        }
2635        if (typeof this.output_document === "function") {
2636            output_document = this.output_document.apply(undefined);
2637        } else {
2638            output_document = this.output_document;
2639        }
2640        if (!output_document) {
2641            return;
2642        }
2643        var node = output_document.getElementById("log");
2644        if (!node) {
2645            if (output_document.readyState === "loading") {
2646                return;
2647            }
2648            node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div");
2649            node.id = "log";
2650            if (output_document.body) {
2651                output_document.body.appendChild(node);
2652            } else {
2653                var root = output_document.documentElement;
2654                var is_html = (root &&
2655                               root.namespaceURI == "http://www.w3.org/1999/xhtml" &&
2656                               root.localName == "html");
2657                var is_svg = (output_document.defaultView &&
2658                              "SVGSVGElement" in output_document.defaultView &&
2659                              root instanceof output_document.defaultView.SVGSVGElement);
2660                if (is_svg) {
2661                    var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
2662                    foreignObject.setAttribute("width", "100%");
2663                    foreignObject.setAttribute("height", "100%");
2664                    root.appendChild(foreignObject);
2665                    foreignObject.appendChild(node);
2666                } else if (is_html) {
2667                    root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body"))
2668                        .appendChild(node);
2669                } else {
2670                    root.appendChild(node);
2671                }
2672            }
2673        }
2674        this.output_document = output_document;
2675        this.output_node = node;
2676    };
2677
2678    Output.prototype.show_status = function() {
2679        if (this.phase < this.STARTED) {
2680            this.init();
2681        }
2682        if (!this.enabled || this.phase === this.COMPLETE) {
2683            return;
2684        }
2685        this.resolve_log();
2686        if (this.phase < this.HAVE_RESULTS) {
2687            this.phase = this.HAVE_RESULTS;
2688        }
2689        var done_count = tests.tests.length - tests.num_pending;
2690        if (this.output_node) {
2691            if (done_count < 100 ||
2692                (done_count < 1000 && done_count % 100 === 0) ||
2693                done_count % 1000 === 0) {
2694                this.output_node.textContent = "Running, " +
2695                    done_count + " complete, " +
2696                    tests.num_pending + " remain";
2697            }
2698        }
2699    };
2700
2701    Output.prototype.show_results = function (tests, harness_status) {
2702        if (this.phase >= this.COMPLETE) {
2703            return;
2704        }
2705        if (!this.enabled) {
2706            return;
2707        }
2708        if (!this.output_node) {
2709            this.resolve_log();
2710        }
2711        this.phase = this.COMPLETE;
2712
2713        var log = this.output_node;
2714        if (!log) {
2715            return;
2716        }
2717        var output_document = this.output_document;
2718
2719        while (log.lastChild) {
2720            log.removeChild(log.lastChild);
2721        }
2722
2723        var stylesheet = output_document.createElementNS(xhtml_ns, "style");
2724        stylesheet.textContent = stylesheetContent;
2725        var heads = output_document.getElementsByTagName("head");
2726        if (heads.length) {
2727            heads[0].appendChild(stylesheet);
2728        }
2729
2730        var status_text_harness = {};
2731        status_text_harness[harness_status.OK] = "OK";
2732        status_text_harness[harness_status.ERROR] = "Error";
2733        status_text_harness[harness_status.TIMEOUT] = "Timeout";
2734
2735        var status_text = {};
2736        status_text[Test.prototype.PASS] = "Pass";
2737        status_text[Test.prototype.FAIL] = "Fail";
2738        status_text[Test.prototype.TIMEOUT] = "Timeout";
2739        status_text[Test.prototype.NOTRUN] = "Not Run";
2740
2741        var status_number = {};
2742        forEach(tests,
2743                function(test) {
2744                    var status = status_text[test.status];
2745                    if (status_number.hasOwnProperty(status)) {
2746                        status_number[status] += 1;
2747                    } else {
2748                        status_number[status] = 1;
2749                    }
2750                });
2751
2752        function status_class(status)
2753        {
2754            return status.replace(/\s/g, '').toLowerCase();
2755        }
2756
2757        var summary_template = ["section", {"id":"summary"},
2758                                ["h2", {}, "Summary"],
2759                                function()
2760                                {
2761
2762                                    var status = status_text_harness[harness_status.status];
2763                                    var rv = [["section", {},
2764                                               ["p", {},
2765                                                "Harness status: ",
2766                                                ["span", {"class":status_class(status)},
2767                                                 status
2768                                                ],
2769                                               ]
2770                                              ]];
2771
2772                                    if (harness_status.status === harness_status.ERROR) {
2773                                        rv[0].push(["pre", {}, harness_status.message]);
2774                                        if (harness_status.stack) {
2775                                            rv[0].push(["pre", {}, harness_status.stack]);
2776                                        }
2777                                    }
2778                                    return rv;
2779                                },
2780                                ["p", {}, "Found ${num_tests} tests"],
2781                                function() {
2782                                    var rv = [["div", {}]];
2783                                    var i = 0;
2784                                    while (status_text.hasOwnProperty(i)) {
2785                                        if (status_number.hasOwnProperty(status_text[i])) {
2786                                            var status = status_text[i];
2787                                            rv[0].push(["div", {"class":status_class(status)},
2788                                                        ["label", {},
2789                                                         ["input", {type:"checkbox", checked:"checked"}],
2790                                                         status_number[status] + " " + status]]);
2791                                        }
2792                                        i++;
2793                                    }
2794                                    return rv;
2795                                },
2796                               ];
2797
2798        log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
2799
2800        forEach(output_document.querySelectorAll("section#summary label"),
2801                function(element)
2802                {
2803                    on_event(element, "click",
2804                             function(e)
2805                             {
2806                                 if (output_document.getElementById("results") === null) {
2807                                     e.preventDefault();
2808                                     return;
2809                                 }
2810                                 var result_class = element.parentNode.getAttribute("class");
2811                                 var style_element = output_document.querySelector("style#hide-" + result_class);
2812                                 var input_element = element.querySelector("input");
2813                                 if (!style_element && !input_element.checked) {
2814                                     style_element = output_document.createElementNS(xhtml_ns, "style");
2815                                     style_element.id = "hide-" + result_class;
2816                                     style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}";
2817                                     output_document.body.appendChild(style_element);
2818                                 } else if (style_element && input_element.checked) {
2819                                     style_element.parentNode.removeChild(style_element);
2820                                 }
2821                             });
2822                });
2823
2824        // This use of innerHTML plus manual escaping is not recommended in
2825        // general, but is necessary here for performance.  Using textContent
2826        // on each individual <td> adds tens of seconds of execution time for
2827        // large test suites (tens of thousands of tests).
2828        function escape_html(s)
2829        {
2830            return s.replace(/\&/g, "&amp;")
2831                .replace(/</g, "&lt;")
2832                .replace(/"/g, "&quot;")
2833                .replace(/'/g, "&#39;");
2834        }
2835
2836        function has_assertions()
2837        {
2838            for (var i = 0; i < tests.length; i++) {
2839                if (tests[i].properties.hasOwnProperty("assert")) {
2840                    return true;
2841                }
2842            }
2843            return false;
2844        }
2845
2846        function get_assertion(test)
2847        {
2848            if (test.properties.hasOwnProperty("assert")) {
2849                if (Array.isArray(test.properties.assert)) {
2850                    return test.properties.assert.join(' ');
2851                }
2852                return test.properties.assert;
2853            }
2854            return '';
2855        }
2856
2857        log.appendChild(document.createElementNS(xhtml_ns, "section"));
2858        var assertions = has_assertions();
2859        var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
2860            "<thead><tr><th>Result</th><th>Test Name</th>" +
2861            (assertions ? "<th>Assertion</th>" : "") +
2862            "<th>Message</th></tr></thead>" +
2863            "<tbody>";
2864        for (var i = 0; i < tests.length; i++) {
2865            html += '<tr class="' +
2866                escape_html(status_class(status_text[tests[i].status])) +
2867                '"><td>' +
2868                escape_html(status_text[tests[i].status]) +
2869                "</td><td>" +
2870                escape_html(tests[i].name) +
2871                "</td><td>" +
2872                (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") +
2873                escape_html(tests[i].message ? tests[i].message : " ") +
2874                (tests[i].stack ? "<pre>" +
2875                 escape_html(tests[i].stack) +
2876                 "</pre>": "") +
2877                "</td></tr>";
2878        }
2879        html += "</tbody></table>";
2880        try {
2881            log.lastChild.innerHTML = html;
2882        } catch (e) {
2883            log.appendChild(document.createElementNS(xhtml_ns, "p"))
2884               .textContent = "Setting innerHTML for the log threw an exception.";
2885            log.appendChild(document.createElementNS(xhtml_ns, "pre"))
2886               .textContent = html;
2887        }
2888    };
2889
2890    /*
2891     * Template code
2892     *
2893     * A template is just a JavaScript structure. An element is represented as:
2894     *
2895     * [tag_name, {attr_name:attr_value}, child1, child2]
2896     *
2897     * the children can either be strings (which act like text nodes), other templates or
2898     * functions (see below)
2899     *
2900     * A text node is represented as
2901     *
2902     * ["{text}", value]
2903     *
2904     * String values have a simple substitution syntax; ${foo} represents a variable foo.
2905     *
2906     * It is possible to embed logic in templates by using a function in a place where a
2907     * node would usually go. The function must either return part of a template or null.
2908     *
2909     * In cases where a set of nodes are required as output rather than a single node
2910     * with children it is possible to just use a list
2911     * [node1, node2, node3]
2912     *
2913     * Usage:
2914     *
2915     * render(template, substitutions) - take a template and an object mapping
2916     * variable names to parameters and return either a DOM node or a list of DOM nodes
2917     *
2918     * substitute(template, substitutions) - take a template and variable mapping object,
2919     * make the variable substitutions and return the substituted template
2920     *
2921     */
2922
2923    function is_single_node(template)
2924    {
2925        return typeof template[0] === "string";
2926    }
2927
2928    function substitute(template, substitutions)
2929    {
2930        if (typeof template === "function") {
2931            var replacement = template(substitutions);
2932            if (!replacement) {
2933                return null;
2934            }
2935
2936            return substitute(replacement, substitutions);
2937        }
2938
2939        if (is_single_node(template)) {
2940            return substitute_single(template, substitutions);
2941        }
2942
2943        return filter(map(template, function(x) {
2944                              return substitute(x, substitutions);
2945                          }), function(x) {return x !== null;});
2946    }
2947
2948    function substitute_single(template, substitutions)
2949    {
2950        var substitution_re = /\$\{([^ }]*)\}/g;
2951
2952        function do_substitution(input) {
2953            var components = input.split(substitution_re);
2954            var rv = [];
2955            for (var i = 0; i < components.length; i += 2) {
2956                rv.push(components[i]);
2957                if (components[i + 1]) {
2958                    rv.push(String(substitutions[components[i + 1]]));
2959                }
2960            }
2961            return rv;
2962        }
2963
2964        function substitute_attrs(attrs, rv)
2965        {
2966            rv[1] = {};
2967            for (var name in template[1]) {
2968                if (attrs.hasOwnProperty(name)) {
2969                    var new_name = do_substitution(name).join("");
2970                    var new_value = do_substitution(attrs[name]).join("");
2971                    rv[1][new_name] = new_value;
2972                }
2973            }
2974        }
2975
2976        function substitute_children(children, rv)
2977        {
2978            for (var i = 0; i < children.length; i++) {
2979                if (children[i] instanceof Object) {
2980                    var replacement = substitute(children[i], substitutions);
2981                    if (replacement !== null) {
2982                        if (is_single_node(replacement)) {
2983                            rv.push(replacement);
2984                        } else {
2985                            extend(rv, replacement);
2986                        }
2987                    }
2988                } else {
2989                    extend(rv, do_substitution(String(children[i])));
2990                }
2991            }
2992            return rv;
2993        }
2994
2995        var rv = [];
2996        rv.push(do_substitution(String(template[0])).join(""));
2997
2998        if (template[0] === "{text}") {
2999            substitute_children(template.slice(1), rv);
3000        } else {
3001            substitute_attrs(template[1], rv);
3002            substitute_children(template.slice(2), rv);
3003        }
3004
3005        return rv;
3006    }
3007
3008    function make_dom_single(template, doc)
3009    {
3010        var output_document = doc || document;
3011        var element;
3012        if (template[0] === "{text}") {
3013            element = output_document.createTextNode("");
3014            for (var i = 1; i < template.length; i++) {
3015                element.data += template[i];
3016            }
3017        } else {
3018            element = output_document.createElementNS(xhtml_ns, template[0]);
3019            for (var name in template[1]) {
3020                if (template[1].hasOwnProperty(name)) {
3021                    element.setAttribute(name, template[1][name]);
3022                }
3023            }
3024            for (var i = 2; i < template.length; i++) {
3025                if (template[i] instanceof Object) {
3026                    var sub_element = make_dom(template[i]);
3027                    element.appendChild(sub_element);
3028                } else {
3029                    var text_node = output_document.createTextNode(template[i]);
3030                    element.appendChild(text_node);
3031                }
3032            }
3033        }
3034
3035        return element;
3036    }
3037
3038    function make_dom(template, substitutions, output_document)
3039    {
3040        if (is_single_node(template)) {
3041            return make_dom_single(template, output_document);
3042        }
3043
3044        return map(template, function(x) {
3045                       return make_dom_single(x, output_document);
3046                   });
3047    }
3048
3049    function render(template, substitutions, output_document)
3050    {
3051        return make_dom(substitute(template, substitutions), output_document);
3052    }
3053
3054    /*
3055     * Utility functions
3056     */
3057    function assert(expected_true, function_name, description, error, substitutions)
3058    {
3059        if (tests.tests.length === 0) {
3060            tests.set_file_is_test();
3061        }
3062        if (expected_true !== true) {
3063            var msg = make_message(function_name, description,
3064                                   error, substitutions);
3065            throw new AssertionError(msg);
3066        }
3067    }
3068
3069    function AssertionError(message)
3070    {
3071        this.message = message;
3072        this.stack = this.get_stack();
3073    }
3074    expose(AssertionError, "AssertionError");
3075
3076    AssertionError.prototype = Object.create(Error.prototype);
3077
3078    AssertionError.prototype.get_stack = function() {
3079        var stack = new Error().stack;
3080        // IE11 does not initialize 'Error.stack' until the object is thrown.
3081        if (!stack) {
3082            try {
3083                throw new Error();
3084            } catch (e) {
3085                stack = e.stack;
3086            }
3087        }
3088
3089        // 'Error.stack' is not supported in all browsers/versions
3090        if (!stack) {
3091            return "(Stack trace unavailable)";
3092        }
3093
3094        var lines = stack.split("\n");
3095
3096        // Create a pattern to match stack frames originating within testharness.js.  These include the
3097        // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
3098        // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
3099        // in case it contains RegExp characters.
3100        var script_url = get_script_url();
3101        var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js";
3102        var re = new RegExp(re_text + ":\\d+:\\d+");
3103
3104        // Some browsers include a preamble that specifies the type of the error object.  Skip this by
3105        // advancing until we find the first stack frame originating from testharness.js.
3106        var i = 0;
3107        while (!re.test(lines[i]) && i < lines.length) {
3108            i++;
3109        }
3110
3111        // Then skip the top frames originating from testharness.js to begin the stack at the test code.
3112        while (re.test(lines[i]) && i < lines.length) {
3113            i++;
3114        }
3115
3116        // Paranoid check that we didn't skip all frames.  If so, return the original stack unmodified.
3117        if (i >= lines.length) {
3118            return stack;
3119        }
3120
3121        return lines.slice(i).join("\n");
3122    }
3123
3124    function make_message(function_name, description, error, substitutions)
3125    {
3126        for (var p in substitutions) {
3127            if (substitutions.hasOwnProperty(p)) {
3128                substitutions[p] = format_value(substitutions[p]);
3129            }
3130        }
3131        var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
3132                                   merge({function_name:function_name,
3133                                          description:(description?description + " ":"")},
3134                                          substitutions));
3135        return node_form.slice(1).join("");
3136    }
3137
3138    function filter(array, callable, thisObj) {
3139        var rv = [];
3140        for (var i = 0; i < array.length; i++) {
3141            if (array.hasOwnProperty(i)) {
3142                var pass = callable.call(thisObj, array[i], i, array);
3143                if (pass) {
3144                    rv.push(array[i]);
3145                }
3146            }
3147        }
3148        return rv;
3149    }
3150
3151    function map(array, callable, thisObj)
3152    {
3153        var rv = [];
3154        rv.length = array.length;
3155        for (var i = 0; i < array.length; i++) {
3156            if (array.hasOwnProperty(i)) {
3157                rv[i] = callable.call(thisObj, array[i], i, array);
3158            }
3159        }
3160        return rv;
3161    }
3162
3163    function extend(array, items)
3164    {
3165        Array.prototype.push.apply(array, items);
3166    }
3167
3168    function forEach(array, callback, thisObj)
3169    {
3170        for (var i = 0; i < array.length; i++) {
3171            if (array.hasOwnProperty(i)) {
3172                callback.call(thisObj, array[i], i, array);
3173            }
3174        }
3175    }
3176
3177    /**
3178     * Immediately invoke a "iteratee" function with a series of values in
3179     * parallel and invoke a final "done" function when all of the "iteratee"
3180     * invocations have signaled completion.
3181     *
3182     * If all callbacks complete synchronously (or if no callbacks are
3183     * specified), the `done_callback` will be invoked synchronously. It is the
3184     * responsibility of the caller to ensure asynchronicity in cases where
3185     * that is desired.
3186     *
3187     * @param {array} value Zero or more values to use in the invocation of
3188     *                      `iter_callback`
3189     * @param {function} iter_callback A function that will be invoked once for
3190     *                                 each of the provided `values`. Two
3191     *                                 arguments will be available in each
3192     *                                 invocation: the value from `values` and
3193     *                                 a function that must be invoked to
3194     *                                 signal completion
3195     * @param {function} done_callback A function that will be invoked after
3196     *                                 all operations initiated by the
3197     *                                 `iter_callback` function have signaled
3198     *                                 completion
3199     */
3200    function all_async(values, iter_callback, done_callback)
3201    {
3202        var remaining = values.length;
3203
3204        if (remaining === 0) {
3205            done_callback();
3206        }
3207
3208        forEach(values,
3209                function(element) {
3210                    var invoked = false;
3211                    var elDone = function() {
3212                        if (invoked) {
3213                            return;
3214                        }
3215
3216                        invoked = true;
3217                        remaining -= 1;
3218
3219                        if (remaining === 0) {
3220                            done_callback();
3221                        }
3222                    };
3223
3224                    iter_callback(element, elDone);
3225                });
3226    }
3227
3228    function merge(a,b)
3229    {
3230        var rv = {};
3231        var p;
3232        for (p in a) {
3233            rv[p] = a[p];
3234        }
3235        for (p in b) {
3236            rv[p] = b[p];
3237        }
3238        return rv;
3239    }
3240
3241    function expose(object, name)
3242    {
3243        var components = name.split(".");
3244        var target = global_scope;
3245        for (var i = 0; i < components.length - 1; i++) {
3246            if (!(components[i] in target)) {
3247                target[components[i]] = {};
3248            }
3249            target = target[components[i]];
3250        }
3251        target[components[components.length - 1]] = object;
3252    }
3253
3254    function is_same_origin(w) {
3255        try {
3256            'random_prop' in w;
3257            return true;
3258        } catch (e) {
3259            return false;
3260        }
3261    }
3262
3263    /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
3264    function get_script_url()
3265    {
3266        if (!('document' in global_scope)) {
3267            return undefined;
3268        }
3269
3270        var scripts = document.getElementsByTagName("script");
3271        for (var i = 0; i < scripts.length; i++) {
3272            var src;
3273            if (scripts[i].src) {
3274                src = scripts[i].src;
3275            } else if (scripts[i].href) {
3276                //SVG case
3277                src = scripts[i].href.baseVal;
3278            }
3279
3280            var matches = src && src.match(/^(.*\/|)testharness\.js$/);
3281            if (matches) {
3282                return src;
3283            }
3284        }
3285        return undefined;
3286    }
3287
3288    /** Returns the <title> or filename or "Untitled" */
3289    function get_title()
3290    {
3291        if ('document' in global_scope) {
3292            //Don't use document.title to work around an Opera bug in XHTML documents
3293            var title = document.getElementsByTagName("title")[0];
3294            if (title && title.firstChild && title.firstChild.data) {
3295                return title.firstChild.data;
3296            }
3297        }
3298        if ('META_TITLE' in global_scope && META_TITLE) {
3299            return META_TITLE;
3300        }
3301        if ('location' in global_scope) {
3302            return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.'));
3303        }
3304        return "Untitled";
3305    }
3306
3307    function supports_post_message(w)
3308    {
3309        var supports;
3310        var type;
3311        // Given IE implements postMessage across nested iframes but not across
3312        // windows or tabs, you can't infer cross-origin communication from the presence
3313        // of postMessage on the current window object only.
3314        //
3315        // Touching the postMessage prop on a window can throw if the window is
3316        // not from the same origin AND post message is not supported in that
3317        // browser. So just doing an existence test here won't do, you also need
3318        // to wrap it in a try..catch block.
3319        try {
3320            type = typeof w.postMessage;
3321            if (type === "function") {
3322                supports = true;
3323            }
3324
3325            // IE8 supports postMessage, but implements it as a host object which
3326            // returns "object" as its `typeof`.
3327            else if (type === "object") {
3328                supports = true;
3329            }
3330
3331            // This is the case where postMessage isn't supported AND accessing a
3332            // window property across origins does NOT throw (e.g. old Safari browser).
3333            else {
3334                supports = false;
3335            }
3336        } catch (e) {
3337            // This is the case where postMessage isn't supported AND accessing a
3338            // window property across origins throws (e.g. old Firefox browser).
3339            supports = false;
3340        }
3341        return supports;
3342    }
3343
3344    /**
3345     * Setup globals
3346     */
3347
3348    var tests = new Tests();
3349
3350    if (global_scope.addEventListener) {
3351        var error_handler = function(e) {
3352            if (tests.tests.length === 0 && !tests.allow_uncaught_exception) {
3353                tests.set_file_is_test();
3354            }
3355
3356            var stack;
3357            if (e.error && e.error.stack) {
3358                stack = e.error.stack;
3359            } else {
3360                stack = e.filename + ":" + e.lineno + ":" + e.colno;
3361            }
3362
3363            if (tests.file_is_test) {
3364                var test = tests.tests[0];
3365                if (test.phase >= test.phases.HAS_RESULT) {
3366                    return;
3367                }
3368                test.set_status(test.FAIL, e.message, stack);
3369                test.phase = test.phases.HAS_RESULT;
3370                // The following function invocation is superfluous.
3371                // TODO: Remove.
3372                test.done();
3373            } else if (!tests.allow_uncaught_exception) {
3374                tests.status.status = tests.status.ERROR;
3375                tests.status.message = e.message;
3376                tests.status.stack = stack;
3377            }
3378            done();
3379        };
3380
3381        addEventListener("error", error_handler, false);
3382        addEventListener("unhandledrejection", function(e){ error_handler(e.reason); }, false);
3383    }
3384
3385    test_environment.on_tests_ready();
3386
3387    /**
3388     * Stylesheet
3389     */
3390     var stylesheetContent = "\
3391html {\
3392    font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;\
3393}\
3394\
3395#log .warning,\
3396#log .warning a {\
3397  color: black;\
3398  background: yellow;\
3399}\
3400\
3401#log .error,\
3402#log .error a {\
3403  color: white;\
3404  background: red;\
3405}\
3406\
3407section#summary {\
3408    margin-bottom:1em;\
3409}\
3410\
3411table#results {\
3412    border-collapse:collapse;\
3413    table-layout:fixed;\
3414    width:100%;\
3415}\
3416\
3417table#results th:first-child,\
3418table#results td:first-child {\
3419    width:4em;\
3420}\
3421\
3422table#results th:last-child,\
3423table#results td:last-child {\
3424    width:50%;\
3425}\
3426\
3427table#results.assertions th:last-child,\
3428table#results.assertions td:last-child {\
3429    width:35%;\
3430}\
3431\
3432table#results th {\
3433    padding:0;\
3434    padding-bottom:0.5em;\
3435    border-bottom:medium solid black;\
3436}\
3437\
3438table#results td {\
3439    padding:1em;\
3440    padding-bottom:0.5em;\
3441    border-bottom:thin solid black;\
3442}\
3443\
3444tr.pass > td:first-child {\
3445    color:green;\
3446}\
3447\
3448tr.fail > td:first-child {\
3449    color:red;\
3450}\
3451\
3452tr.timeout > td:first-child {\
3453    color:red;\
3454}\
3455\
3456tr.notrun > td:first-child {\
3457    color:blue;\
3458}\
3459\
3460.pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child {\
3461    font-variant:small-caps;\
3462}\
3463\
3464table#results span {\
3465    display:block;\
3466}\
3467\
3468table#results span.expected {\
3469    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
3470    white-space:pre;\
3471}\
3472\
3473table#results span.actual {\
3474    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
3475    white-space:pre;\
3476}\
3477\
3478span.ok {\
3479    color:green;\
3480}\
3481\
3482tr.error {\
3483    color:red;\
3484}\
3485\
3486span.timeout {\
3487    color:red;\
3488}\
3489\
3490span.ok, span.timeout, span.error {\
3491    font-variant:small-caps;\
3492}\
3493";
3494
3495})(this);
3496// vim: set expandtab shiftwidth=4 tabstop=4:
3497