2 /*jshint latedef: nofunc*/
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 */
18 // default timeout is 10 seconds, test can override if needed
28 var xhtml_ns = "http://www.w3.org/1999/xhtml";
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:
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();
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);
44 * // Should return a new unique default test name.
45 * DOMString next_default_test_name();
47 * // Should return the test harness timeout duration in milliseconds.
48 * float test_timeout();
50 * // Should return the global scope object.
51 * object global_scope();
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.
61 function WindowTestEnvironment() {
62 this.name_counter = 0;
63 this.window_cache = null;
64 this.output_handler = null;
65 this.all_loaded = false;
67 on_event(window, 'load', function() {
68 this_obj.all_loaded = true;
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) {
77 w[selector].apply(undefined, callback_args);
84 if (supports_post_message(w) && w !== self) {
85 w.postMessage(message_arg, "*");
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
95 var cache = this.window_cache;
97 cache = [[self, true]];
101 var origins = location.ancestorOrigins;
102 while (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.
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.
116 so = (location.origin == origins[i]);
118 so = is_same_origin(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)]);
130 this.window_cache = cache;
135 callback.apply(null, a);
139 WindowTestEnvironment.prototype.on_tests_ready = function() {
140 var output = new Output();
141 this.output_handler = output;
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 });
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() });
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() });
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() });
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 : "";
174 return prefix + suffix;
177 WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
178 this.output_handler.setup(properties);
181 WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
182 on_event(window, 'load', callback);
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;
195 return settings.harness_timeout.normal;
198 WindowTestEnvironment.prototype.global_scope = function() {
203 * Base TestEnvironment implementation for a generic web worker.
205 * Workers accumulate test results. One or more clients can connect and
206 * retrieve results from a worker at any time.
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
214 * A client document using testharness can use fetch_tests_from_worker() to
215 * retrieve results from a worker. See apisample16.html.
217 function WorkerTestEnvironment() {
218 this.name_counter = 0;
219 this.all_loaded = true;
220 this.message_list = [];
221 this.message_ports = [];
224 WorkerTestEnvironment.prototype._dispatch = function(message) {
225 this.message_list.push(message);
226 for (var i = 0; i < this.message_ports.length; ++i)
228 this.message_ports[i].postMessage(message);
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)
238 port.postMessage(this.message_list[i]);
242 WorkerTestEnvironment.prototype.next_default_test_name = function() {
243 var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
245 return "Untitled" + suffix;
248 WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
250 WorkerTestEnvironment.prototype.on_tests_ready = function() {
253 function(properties) {
256 properties: properties,
259 add_test_state_callback(
263 test: test.structured_clone()
270 test: test.structured_clone()
273 add_completion_callback(
274 function(tests, harness_status) {
279 return test.structured_clone();
281 status: harness_status.structured_clone()
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.
294 WorkerTestEnvironment.prototype.global_scope = function() {
299 * Dedicated web workers.
300 * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
302 * This class is used as the test_environment when testharness is running
303 * inside a dedicated worker.
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);
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;
322 * Shared web workers.
323 * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
325 * This class is used as the test_environment when testharness is running
326 * inside a shared web worker.
328 function SharedWorkerTestEnvironment() {
329 WorkerTestEnvironment.call(this);
331 // Shared workers receive message ports via the 'onconnect' event for
333 self.addEventListener("connect",
334 function(message_event) {
335 this_obj._add_message_port(message_event.source);
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;
349 * http://www.w3.org/TR/service-workers/
351 * This class is used as the test_environment when testharness is running
352 * inside a service worker.
354 function ServiceWorkerTestEnvironment() {
355 WorkerTestEnvironment.call(this);
356 this.all_loaded = false;
357 this.on_loaded_callback = null;
359 self.addEventListener("message",
361 if (event.data.type && event.data.type === "connect") {
362 this_obj._add_message_port(event.ports[0]);
363 event.ports[0].start();
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",
374 this_obj.all_loaded = true;
375 if (this_obj.on_loaded_callback) {
376 this_obj.on_loaded_callback();
380 ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
382 ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
383 if (this.all_loaded) {
386 this.on_loaded_callback = callback;
390 function create_test_environment() {
391 if ('document' in self) {
392 return new WindowTestEnvironment();
394 if ('DedicatedWorkerGlobalScope' in self &&
395 self instanceof DedicatedWorkerGlobalScope) {
396 return new DedicatedWorkerTestEnvironment();
398 if ('SharedWorkerGlobalScope' in self &&
399 self instanceof SharedWorkerGlobalScope) {
400 return new SharedWorkerTestEnvironment();
402 if ('ServiceWorkerGlobalScope' in self &&
403 self instanceof ServiceWorkerGlobalScope) {
404 return new ServiceWorkerTestEnvironment();
406 throw new Error("Unsupported test environment");
409 var test_environment = create_test_environment();
411 function is_shared_worker(worker) {
412 return 'SharedWorker' in self && worker instanceof SharedWorker;
415 function is_service_worker(worker) {
416 return 'ServiceWorker' in self && worker instanceof ServiceWorker;
423 function test(func, name, properties)
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) {
434 function async_test(func, name, properties)
436 if (typeof func !== "function") {
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);
445 test_obj.step(func, test_obj, test_obj);
450 function promise_test(func, name, properties) {
451 var test = async_test(name, properties);
452 Promise.resolve(test.step(func, test, test))
457 .catch(test.step_func(
459 if (value instanceof AssertionError) {
462 assert(false, "promise_test", null,
463 "Unhandled rejection with value: ${value}", {value:value});
467 function setup(func_or_properties, maybe_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;
477 properties = func_or_properties;
479 tests.setup(func, properties);
480 test_environment.on_new_harness_properties(properties);
484 if (tests.tests.length === 0) {
485 tests.set_file_is_test();
487 if (tests.file_is_test) {
488 tests.tests[0].done();
493 function generate_tests(func, args, properties) {
494 forEach(args, function(x, i)
499 func.apply(this, x.slice(1));
502 Array.isArray(properties) ? properties[i] : properties);
506 function on_event(object, event, callback)
508 object.addEventListener(event, callback, false);
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');
520 * Return a string truncated to the given length, with ... added at the end
523 function truncate(s, len)
525 if (s.length > len) {
526 return s.substring(0, len - 3) + "...";
532 * Return true if object is probably a Node object.
534 function is_node(object)
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) {
547 // The object is probably Node.prototype or another prototype
548 // object that inherits from it, and not a Node instance.
557 * Convert a value to a nice, human-readable string
559 function format_value(val, seen)
564 if (typeof val === "object" && val !== null) {
565 if (seen.indexOf(val) >= 0) {
570 if (Array.isArray(val)) {
571 return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]";
574 switch (typeof val) {
576 val = val.replace("\\", "\\\\");
577 for (var i = 0; i < 32; 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;
613 val = val.replace(RegExp(String.fromCharCode(i), "g"), replace);
615 return '"' + val.replace(/"/g, '\\"') + '"';
620 // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
622 if (val === -0 && 1/val === -Infinity) {
631 // Special-case Node objects, since those come up a lot in my tests. I
632 // ignore namespaces.
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 + '"';
640 ret += ">" + val.innerHTML + "</" + val.localName + ">";
641 return "Element node " + truncate(ret, 60);
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");
655 return "Node object of unknown type";
661 return typeof val + ' "' + truncate(String(val), 60) + '"';
664 expose(format_value, "format_value");
670 function assert_true(actual, description)
672 assert(actual === true, "assert_true", description,
673 "expected true got ${actual}", {actual:actual});
675 expose(assert_true, "assert_true");
677 function assert_false(actual, description)
679 assert(actual === false, "assert_false", description,
680 "expected false got ${actual}", {actual:actual});
682 expose(assert_false, "assert_false");
684 function same_value(x, y) {
689 if (x === 0 && y === 0) {
690 //Distinguish +0 and -0
696 function assert_equals(actual, expected, description)
699 * Test if two primitives are equal or two objects
700 * are the same object
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});
708 assert(same_value(actual, expected), "assert_equals", description,
709 "expected ${expected} but got ${actual}",
710 {expected:expected, actual:actual});
712 expose(assert_equals, "assert_equals");
714 function assert_not_equals(actual, expected, description)
717 * Test if two primitives are unequal or two objects
718 * are different objects
720 assert(!same_value(actual, expected), "assert_not_equals", description,
721 "got disallowed value ${actual}",
724 expose(assert_not_equals, "assert_not_equals");
726 function assert_in_array(actual, expected, description)
728 assert(expected.indexOf(actual) != -1, "assert_in_array", description,
729 "value ${actual} not in array ${expected}",
730 {actual:actual, expected:expected});
732 expose(assert_in_array, "assert_in_array");
734 function assert_object_equals(actual, expected, description)
736 //This needs to be improved a great deal
737 function check_equal(actual, expected, stack)
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);
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});
756 for (p in expected) {
757 assert(actual.hasOwnProperty(p),
758 "assert_object_equals", description,
759 "expected property ${p} missing", {p:p});
763 check_equal(actual, expected, []);
765 expose(assert_object_equals, "assert_object_equals");
767 function assert_array_equals(actual, expected, description)
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]});
786 expose(assert_array_equals, "assert_array_equals");
788 function assert_approx_equals(actual, expected, epsilon, description)
791 * Test if two primitive numbers are equal withing +/- epsilon
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});
803 expose(assert_approx_equals, "assert_approx_equals");
805 function assert_less_than(actual, expected, description)
808 * Test if a primitive number is less than another
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});
820 expose(assert_less_than, "assert_less_than");
822 function assert_greater_than(actual, expected, description)
825 * Test if a primitive number is greater than another
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});
837 expose(assert_greater_than, "assert_greater_than");
839 function assert_less_than_equal(actual, expected, description)
842 * Test if a primitive number is less than or equal to another
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});
854 expose(assert_less_than_equal, "assert_less_than_equal");
856 function assert_greater_than_equal(actual, expected, description)
859 * Test if a primitive number is greater than or equal to another
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});
871 expose(assert_greater_than_equal, "assert_greater_than_equal");
873 function assert_regexp_match(actual, expected, description) {
875 * Test if a string (actual) matches a regexp (expected)
877 assert(expected.test(actual),
878 "assert_regexp_match", description,
879 "expected ${expected} but got ${actual}",
880 {expected:expected, actual:actual});
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 + "]",
888 expose(assert_class_string, "assert_class_string");
891 function _assert_own_property(name) {
892 return function(object, property_name, description)
894 assert(object.hasOwnProperty(property_name),
896 "expected property ${p} missing", {p:property_name});
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)
904 assert(!object.hasOwnProperty(property_name),
905 "assert_not_exists", description,
906 "unexpected property ${p} found", {p:property_name});
908 expose(assert_not_exists, "assert_not_exists");
910 function _assert_inherits(name) {
911 return function (object, property_name, description)
913 assert(typeof object === "object",
915 "provided value is not an object");
917 assert("hasOwnProperty" in object,
919 "provided value is an object but has no hasOwnProperty method");
921 assert(!object.hasOwnProperty(property_name),
923 "property ${p} found on object expected in prototype chain",
926 assert(property_name in object,
928 "property ${p} not found in prototype chain",
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)
937 var initial_value = object[property_name];
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",
947 object[property_name] = initial_value;
950 expose(assert_readonly, "assert_readonly");
952 function assert_throws(code, func, description)
956 assert(false, "assert_throws", description,
957 "${func} did not throw", {func:func});
959 if (e instanceof AssertionError) {
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,
971 expected_name:code.name});
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'
999 var name = code in code_name_map ? code_name_map[code] : code;
1001 var name_code_map = {
1003 HierarchyRequestError: 3,
1004 WrongDocumentError: 4,
1005 InvalidCharacterError: 5,
1006 NoModificationAllowedError: 7,
1008 NotSupportedError: 9,
1009 InvalidStateError: 11,
1011 InvalidModificationError: 13,
1013 InvalidAccessError: 15,
1014 TypeMismatchError: 17,
1018 URLMismatchError: 21,
1019 QuotaExceededError: 22,
1021 InvalidNodeTypeError: 24,
1027 TransactionInactiveError: 0,
1032 if (!(name in name_code_map)) {
1033 throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
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;
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]});
1062 expose(assert_throws, "assert_throws");
1064 function assert_unreached(description) {
1065 assert(false, "assert_unreached", description,
1066 "Reached unreachable code");
1068 expose(assert_unreached, "assert_unreached");
1070 function assert_any(assert_func, actual, expected_array)
1072 var args = [].slice.call(arguments, 3);
1075 forEach(expected_array,
1079 assert_func.apply(this, [actual, expected].concat(args));
1082 errors.push(e.message);
1086 throw new AssertionError(errors.join("\n\n"));
1089 expose(assert_any, "assert_any");
1091 function Test(name, properties)
1093 if (tests.file_is_test && tests.tests.length) {
1094 throw new Error("Tried to create a test with file_is_test");
1098 this.phase = this.phases.INITIAL;
1100 this.status = this.NOTRUN;
1101 this.timeout_id = 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;
1109 this.timeout_length = null;
1112 this.message = null;
1116 this.cleanup_callbacks = [];
1128 Test.prototype = merge({}, Test.statuses);
1130 Test.prototype.phases = {
1137 Test.prototype.structured_clone = function()
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),
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;
1153 Test.prototype.step = function(func, this_obj)
1155 if (this.phase > this.phases.STARTED) {
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) {
1169 this.steps.push(func);
1171 if (arguments.length === 1) {
1176 return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
1178 if (this.phase >= this.phases.HAS_RESULT) {
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 + ")";
1189 this.set_status(this.FAIL, message);
1190 this.phase = this.phases.HAS_RESULT;
1195 Test.prototype.step_func = function(func, this_obj)
1197 var test_this = this;
1199 if (arguments.length === 1) {
1200 this_obj = test_this;
1205 return test_this.step.apply(test_this, [func, this_obj].concat(
1206 Array.prototype.slice.call(arguments)));
1210 Test.prototype.step_func_done = function(func, this_obj)
1212 var test_this = this;
1214 if (arguments.length === 1) {
1215 this_obj = test_this;
1221 test_this.step.apply(test_this, [func, this_obj].concat(
1222 Array.prototype.slice.call(arguments)));
1228 Test.prototype.unreached_func = function(description)
1230 return this.step_func(function() {
1231 assert_unreached(description);
1235 Test.prototype.add_cleanup = function(callback) {
1236 this.cleanup_callbacks.push(callback);
1239 Test.prototype.force_timeout = function() {
1240 this.set_status(this.TIMEOUT);
1241 this.phase = this.phases.HAS_RESULT;
1244 Test.prototype.set_timeout = function()
1246 if (this.timeout_length !== null) {
1247 var this_obj = this;
1248 this.timeout_id = setTimeout(function()
1251 }, this.timeout_length);
1255 Test.prototype.set_status = function(status, message)
1257 this.status = status;
1258 this.message = message;
1261 Test.prototype.timeout = function()
1263 this.timeout_id = null;
1264 this.set_status(this.TIMEOUT, "Test timed out");
1265 this.phase = this.phases.HAS_RESULT;
1269 Test.prototype.done = function()
1271 if (this.phase == this.phases.COMPLETE) {
1275 if (this.phase <= this.phases.STARTED) {
1276 this.set_status(this.PASS, null);
1279 this.phase = this.phases.COMPLETE;
1281 clearTimeout(this.timeout_id);
1286 Test.prototype.cleanup = function() {
1287 forEach(this.cleanup_callbacks,
1288 function(cleanup_callback) {
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.
1300 function RemoteTest(clone) {
1301 var this_obj = this;
1302 Object.keys(clone).forEach(
1304 this_obj[key] = clone[key];
1307 this.phase = this.phases.INITIAL;
1308 this.update_state_from(clone);
1312 RemoteTest.prototype.structured_clone = function() {
1314 Object.keys(this).forEach(
1316 if (typeof(this[key]) === "object") {
1317 clone[key] = merge({}, this[key]);
1319 clone[key] = this[key];
1322 clone.phases = merge({}, this.phases);
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;
1335 RemoteTest.prototype.done = function() {
1336 this.phase = this.phases.COMPLETE;
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.
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); };
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;
1366 message_port = worker;
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 =
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);
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
1389 status: tests.status.ERROR,
1390 message: "Error in worker" + filename + ": " + message
1393 error.preventDefault();
1396 RemoteWorker.prototype.test_state = function(data) {
1397 var remote_test = this.tests[data.test.index];
1399 remote_test = new RemoteTest(data.test);
1400 this.tests[data.test.index] = remote_test;
1402 remote_test.update_state_from(data.test);
1403 tests.notify_test_state(remote_test);
1406 RemoteWorker.prototype.test_done = function(data) {
1407 var remote_test = this.tests[data.test.index];
1408 remote_test.update_state_from(data.test);
1410 tests.result(remote_test);
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;
1419 this.running = false;
1421 if (tests.all_done()) {
1426 RemoteWorker.prototype.message_handlers = {
1427 test_state: RemoteWorker.prototype.test_state,
1428 result: RemoteWorker.prototype.test_done,
1429 complete: RemoteWorker.prototype.worker_done
1436 function TestsStatus()
1439 this.message = null;
1442 TestsStatus.statuses = {
1448 TestsStatus.prototype = merge({}, TestsStatus.statuses);
1450 TestsStatus.prototype.structured_clone = function()
1452 if (!this._structured_clone) {
1453 var msg = this.message;
1454 msg = msg ? String(msg) : msg;
1455 this._structured_clone = merge({
1458 }, TestsStatus.statuses);
1460 return this._structured_clone;
1466 this.num_pending = 0;
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();
1510 Tests.prototype.setup = function(func, properties)
1512 if (this.phase >= this.phases.HAVE_RESULTS) {
1516 if (this.phase < this.phases.SETUP) {
1517 this.phase = this.phases.SETUP;
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)
1533 clearTimeout(this.timeout_id);
1535 } else if (p == "timeout_multiplier") {
1536 this.timeout_multiplier = value;
1545 this.status.status = this.status.ERROR;
1546 this.status.message = String(e);
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");
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
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() {
1568 }, this.timeout_length);
1572 Tests.prototype.timeout = function() {
1573 if (this.status.status === null) {
1574 this.status.status = this.status.TIMEOUT;
1579 Tests.prototype.end_wait = function()
1581 this.wait_for_finish = false;
1582 if (this.all_done()) {
1587 Tests.prototype.push = function(test)
1589 if (this.phase < this.phases.HAVE_TESTS) {
1593 test.index = this.tests.push(test);
1594 this.notify_test_state(test);
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);
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; }));
1612 Tests.prototype.start = function() {
1613 this.phase = this.phases.HAVE_TESTS;
1614 this.notify_start();
1617 Tests.prototype.notify_start = function() {
1618 var this_obj = this;
1619 forEach (this.start_callbacks,
1622 callback(this_obj.properties);
1626 Tests.prototype.result = function(test)
1628 if (this.phase > this.phases.HAVE_RESULTS) {
1631 this.phase = this.phases.HAVE_RESULTS;
1633 this.notify_result(test);
1636 Tests.prototype.notify_result = function(test) {
1637 var this_obj = this;
1638 this.processing_callbacks = true;
1639 forEach(this.test_done_callbacks,
1642 callback(test, this_obj);
1644 this.processing_callbacks = false;
1645 if (this_obj.all_done()) {
1646 this_obj.complete();
1650 Tests.prototype.complete = function() {
1651 if (this.phase === this.phases.COMPLETE) {
1654 this.phase = this.phases.COMPLETE;
1655 var this_obj = this;
1659 if (x.phase < x.phases.COMPLETE) {
1660 this_obj.notify_result(x);
1662 x.phase = x.phases.COMPLETE;
1666 this.notify_complete();
1669 Tests.prototype.notify_complete = function() {
1670 var this_obj = this;
1671 if (this.status.status === null) {
1672 this.status.status = this.status.OK;
1675 forEach (this.all_done_callbacks,
1678 callback(this_obj.tests, this_obj.status);
1682 Tests.prototype.fetch_tests_from_worker = function(worker) {
1683 if (this.phase >= this.phases.COMPLETE) {
1687 this.pending_workers.push(new RemoteWorker(worker));
1690 function fetch_tests_from_worker(port) {
1691 tests.fetch_tests_from_worker(port);
1693 expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
1695 function timeout() {
1696 if (tests.timeout_length === null) {
1700 expose(timeout, 'timeout');
1702 function add_start_callback(callback) {
1703 tests.start_callbacks.push(callback);
1706 function add_test_state_callback(callback) {
1707 tests.test_state_callbacks.push(callback);
1710 function add_result_callback(callback)
1712 tests.test_done_callbacks.push(callback);
1715 function add_completion_callback(callback)
1717 tests.all_done_callbacks.push(callback);
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');
1730 this.output_document = document;
1731 this.output_node = null;
1732 this.enabled = settings.output;
1733 this.phase = this.INITIAL;
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) {
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);
1752 Output.prototype.init = function(properties) {
1753 if (this.phase >= this.STARTED) {
1756 if (properties.output_document) {
1757 this.output_document = properties.output_document;
1759 this.output_document = document;
1761 this.phase = this.STARTED;
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);
1769 output_document = this.output_document;
1771 if (!output_document) {
1774 var node = output_document.getElementById("log");
1776 if (!document.body || document.readyState == "loading") {
1779 node = output_document.createElement("div");
1781 output_document.body.appendChild(node);
1783 this.output_document = output_document;
1784 this.output_node = node;
1787 Output.prototype.show_status = function() {
1788 if (this.phase < this.STARTED) {
1791 if (!this.enabled) {
1794 if (this.phase < this.HAVE_RESULTS) {
1796 this.phase = this.HAVE_RESULTS;
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";
1810 Output.prototype.show_results = function (tests, harness_status) {
1811 if (this.phase >= this.COMPLETE) {
1814 if (!this.enabled) {
1817 if (!this.output_node) {
1820 this.phase = this.COMPLETE;
1822 var log = this.output_node;
1826 var output_document = this.output_document;
1828 while (log.lastChild) {
1829 log.removeChild(log.lastChild);
1832 var script_prefix = null;
1833 var scripts = document.getElementsByTagName("script");
1834 for (var i = 0; i < scripts.length; i++) {
1836 if (scripts[i].src) {
1837 src = scripts[i].src;
1838 } else if (scripts[i].href) {
1840 src = scripts[i].href.baseVal;
1843 var matches = src && src.match(/^(.*\/|)testharness\.js$/);
1845 script_prefix = matches[1];
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");
1856 heads[0].appendChild(stylesheet);
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 = {};
1874 var status = status_text[test.status];
1875 if (status_number.hasOwnProperty(status)) {
1876 status_number[status] += 1;
1878 status_number[status] = 1;
1882 function status_class(status)
1884 return status.replace(/\s/g, '').toLowerCase();
1887 var summary_template = ["section", {"id":"summary"},
1888 ["h2", {}, "Summary"],
1892 var status = status_text_harness[harness_status.status];
1893 var rv = [["section", {},
1896 ["span", {"class":status_class(status)},
1902 if (harness_status.status === harness_status.ERROR) {
1903 rv[0].push(["pre", {}, harness_status.message]);
1907 ["p", {}, "Found ${num_tests} tests"],
1909 var rv = [["div", {}]];
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)},
1916 ["input", {type:"checkbox", checked:"checked"}],
1917 status_number[status] + " " + status]]);
1925 log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
1927 forEach(output_document.querySelectorAll("section#summary label"),
1930 on_event(element, "click",
1933 if (output_document.getElementById("results") === null) {
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);
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)
1957 return s.replace(/\&/g, "&")
1958 .replace(/</g, "<")
1959 .replace(/"/g, """)
1960 .replace(/'/g, "'");
1963 function has_assertions()
1965 for (var i = 0; i < tests.length; i++) {
1966 if (tests[i].properties.hasOwnProperty("assert")) {
1973 function get_assertion(test)
1975 if (test.properties.hasOwnProperty("assert")) {
1976 if (Array.isArray(test.properties.assert)) {
1977 return test.properties.assert.join(' ');
1979 return test.properties.assert;
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>" +
1991 for (var i = 0; i < tests.length; i++) {
1992 html += '<tr class="' +
1993 escape_html(status_class(status_text[tests[i].status])) +
1995 escape_html(status_text[tests[i].status]) +
1997 escape_html(tests[i].name) +
1999 (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") +
2000 escape_html(tests[i].message ? tests[i].message : " ") +
2003 html += "</tbody></table>";
2005 log.lastChild.innerHTML = html;
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;
2017 * A template is just a javascript structure. An element is represented as:
2019 * [tag_name, {attr_name:attr_value}, child1, child2]
2021 * the children can either be strings (which act like text nodes), other templates or
2022 * functions (see below)
2024 * A text node is represented as
2028 * String values have a simple substitution syntax; ${foo} represents a variable foo.
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.
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]
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
2042 * substitute(template, substitutions) - take a template and variable mapping object,
2043 * make the variable substitutions and return the substituted template
2047 function is_single_node(template)
2049 return typeof template[0] === "string";
2052 function substitute(template, substitutions)
2054 if (typeof template === "function") {
2055 var replacement = template(substitutions);
2060 return substitute(replacement, substitutions);
2063 if (is_single_node(template)) {
2064 return substitute_single(template, substitutions);
2067 return filter(map(template, function(x) {
2068 return substitute(x, substitutions);
2069 }), function(x) {return x !== null;});
2072 function substitute_single(template, substitutions)
2074 var substitution_re = /\$\{([^ }]*)\}/g;
2076 function do_substitution(input) {
2077 var components = input.split(substitution_re);
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]]));
2088 function substitute_attrs(attrs, rv)
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;
2100 function substitute_children(children, rv)
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);
2109 extend(rv, replacement);
2113 extend(rv, do_substitution(String(children[i])));
2120 rv.push(do_substitution(String(template[0])).join(""));
2122 if (template[0] === "{text}") {
2123 substitute_children(template.slice(1), rv);
2125 substitute_attrs(template[1], rv);
2126 substitute_children(template.slice(2), rv);
2132 function make_dom_single(template, doc)
2134 var output_document = doc || document;
2136 if (template[0] === "{text}") {
2137 element = output_document.createTextNode("");
2138 for (var i = 1; i < template.length; i++) {
2139 element.data += template[i];
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]);
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);
2153 var text_node = output_document.createTextNode(template[i]);
2154 element.appendChild(text_node);
2162 function make_dom(template, substitutions, output_document)
2164 if (is_single_node(template)) {
2165 return make_dom_single(template, output_document);
2168 return map(template, function(x) {
2169 return make_dom_single(x, output_document);
2173 function render(template, substitutions, output_document)
2175 return make_dom(substitute(template, substitutions), output_document);
2181 function assert(expected_true, function_name, description, error, substitutions)
2183 if (tests.tests.length === 0) {
2184 tests.set_file_is_test();
2186 if (expected_true !== true) {
2187 var msg = make_message(function_name, description,
2188 error, substitutions);
2189 throw new AssertionError(msg);
2193 function AssertionError(message)
2195 this.message = message;
2198 AssertionError.prototype.toString = function() {
2199 return this.message;
2202 function make_message(function_name, description, error, substitutions)
2204 for (var p in substitutions) {
2205 if (substitutions.hasOwnProperty(p)) {
2206 substitutions[p] = format_value(substitutions[p]);
2209 var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
2210 merge({function_name:function_name,
2211 description:(description?description + " ":"")},
2213 return node_form.slice(1).join("");
2216 function filter(array, callable, thisObj) {
2218 for (var i = 0; i < array.length; i++) {
2219 if (array.hasOwnProperty(i)) {
2220 var pass = callable.call(thisObj, array[i], i, array);
2229 function map(array, callable, thisObj)
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);
2241 function extend(array, items)
2243 Array.prototype.push.apply(array, items);
2246 function forEach (array, callback, thisObj)
2248 for (var i = 0; i < array.length; i++) {
2249 if (array.hasOwnProperty(i)) {
2250 callback.call(thisObj, array[i], i, array);
2268 function expose(object, name)
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]] = {};
2276 target = target[components[i]];
2278 target[components[components.length - 1]] = object;
2281 function is_same_origin(w) {
2290 function supports_post_message(w)
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.
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.
2303 type = typeof w.postMessage;
2304 if (type === "function") {
2308 // IE8 supports postMessage, but implements it as a host object which
2309 // returns "object" as its `typeof`.
2310 else if (type === "object") {
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).
2320 // This is the case where postMessage isn't supported AND accessing a
2321 // window property across origins throws (e.g. old Firefox browser).
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) {
2339 var message = e.message;
2340 test.set_status(test.FAIL, message);
2341 test.phase = test.phases.HAS_RESULT;
2344 } else if (!tests.allow_uncaught_exception) {
2345 tests.status.status = tests.status.ERROR;
2346 tests.status.message = e.message;
2350 test_environment.on_tests_ready();
2353 // vim: set expandtab shiftwidth=4 tabstop=4: