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