diff options
Diffstat (limited to 'dom/imptests/testharness.js')
-rw-r--r-- | dom/imptests/testharness.js | 2657 |
1 files changed, 2657 insertions, 0 deletions
diff --git a/dom/imptests/testharness.js b/dom/imptests/testharness.js new file mode 100644 index 0000000000..27ce673e91 --- /dev/null +++ b/dom/imptests/testharness.js @@ -0,0 +1,2657 @@ +/*global self*/ +/*jshint latedef: nofunc*/ +/* +Distributed under both the W3C Test Suite License [1] and the W3C +3-clause BSD License [2]. To contribute to a W3C Test Suite, see the +policies and contribution forms [3]. + +[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license +[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license +[3] http://www.w3.org/2004/10/27-testcases +*/ + +/* Documentation is in docs/api.md */ + +(function () +{ + var debug = false; + // default timeout is 10 seconds, test can override if needed + var settings = { + output:true, + harness_timeout:{ + "normal":10000, + "long":60000 + }, + test_timeout:null, + message_events: ["start", "test_state", "result", "completion"] + }; + + var xhtml_ns = "http://www.w3.org/1999/xhtml"; + + /* + * TestEnvironment is an abstraction for the environment in which the test + * harness is used. Each implementation of a test environment has to provide + * the following interface: + * + * interface TestEnvironment { + * // Invoked after the global 'tests' object has been created and it's + * // safe to call add_*_callback() to register event handlers. + * void on_tests_ready(); + * + * // Invoked after setup() has been called to notify the test environment + * // of changes to the test harness properties. + * void on_new_harness_properties(object properties); + * + * // Should return a new unique default test name. + * DOMString next_default_test_name(); + * + * // Should return the test harness timeout duration in milliseconds. + * float test_timeout(); + * + * // Should return the global scope object. + * object global_scope(); + * }; + */ + + /* + * A test environment with a DOM. The global object is 'window'. By default + * test results are displayed in a table. Any parent windows receive + * callbacks or messages via postMessage() when test events occur. See + * apisample11.html and apisample12.html. + */ + function WindowTestEnvironment() { + this.name_counter = 0; + this.window_cache = null; + this.output_handler = null; + this.all_loaded = false; + var this_obj = this; + this.message_events = []; + + this.message_functions = { + start: [add_start_callback, remove_start_callback, + function (properties) { + this_obj._dispatch("start_callback", [properties], + {type: "start", properties: properties}); + }], + + test_state: [add_test_state_callback, remove_test_state_callback, + function(test) { + this_obj._dispatch("test_state_callback", [test], + {type: "test_state", + test: test.structured_clone()}); + }], + result: [add_result_callback, remove_result_callback, + function (test) { + this_obj.output_handler.show_status(); + this_obj._dispatch("result_callback", [test], + {type: "result", + test: test.structured_clone()}); + }], + completion: [add_completion_callback, remove_completion_callback, + function (tests, harness_status) { + var cloned_tests = map(tests, function(test) { + return test.structured_clone(); + }); + this_obj._dispatch("completion_callback", [tests, harness_status], + {type: "complete", + tests: cloned_tests, + status: harness_status.structured_clone()}); + }] + } + + on_event(window, 'load', function() { + this_obj.all_loaded = true; + }); + } + + WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) { + this._forEach_windows( + function(w, same_origin) { + if (same_origin) { + try { + var has_selector = selector in w; + } catch(e) { + // If document.domain was set at some point same_origin can be + // wrong and the above will fail. + has_selector = false; + } + if (has_selector) { + try { + w[selector].apply(undefined, callback_args); + } catch (e) { + if (debug) { + throw e; + } + } + } + } + if (supports_post_message(w) && w !== self) { + w.postMessage(message_arg, "*"); + } + }); + }; + + WindowTestEnvironment.prototype._forEach_windows = function(callback) { + // Iterate of the the windows [self ... top, opener]. The callback is passed + // two objects, the first one is the windows object itself, the second one + // is a boolean indicating whether or not its on the same origin as the + // current window. + var cache = this.window_cache; + if (!cache) { + cache = [[self, true]]; + var w = self; + var i = 0; + var so; + var origins = location.ancestorOrigins; + while (w != w.parent) { + w = w.parent; + // In WebKit, calls to parent windows' properties that aren't on the same + // origin cause an error message to be displayed in the error console but + // don't throw an exception. This is a deviation from the current HTML5 + // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504 + // The problem with WebKit's behavior is that it pollutes the error console + // with error messages that can't be caught. + // + // This issue can be mitigated by relying on the (for now) proprietary + // `location.ancestorOrigins` property which returns an ordered list of + // the origins of enclosing windows. See: + // http://trac.webkit.org/changeset/113945. + if (origins) { + so = (location.origin == origins[i]); + } else { + so = is_same_origin(w); + } + cache.push([w, so]); + i++; + } + w = window.opener; + if (w) { + // window.opener isn't included in the `location.ancestorOrigins` prop. + // We'll just have to deal with a simple check and an error msg on WebKit + // browsers in this case. + cache.push([w, is_same_origin(w)]); + } + this.window_cache = cache; + } + + forEach(cache, + function(a) { + callback.apply(null, a); + }); + }; + + WindowTestEnvironment.prototype.on_tests_ready = function() { + var output = new Output(); + this.output_handler = output; + + var this_obj = this; + + add_start_callback(function (properties) { + this_obj.output_handler.init(properties); + }); + + add_test_state_callback(function(test) { + this_obj.output_handler.show_status(); + }); + + add_result_callback(function (test) { + this_obj.output_handler.show_status(); + }); + + add_completion_callback(function (tests, harness_status) { + this_obj.output_handler.show_results(tests, harness_status); + }); + this.setup_messages(settings.message_events); + }; + + WindowTestEnvironment.prototype.setup_messages = function(new_events) { + var this_obj = this; + forEach(settings.message_events, function(x) { + var current_dispatch = this_obj.message_events.includes(x); + var new_dispatch = new_events.includes(x); + if (!current_dispatch && new_dispatch) { + this_obj.message_functions[x][0](this_obj.message_functions[x][2]); + } else if (current_dispatch && !new_dispatch) { + this_obj.message_functions[x][1](this_obj.message_functions[x][2]); + } + }); + this.message_events = new_events; + } + + WindowTestEnvironment.prototype.next_default_test_name = function() { + //Don't use document.title to work around an Opera bug in XHTML documents + var title = document.getElementsByTagName("title")[0]; + var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled"; + var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; + this.name_counter++; + return prefix + suffix; + }; + + WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) { + this.output_handler.setup(properties); + if (properties.hasOwnProperty("message_events")) { + this.setup_messages(properties.message_events); + } + }; + + WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + on_event(window, 'load', callback); + }; + + WindowTestEnvironment.prototype.test_timeout = function() { + var metas = document.getElementsByTagName("meta"); + for (var i = 0; i < metas.length; i++) { + if (metas[i].name == "timeout") { + if (metas[i].content == "long") { + return settings.harness_timeout.long; + } + break; + } + } + return settings.harness_timeout.normal; + }; + + WindowTestEnvironment.prototype.global_scope = function() { + return window; + }; + + /* + * Base TestEnvironment implementation for a generic web worker. + * + * Workers accumulate test results. One or more clients can connect and + * retrieve results from a worker at any time. + * + * WorkerTestEnvironment supports communicating with a client via a + * MessagePort. The mechanism for determining the appropriate MessagePort + * for communicating with a client depends on the type of worker and is + * implemented by the various specializations of WorkerTestEnvironment + * below. + * + * A client document using testharness can use fetch_tests_from_worker() to + * retrieve results from a worker. See apisample16.html. + */ + function WorkerTestEnvironment() { + this.name_counter = 0; + this.all_loaded = true; + this.message_list = []; + this.message_ports = []; + } + + WorkerTestEnvironment.prototype._dispatch = function(message) { + this.message_list.push(message); + for (var i = 0; i < this.message_ports.length; ++i) + { + this.message_ports[i].postMessage(message); + } + }; + + // The only requirement is that port has a postMessage() method. It doesn't + // have to be an instance of a MessagePort, and often isn't. + WorkerTestEnvironment.prototype._add_message_port = function(port) { + this.message_ports.push(port); + for (var i = 0; i < this.message_list.length; ++i) + { + port.postMessage(this.message_list[i]); + } + }; + + WorkerTestEnvironment.prototype.next_default_test_name = function() { + var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; + this.name_counter++; + return "Untitled" + suffix; + }; + + WorkerTestEnvironment.prototype.on_new_harness_properties = function() {}; + + WorkerTestEnvironment.prototype.on_tests_ready = function() { + var this_obj = this; + add_start_callback( + function(properties) { + this_obj._dispatch({ + type: "start", + properties: properties, + }); + }); + add_test_state_callback( + function(test) { + this_obj._dispatch({ + type: "test_state", + test: test.structured_clone() + }); + }); + add_result_callback( + function(test) { + this_obj._dispatch({ + type: "result", + test: test.structured_clone() + }); + }); + add_completion_callback( + function(tests, harness_status) { + this_obj._dispatch({ + type: "complete", + tests: map(tests, + function(test) { + return test.structured_clone(); + }), + status: harness_status.structured_clone() + }); + }); + }; + + WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {}; + + WorkerTestEnvironment.prototype.test_timeout = function() { + // Tests running in a worker don't have a default timeout. I.e. all + // worker tests behave as if settings.explicit_timeout is true. + return null; + }; + + WorkerTestEnvironment.prototype.global_scope = function() { + return self; + }; + + /* + * Dedicated web workers. + * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope + * + * This class is used as the test_environment when testharness is running + * inside a dedicated worker. + */ + function DedicatedWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + // self is an instance of DedicatedWorkerGlobalScope which exposes + // a postMessage() method for communicating via the message channel + // established when the worker is created. + this._add_message_port(self); + } + DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() { + WorkerTestEnvironment.prototype.on_tests_ready.call(this); + // In the absence of an onload notification, we a require dedicated + // workers to explicitly signal when the tests are done. + tests.wait_for_finish = true; + }; + + /* + * Shared web workers. + * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope + * + * This class is used as the test_environment when testharness is running + * inside a shared web worker. + */ + function SharedWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + var this_obj = this; + // Shared workers receive message ports via the 'onconnect' event for + // each connection. + self.addEventListener("connect", + function(message_event) { + this_obj._add_message_port(message_event.source); + }); + } + SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + SharedWorkerTestEnvironment.prototype.on_tests_ready = function() { + WorkerTestEnvironment.prototype.on_tests_ready.call(this); + // In the absence of an onload notification, we a require shared + // workers to explicitly signal when the tests are done. + tests.wait_for_finish = true; + }; + + /* + * Service workers. + * http://www.w3.org/TR/service-workers/ + * + * This class is used as the test_environment when testharness is running + * inside a service worker. + */ + function ServiceWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + this.all_loaded = false; + this.on_loaded_callback = null; + var this_obj = this; + self.addEventListener("message", + function(event) { + if (event.data.type && event.data.type === "connect") { + if (event.ports && event.ports[0]) { + // If a MessageChannel was passed, then use it to + // send results back to the main window. This + // allows the tests to work even if the browser + // does not fully support MessageEvent.source in + // ServiceWorkers yet. + this_obj._add_message_port(event.ports[0]); + event.ports[0].start(); + } else { + // If there is no MessageChannel, then attempt to + // use the MessageEvent.source to send results + // back to the main window. + this_obj._add_message_port(event.source); + } + } + }); + + // The oninstall event is received after the service worker script and + // all imported scripts have been fetched and executed. It's the + // equivalent of an onload event for a document. All tests should have + // been added by the time this event is received, thus it's not + // necessary to wait until the onactivate event. + on_event(self, "install", + function(event) { + this_obj.all_loaded = true; + if (this_obj.on_loaded_callback) { + this_obj.on_loaded_callback(); + } + }); + } + ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + if (this.all_loaded) { + callback(); + } else { + this.on_loaded_callback = callback; + } + }; + + function create_test_environment() { + if ('document' in self) { + return new WindowTestEnvironment(); + } + if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + return new DedicatedWorkerTestEnvironment(); + } + if ('SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + return new SharedWorkerTestEnvironment(); + } + if ('ServiceWorkerGlobalScope' in self && + self instanceof ServiceWorkerGlobalScope) { + return new ServiceWorkerTestEnvironment(); + } + throw new Error("Unsupported test environment"); + } + + var test_environment = create_test_environment(); + + function is_shared_worker(worker) { + return 'SharedWorker' in self && worker instanceof SharedWorker; + } + + function is_service_worker(worker) { + return 'ServiceWorker' in self && worker instanceof ServiceWorker; + } + + /* + * API functions + */ + + function test(func, name, properties) + { + var test_name = name ? name : test_environment.next_default_test_name(); + properties = properties ? properties : {}; + var test_obj = new Test(test_name, properties); + test_obj.step(func, test_obj, test_obj); + if (test_obj.phase === test_obj.phases.STARTED) { + test_obj.done(); + } + } + + function async_test(func, name, properties) + { + if (typeof func !== "function") { + properties = name; + name = func; + func = null; + } + var test_name = name ? name : test_environment.next_default_test_name(); + properties = properties ? properties : {}; + var test_obj = new Test(test_name, properties); + if (func) { + test_obj.step(func, test_obj, test_obj); + } + return test_obj; + } + + function promise_test(func, name, properties) { + var test = async_test(name, properties); + // If there is no promise tests queue make one. + test.step(function() { + if (!tests.promise_tests) { + tests.promise_tests = Promise.resolve(); + } + }); + tests.promise_tests = tests.promise_tests.then(function() { + return Promise.resolve(test.step(func, test, test)) + .then( + function() { + test.done(); + }) + .catch(test.step_func( + function(value) { + if (value instanceof AssertionError) { + throw value; + } + assert(false, "promise_test", null, + "Unhandled rejection with value: ${value}", {value:value}); + })); + }); + } + + function promise_rejects(test, expected, promise) { + return promise.then(test.unreached_func("Should have rejected.")).catch(function(e) { + assert_throws(expected, function() { throw e }); + }); + } + + /** + * This constructor helper allows DOM events to be handled using Promises, + * which can make it a lot easier to test a very specific series of events, + * including ensuring that unexpected events are not fired at any point. + */ + function EventWatcher(test, watchedNode, eventTypes) + { + if (typeof eventTypes == 'string') { + eventTypes = [eventTypes]; + } + + var waitingFor = null; + + var eventHandler = test.step_func(function(evt) { + assert_true(!!waitingFor, + 'Not expecting event, but got ' + evt.type + ' event'); + assert_equals(evt.type, waitingFor.types[0], + 'Expected ' + waitingFor.types[0] + ' event, but got ' + + evt.type + ' event instead'); + if (waitingFor.types.length > 1) { + // Pop first event from array + waitingFor.types.shift(); + return; + } + // We need to null out waitingFor before calling the resolve function + // since the Promise's resolve handlers may call wait_for() which will + // need to set waitingFor. + var resolveFunc = waitingFor.resolve; + waitingFor = null; + resolveFunc(evt); + }); + + for (var i = 0; i < eventTypes.length; i++) { + watchedNode.addEventListener(eventTypes[i], eventHandler); + } + + /** + * Returns a Promise that will resolve after the specified event or + * series of events has occured. + */ + this.wait_for = function(types) { + if (waitingFor) { + return Promise.reject('Already waiting for an event or events'); + } + if (typeof types == 'string') { + types = [types]; + } + return new Promise(function(resolve, reject) { + waitingFor = { + types: types, + resolve: resolve, + reject: reject + }; + }); + }; + + function stop_watching() { + for (var i = 0; i < eventTypes.length; i++) { + watchedNode.removeEventListener(eventTypes[i], eventHandler); + } + }; + + test.add_cleanup(stop_watching); + + return this; + } + expose(EventWatcher, 'EventWatcher'); + + function setup(func_or_properties, maybe_properties) + { + var func = null; + var properties = {}; + if (arguments.length === 2) { + func = func_or_properties; + properties = maybe_properties; + } else if (func_or_properties instanceof Function) { + func = func_or_properties; + } else { + properties = func_or_properties; + } + tests.setup(func, properties); + test_environment.on_new_harness_properties(properties); + } + + function done() { + if (tests.tests.length === 0) { + tests.set_file_is_test(); + } + if (tests.file_is_test) { + tests.tests[0].done(); + } + tests.end_wait(); + } + + function generate_tests(func, args, properties) { + forEach(args, function(x, i) + { + var name = x[0]; + test(function() + { + func.apply(this, x.slice(1)); + }, + name, + Array.isArray(properties) ? properties[i] : properties); + }); + } + + function on_event(object, event, callback) + { + object.addEventListener(event, callback); + } + + function step_timeout(f, t) { + var outer_this = this; + var args = Array.prototype.slice.call(arguments, 2); + return setTimeout(function() { + f.apply(outer_this, args); + }, t * tests.timeout_multiplier); + } + + expose(test, 'test'); + expose(async_test, 'async_test'); + expose(promise_test, 'promise_test'); + expose(promise_rejects, 'promise_rejects'); + expose(generate_tests, 'generate_tests'); + expose(setup, 'setup'); + expose(done, 'done'); + expose(on_event, 'on_event'); + expose(step_timeout, 'step_timeout'); + + /* + * Return a string truncated to the given length, with ... added at the end + * if it was longer. + */ + function truncate(s, len) + { + if (s.length > len) { + return s.substring(0, len - 3) + "..."; + } + return s; + } + + /* + * Return true if object is probably a Node object. + */ + function is_node(object) + { + // I use duck-typing instead of instanceof, because + // instanceof doesn't work if the node is from another window (like an + // iframe's contentWindow): + // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 + if ("nodeType" in object && + "nodeName" in object && + "nodeValue" in object && + "childNodes" in object) { + try { + object.nodeType; + } catch (e) { + // The object is probably Node.prototype or another prototype + // object that inherits from it, and not a Node instance. + return false; + } + return true; + } + return false; + } + + /* + * Convert a value to a nice, human-readable string + */ + function format_value(val, seen) + { + if (!seen) { + seen = []; + } + if (typeof val === "object" && val !== null) { + if (seen.includes(val)) { + return "[...]"; + } + seen.push(val); + } + if (Array.isArray(val)) { + return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]"; + } + + switch (typeof val) { + case "string": + val = val.replace("\\", "\\\\"); + for (var i = 0; i < 32; i++) { + var replace = "\\"; + switch (i) { + case 0: replace += "0"; break; + case 1: replace += "x01"; break; + case 2: replace += "x02"; break; + case 3: replace += "x03"; break; + case 4: replace += "x04"; break; + case 5: replace += "x05"; break; + case 6: replace += "x06"; break; + case 7: replace += "x07"; break; + case 8: replace += "b"; break; + case 9: replace += "t"; break; + case 10: replace += "n"; break; + case 11: replace += "v"; break; + case 12: replace += "f"; break; + case 13: replace += "r"; break; + case 14: replace += "x0e"; break; + case 15: replace += "x0f"; break; + case 16: replace += "x10"; break; + case 17: replace += "x11"; break; + case 18: replace += "x12"; break; + case 19: replace += "x13"; break; + case 20: replace += "x14"; break; + case 21: replace += "x15"; break; + case 22: replace += "x16"; break; + case 23: replace += "x17"; break; + case 24: replace += "x18"; break; + case 25: replace += "x19"; break; + case 26: replace += "x1a"; break; + case 27: replace += "x1b"; break; + case 28: replace += "x1c"; break; + case 29: replace += "x1d"; break; + case 30: replace += "x1e"; break; + case 31: replace += "x1f"; break; + } + val = val.replace(RegExp(String.fromCharCode(i), "g"), replace); + } + return '"' + val.replace(/"/g, '\\"') + '"'; + case "boolean": + case "undefined": + return String(val); + case "number": + // In JavaScript, -0 === 0 and String(-0) == "0", so we have to + // special-case. + if (val === -0 && 1/val === -Infinity) { + return "-0"; + } + return String(val); + case "object": + if (val === null) { + return "null"; + } + + // Special-case Node objects, since those come up a lot in my tests. I + // ignore namespaces. + if (is_node(val)) { + switch (val.nodeType) { + case Node.ELEMENT_NODE: + var ret = "<" + val.localName; + for (var i = 0; i < val.attributes.length; i++) { + ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; + } + ret += ">" + val.innerHTML + "</" + val.localName + ">"; + return "Element node " + truncate(ret, 60); + case Node.TEXT_NODE: + return 'Text node "' + truncate(val.data, 60) + '"'; + case Node.PROCESSING_INSTRUCTION_NODE: + return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); + case Node.COMMENT_NODE: + return "Comment node <!--" + truncate(val.data, 60) + "-->"; + case Node.DOCUMENT_NODE: + return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + case Node.DOCUMENT_TYPE_NODE: + return "DocumentType node"; + case Node.DOCUMENT_FRAGMENT_NODE: + return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + default: + return "Node object of unknown type"; + } + } + + /* falls through */ + default: + return typeof val + ' "' + truncate(String(val), 60) + '"'; + } + } + expose(format_value, "format_value"); + + /* + * Assertions + */ + + function assert_true(actual, description) + { + assert(actual === true, "assert_true", description, + "expected true got ${actual}", {actual:actual}); + } + expose(assert_true, "assert_true"); + + function assert_false(actual, description) + { + assert(actual === false, "assert_false", description, + "expected false got ${actual}", {actual:actual}); + } + expose(assert_false, "assert_false"); + + function same_value(x, y) { + if (y !== y) { + //NaN case + return x !== x; + } + if (x === 0 && y === 0) { + //Distinguish +0 and -0 + return 1/x === 1/y; + } + return x === y; + } + + function assert_equals(actual, expected, description) + { + /* + * Test if two primitives are equal or two objects + * are the same object + */ + if (typeof actual != typeof expected) { + assert(false, "assert_equals", description, + "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", + {expected:expected, actual:actual}); + return; + } + assert(same_value(actual, expected), "assert_equals", description, + "expected ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_equals, "assert_equals"); + + function assert_not_equals(actual, expected, description) + { + /* + * Test if two primitives are unequal or two objects + * are different objects + */ + assert(!same_value(actual, expected), "assert_not_equals", description, + "got disallowed value ${actual}", + {actual:actual}); + } + expose(assert_not_equals, "assert_not_equals"); + + function assert_in_array(actual, expected, description) + { + assert(expected.includes(actual), "assert_in_array", description, + "value ${actual} not in array ${expected}", + {actual:actual, expected:expected}); + } + expose(assert_in_array, "assert_in_array"); + + function assert_object_equals(actual, expected, description) + { + //This needs to be improved a great deal + function check_equal(actual, expected, stack) + { + stack.push(actual); + + var p; + for (p in actual) { + assert(expected.hasOwnProperty(p), "assert_object_equals", description, + "unexpected property ${p}", {p:p}); + + if (typeof actual[p] === "object" && actual[p] !== null) { + if (!stack.includes(actual[p])) { + check_equal(actual[p], expected[p], stack); + } + } else { + assert(same_value(actual[p], expected[p]), "assert_object_equals", description, + "property ${p} expected ${expected} got ${actual}", + {p:p, expected:expected, actual:actual}); + } + } + for (p in expected) { + assert(actual.hasOwnProperty(p), + "assert_object_equals", description, + "expected property ${p} missing", {p:p}); + } + stack.pop(); + } + check_equal(actual, expected, []); + } + expose(assert_object_equals, "assert_object_equals"); + + function assert_array_equals(actual, expected, description) + { + assert(actual.length === expected.length, + "assert_array_equals", description, + "lengths differ, expected ${expected} got ${actual}", + {expected:expected.length, actual:actual.length}); + + for (var i = 0; i < actual.length; i++) { + assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), + "assert_array_equals", description, + "property ${i}, property expected to be ${expected} but was ${actual}", + {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", + actual:actual.hasOwnProperty(i) ? "present" : "missing"}); + assert(same_value(expected[i], actual[i]), + "assert_array_equals", description, + "property ${i}, expected ${expected} but got ${actual}", + {i:i, expected:expected[i], actual:actual[i]}); + } + } + expose(assert_array_equals, "assert_array_equals"); + + function assert_approx_equals(actual, expected, epsilon, description) + { + /* + * Test if two primitive numbers are equal withing +/- epsilon + */ + assert(typeof actual === "number", + "assert_approx_equals", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(Math.abs(actual - expected) <= epsilon, + "assert_approx_equals", description, + "expected ${expected} +/- ${epsilon} but got ${actual}", + {expected:expected, actual:actual, epsilon:epsilon}); + } + expose(assert_approx_equals, "assert_approx_equals"); + + function assert_less_than(actual, expected, description) + { + /* + * Test if a primitive number is less than another + */ + assert(typeof actual === "number", + "assert_less_than", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual < expected, + "assert_less_than", description, + "expected a number less than ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_less_than, "assert_less_than"); + + function assert_greater_than(actual, expected, description) + { + /* + * Test if a primitive number is greater than another + */ + assert(typeof actual === "number", + "assert_greater_than", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual > expected, + "assert_greater_than", description, + "expected a number greater than ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_greater_than, "assert_greater_than"); + + function assert_between_exclusive(actual, lower, upper, description) + { + /* + * Test if a primitive number is between two others + */ + assert(typeof actual === "number", + "assert_between_exclusive", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual > lower && actual < upper, + "assert_between_exclusive", description, + "expected a number greater than ${lower} " + + "and less than ${upper} but got ${actual}", + {lower:lower, upper:upper, actual:actual}); + } + expose(assert_between_exclusive, "assert_between_exclusive"); + + function assert_less_than_equal(actual, expected, description) + { + /* + * Test if a primitive number is less than or equal to another + */ + assert(typeof actual === "number", + "assert_less_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual <= expected, + "assert_less_than_equal", description, + "expected a number less than or equal to ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_less_than_equal, "assert_less_than_equal"); + + function assert_greater_than_equal(actual, expected, description) + { + /* + * Test if a primitive number is greater than or equal to another + */ + assert(typeof actual === "number", + "assert_greater_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual >= expected, + "assert_greater_than_equal", description, + "expected a number greater than or equal to ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_greater_than_equal, "assert_greater_than_equal"); + + function assert_between_inclusive(actual, lower, upper, description) + { + /* + * Test if a primitive number is between to two others or equal to either of them + */ + assert(typeof actual === "number", + "assert_between_inclusive", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual >= lower && actual <= upper, + "assert_between_inclusive", description, + "expected a number greater than or equal to ${lower} " + + "and less than or equal to ${upper} but got ${actual}", + {lower:lower, upper:upper, actual:actual}); + } + expose(assert_between_inclusive, "assert_between_inclusive"); + + function assert_regexp_match(actual, expected, description) { + /* + * Test if a string (actual) matches a regexp (expected) + */ + assert(expected.test(actual), + "assert_regexp_match", description, + "expected ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_regexp_match, "assert_regexp_match"); + + function assert_class_string(object, class_string, description) { + assert_equals({}.toString.call(object), "[object " + class_string + "]", + description); + } + expose(assert_class_string, "assert_class_string"); + + + function _assert_own_property(name) { + return function(object, property_name, description) + { + assert(object.hasOwnProperty(property_name), + name, description, + "expected property ${p} missing", {p:property_name}); + }; + } + expose(_assert_own_property("assert_exists"), "assert_exists"); + expose(_assert_own_property("assert_own_property"), "assert_own_property"); + + function assert_not_exists(object, property_name, description) + { + assert(!object.hasOwnProperty(property_name), + "assert_not_exists", description, + "unexpected property ${p} found", {p:property_name}); + } + expose(assert_not_exists, "assert_not_exists"); + + function _assert_inherits(name) { + return function (object, property_name, description) + { + assert(typeof object === "object", + name, description, + "provided value is not an object"); + + assert("hasOwnProperty" in object, + name, description, + "provided value is an object but has no hasOwnProperty method"); + + assert(!object.hasOwnProperty(property_name), + name, description, + "property ${p} found on object expected in prototype chain", + {p:property_name}); + + assert(property_name in object, + name, description, + "property ${p} not found in prototype chain", + {p:property_name}); + }; + } + expose(_assert_inherits("assert_inherits"), "assert_inherits"); + expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); + + function assert_readonly(object, property_name, description) + { + var initial_value = object[property_name]; + try { + //Note that this can have side effects in the case where + //the property has PutForwards + object[property_name] = initial_value + "a"; //XXX use some other value here? + assert(same_value(object[property_name], initial_value), + "assert_readonly", description, + "changing property ${p} succeeded", + {p:property_name}); + } finally { + object[property_name] = initial_value; + } + } + expose(assert_readonly, "assert_readonly"); + + function assert_throws(code, func, description) + { + try { + func.call(this); + assert(false, "assert_throws", description, + "${func} did not throw", {func:func}); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + if (code === null) { + return; + } + if (typeof code === "object") { + assert(typeof e == "object" && "name" in e && e.name == code.name, + "assert_throws", description, + "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", + {func:func, actual:e, actual_name:e.name, + expected:code, + expected_name:code.name}); + return; + } + + var code_name_map = { + INDEX_SIZE_ERR: 'IndexSizeError', + HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', + WRONG_DOCUMENT_ERR: 'WrongDocumentError', + INVALID_CHARACTER_ERR: 'InvalidCharacterError', + NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', + NOT_FOUND_ERR: 'NotFoundError', + NOT_SUPPORTED_ERR: 'NotSupportedError', + INVALID_STATE_ERR: 'InvalidStateError', + SYNTAX_ERR: 'SyntaxError', + INVALID_MODIFICATION_ERR: 'InvalidModificationError', + NAMESPACE_ERR: 'NamespaceError', + INVALID_ACCESS_ERR: 'InvalidAccessError', + TYPE_MISMATCH_ERR: 'TypeMismatchError', + SECURITY_ERR: 'SecurityError', + NETWORK_ERR: 'NetworkError', + ABORT_ERR: 'AbortError', + URL_MISMATCH_ERR: 'URLMismatchError', + QUOTA_EXCEEDED_ERR: 'QuotaExceededError', + TIMEOUT_ERR: 'TimeoutError', + INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', + DATA_CLONE_ERR: 'DataCloneError' + }; + + var name = code in code_name_map ? code_name_map[code] : code; + + var name_code_map = { + IndexSizeError: 1, + HierarchyRequestError: 3, + WrongDocumentError: 4, + InvalidCharacterError: 5, + NoModificationAllowedError: 7, + NotFoundError: 8, + NotSupportedError: 9, + InvalidStateError: 11, + SyntaxError: 12, + InvalidModificationError: 13, + NamespaceError: 14, + InvalidAccessError: 15, + TypeMismatchError: 17, + SecurityError: 18, + NetworkError: 19, + AbortError: 20, + URLMismatchError: 21, + QuotaExceededError: 22, + TimeoutError: 23, + InvalidNodeTypeError: 24, + DataCloneError: 25, + + EncodingError: 0, + NotReadableError: 0, + UnknownError: 0, + ConstraintError: 0, + DataError: 0, + TransactionInactiveError: 0, + ReadOnlyError: 0, + VersionError: 0, + OperationError: 0, + }; + + if (!(name in name_code_map)) { + throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()'); + } + + var required_props = { code: name_code_map[name] }; + + if (required_props.code === 0 || + (typeof e == "object" && + "name" in e && + e.name !== e.name.toUpperCase() && + e.name !== "DOMException")) { + // New style exception: also test the name property. + required_props.name = name; + } + + //We'd like to test that e instanceof the appropriate interface, + //but we can't, because we don't know what window it was created + //in. It might be an instanceof the appropriate interface on some + //unknown other window. TODO: Work around this somehow? + + assert(typeof e == "object", + "assert_throws", description, + "${func} threw ${e} with type ${type}, not an object", + {func:func, e:e, type:typeof e}); + + for (var prop in required_props) { + assert(typeof e == "object" && prop in e && e[prop] == required_props[prop], + "assert_throws", description, + "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}", + {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); + } + } + } + expose(assert_throws, "assert_throws"); + + function assert_unreached(description) { + assert(false, "assert_unreached", description, + "Reached unreachable code"); + } + expose(assert_unreached, "assert_unreached"); + + function assert_any(assert_func, actual, expected_array) + { + var args = [].slice.call(arguments, 3); + var errors = []; + var passed = false; + forEach(expected_array, + function(expected) + { + try { + assert_func.apply(this, [actual, expected].concat(args)); + passed = true; + } catch (e) { + errors.push(e.message); + } + }); + if (!passed) { + throw new AssertionError(errors.join("\n\n")); + } + } + expose(assert_any, "assert_any"); + + function Test(name, properties) + { + if (tests.file_is_test && tests.tests.length) { + throw new Error("Tried to create a test with file_is_test"); + } + this.name = name; + + this.phase = this.phases.INITIAL; + + this.status = this.NOTRUN; + this.timeout_id = null; + this.index = null; + + this.properties = properties; + var timeout = properties.timeout ? properties.timeout : settings.test_timeout; + if (timeout !== null) { + this.timeout_length = timeout * tests.timeout_multiplier; + } else { + this.timeout_length = null; + } + + this.message = null; + this.stack = null; + + this.steps = []; + + this.cleanup_callbacks = []; + + tests.push(this); + } + + Test.statuses = { + PASS:0, + FAIL:1, + TIMEOUT:2, + NOTRUN:3 + }; + + Test.prototype = merge({}, Test.statuses); + + Test.prototype.phases = { + INITIAL:0, + STARTED:1, + HAS_RESULT:2, + COMPLETE:3 + }; + + Test.prototype.structured_clone = function() + { + if (!this._structured_clone) { + var msg = this.message; + msg = msg ? String(msg) : msg; + this._structured_clone = merge({ + name:String(this.name), + properties:merge({}, this.properties), + }, Test.statuses); + } + this._structured_clone.status = this.status; + this._structured_clone.message = this.message; + this._structured_clone.stack = this.stack; + this._structured_clone.index = this.index; + return this._structured_clone; + }; + + Test.prototype.step = function(func, this_obj) + { + if (this.phase > this.phases.STARTED) { + return; + } + this.phase = this.phases.STARTED; + //If we don't get a result before the harness times out that will be a test timout + this.set_status(this.TIMEOUT, "Test timed out"); + + tests.started = true; + tests.notify_test_state(this); + + if (this.timeout_id === null) { + this.set_timeout(); + } + + this.steps.push(func); + + if (arguments.length === 1) { + this_obj = this; + } + + try { + return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); + } catch (e) { + if (this.phase >= this.phases.HAS_RESULT) { + return; + } + var message = String((typeof e === "object" && e !== null) ? e.message : e); + var stack = e.stack ? e.stack : null; + + this.set_status(this.FAIL, message, stack); + this.phase = this.phases.HAS_RESULT; + this.done(); + } + }; + + Test.prototype.step_func = function(func, this_obj) + { + var test_this = this; + + if (arguments.length === 1) { + this_obj = test_this; + } + + return function() + { + return test_this.step.apply(test_this, [func, this_obj].concat( + Array.prototype.slice.call(arguments))); + }; + }; + + Test.prototype.step_func_done = function(func, this_obj) + { + var test_this = this; + + if (arguments.length === 1) { + this_obj = test_this; + } + + return function() + { + if (func) { + test_this.step.apply(test_this, [func, this_obj].concat( + Array.prototype.slice.call(arguments))); + } + test_this.done(); + }; + }; + + Test.prototype.unreached_func = function(description) + { + return this.step_func(function() { + assert_unreached(description); + }); + }; + + Test.prototype.step_timeout = function(f, timeout) { + var test_this = this; + var args = Array.prototype.slice.call(arguments, 2); + return setTimeout(this.step_func(function() { + return f.apply(test_this, args); + }, timeout * tests.timeout_multiplier)); + } + + Test.prototype.add_cleanup = function(callback) { + this.cleanup_callbacks.push(callback); + }; + + Test.prototype.force_timeout = function() { + this.set_status(this.TIMEOUT); + this.phase = this.phases.HAS_RESULT; + }; + + Test.prototype.set_timeout = function() + { + if (this.timeout_length !== null) { + var this_obj = this; + this.timeout_id = setTimeout(function() + { + this_obj.timeout(); + }, this.timeout_length); + } + }; + + Test.prototype.set_status = function(status, message, stack) + { + this.status = status; + this.message = message; + this.stack = stack ? stack : null; + }; + + Test.prototype.timeout = function() + { + this.timeout_id = null; + this.set_status(this.TIMEOUT, "Test timed out"); + this.phase = this.phases.HAS_RESULT; + this.done(); + }; + + Test.prototype.done = function() + { + if (this.phase == this.phases.COMPLETE) { + return; + } + + if (this.phase <= this.phases.STARTED) { + this.set_status(this.PASS, null); + } + + this.phase = this.phases.COMPLETE; + + clearTimeout(this.timeout_id); + tests.result(this); + this.cleanup(); + }; + + Test.prototype.cleanup = function() { + forEach(this.cleanup_callbacks, + function(cleanup_callback) { + cleanup_callback(); + }); + }; + + /* + * A RemoteTest object mirrors a Test object on a remote worker. The + * associated RemoteWorker updates the RemoteTest object in response to + * received events. In turn, the RemoteTest object replicates these events + * on the local document. This allows listeners (test result reporting + * etc..) to transparently handle local and remote events. + */ + function RemoteTest(clone) { + var this_obj = this; + Object.keys(clone).forEach( + function(key) { + this_obj[key] = clone[key]; + }); + this.index = null; + this.phase = this.phases.INITIAL; + this.update_state_from(clone); + tests.push(this); + } + + RemoteTest.prototype.structured_clone = function() { + var clone = {}; + Object.keys(this).forEach( + key => { + if (typeof(this[key]) === "object") { + clone[key] = merge({}, this[key]); + } else { + clone[key] = this[key]; + } + }); + clone.phases = merge({}, this.phases); + return clone; + }; + + RemoteTest.prototype.cleanup = function() {}; + RemoteTest.prototype.phases = Test.prototype.phases; + RemoteTest.prototype.update_state_from = function(clone) { + this.status = clone.status; + this.message = clone.message; + this.stack = clone.stack; + if (this.phase === this.phases.INITIAL) { + this.phase = this.phases.STARTED; + } + }; + RemoteTest.prototype.done = function() { + this.phase = this.phases.COMPLETE; + } + + /* + * A RemoteWorker listens for test events from a worker. These events are + * then used to construct and maintain RemoteTest objects that mirror the + * tests running on the remote worker. + */ + function RemoteWorker(worker) { + this.running = true; + this.tests = new Array(); + + var this_obj = this; + worker.onerror = function(error) { this_obj.worker_error(error); }; + + var message_port; + + if (is_service_worker(worker)) { + if (window.MessageChannel) { + // The ServiceWorker's implicit MessagePort is currently not + // reliably accessible from the ServiceWorkerGlobalScope due to + // Blink setting MessageEvent.source to null for messages sent + // via ServiceWorker.postMessage(). Until that's resolved, + // create an explicit MessageChannel and pass one end to the + // worker. + var message_channel = new MessageChannel(); + message_port = message_channel.port1; + message_port.start(); + worker.postMessage({type: "connect"}, [message_channel.port2]); + } else { + // If MessageChannel is not available, then try the + // ServiceWorker.postMessage() approach using MessageEvent.source + // on the other end. + message_port = navigator.serviceWorker; + worker.postMessage({type: "connect"}); + } + } else if (is_shared_worker(worker)) { + message_port = worker.port; + } else { + message_port = worker; + } + + // Keeping a reference to the worker until worker_done() is seen + // prevents the Worker object and its MessageChannel from going away + // before all the messages are dispatched. + this.worker = worker; + + message_port.onmessage = + function(message) { + if (this_obj.running && (message.data.type in this_obj.message_handlers)) { + this_obj.message_handlers[message.data.type].call(this_obj, message.data); + } + }; + } + + RemoteWorker.prototype.worker_error = function(error) { + var message = error.message || String(error); + var filename = (error.filename ? " " + error.filename: ""); + // FIXME: Display worker error states separately from main document + // error state. + this.worker_done({ + status: { + status: tests.status.ERROR, + message: "Error in worker" + filename + ": " + message, + stack: error.stack + } + }); + error.preventDefault(); + }; + + RemoteWorker.prototype.test_state = function(data) { + var remote_test = this.tests[data.test.index]; + if (!remote_test) { + remote_test = new RemoteTest(data.test); + this.tests[data.test.index] = remote_test; + } + remote_test.update_state_from(data.test); + tests.notify_test_state(remote_test); + }; + + RemoteWorker.prototype.test_done = function(data) { + var remote_test = this.tests[data.test.index]; + remote_test.update_state_from(data.test); + remote_test.done(); + tests.result(remote_test); + }; + + RemoteWorker.prototype.worker_done = function(data) { + if (tests.status.status === null && + data.status.status !== data.status.OK) { + tests.status.status = data.status.status; + tests.status.message = data.status.message; + tests.status.stack = data.status.stack; + } + this.running = false; + this.worker = null; + if (tests.all_done()) { + tests.complete(); + } + }; + + RemoteWorker.prototype.message_handlers = { + test_state: RemoteWorker.prototype.test_state, + result: RemoteWorker.prototype.test_done, + complete: RemoteWorker.prototype.worker_done + }; + + /* + * Harness + */ + + function TestsStatus() + { + this.status = null; + this.message = null; + this.stack = null; + } + + TestsStatus.statuses = { + OK:0, + ERROR:1, + TIMEOUT:2 + }; + + TestsStatus.prototype = merge({}, TestsStatus.statuses); + + TestsStatus.prototype.structured_clone = function() + { + if (!this._structured_clone) { + var msg = this.message; + msg = msg ? String(msg) : msg; + this._structured_clone = merge({ + status:this.status, + message:msg, + stack:this.stack + }, TestsStatus.statuses); + } + return this._structured_clone; + }; + + function Tests() + { + this.tests = []; + this.num_pending = 0; + + this.phases = { + INITIAL:0, + SETUP:1, + HAVE_TESTS:2, + HAVE_RESULTS:3, + COMPLETE:4 + }; + this.phase = this.phases.INITIAL; + + this.properties = {}; + + this.wait_for_finish = false; + this.processing_callbacks = false; + + this.allow_uncaught_exception = false; + + this.file_is_test = false; + + this.timeout_multiplier = 1; + this.timeout_length = test_environment.test_timeout(); + this.timeout_id = null; + + this.start_callbacks = []; + this.test_state_callbacks = []; + this.test_done_callbacks = []; + this.all_done_callbacks = []; + + this.pending_workers = []; + + this.status = new TestsStatus(); + + var this_obj = this; + + test_environment.add_on_loaded_callback(function() { + if (this_obj.all_done()) { + this_obj.complete(); + } + }); + + this.set_timeout(); + } + + Tests.prototype.setup = function(func, properties) + { + if (this.phase >= this.phases.HAVE_RESULTS) { + return; + } + + if (this.phase < this.phases.SETUP) { + this.phase = this.phases.SETUP; + } + + this.properties = properties; + + for (var p in properties) { + if (properties.hasOwnProperty(p)) { + var value = properties[p]; + if (p == "allow_uncaught_exception") { + this.allow_uncaught_exception = value; + } else if (p == "explicit_done" && value) { + this.wait_for_finish = true; + } else if (p == "explicit_timeout" && value) { + this.timeout_length = null; + if (this.timeout_id) + { + clearTimeout(this.timeout_id); + } + } else if (p == "timeout_multiplier") { + this.timeout_multiplier = value; + } + } + } + + if (func) { + try { + func(); + } catch (e) { + this.status.status = this.status.ERROR; + this.status.message = String(e); + this.status.stack = e.stack ? e.stack : null; + } + } + this.set_timeout(); + }; + + Tests.prototype.set_file_is_test = function() { + if (this.tests.length > 0) { + throw new Error("Tried to set file as test after creating a test"); + } + this.wait_for_finish = true; + this.file_is_test = true; + // Create the test, which will add it to the list of tests + async_test(); + }; + + Tests.prototype.set_timeout = function() { + var this_obj = this; + clearTimeout(this.timeout_id); + if (this.timeout_length !== null) { + this.timeout_id = setTimeout(function() { + this_obj.timeout(); + }, this.timeout_length); + } + }; + + Tests.prototype.timeout = function() { + if (this.status.status === null) { + this.status.status = this.status.TIMEOUT; + } + this.complete(); + }; + + Tests.prototype.end_wait = function() + { + this.wait_for_finish = false; + if (this.all_done()) { + this.complete(); + } + }; + + Tests.prototype.push = function(test) + { + if (this.phase < this.phases.HAVE_TESTS) { + this.start(); + } + this.num_pending++; + test.index = this.tests.push(test); + this.notify_test_state(test); + }; + + Tests.prototype.notify_test_state = function(test) { + var this_obj = this; + forEach(this.test_state_callbacks, + function(callback) { + callback(test, this_obj); + }); + }; + + Tests.prototype.all_done = function() { + return (this.tests.length > 0 && test_environment.all_loaded && + this.num_pending === 0 && !this.wait_for_finish && + !this.processing_callbacks && + !this.pending_workers.some(function(w) { return w.running; })); + }; + + Tests.prototype.start = function() { + this.phase = this.phases.HAVE_TESTS; + this.notify_start(); + }; + + Tests.prototype.notify_start = function() { + var this_obj = this; + forEach (this.start_callbacks, + function(callback) + { + callback(this_obj.properties); + }); + }; + + Tests.prototype.result = function(test) + { + if (this.phase > this.phases.HAVE_RESULTS) { + return; + } + this.phase = this.phases.HAVE_RESULTS; + this.num_pending--; + this.notify_result(test); + }; + + Tests.prototype.notify_result = function(test) { + var this_obj = this; + this.processing_callbacks = true; + forEach(this.test_done_callbacks, + function(callback) + { + callback(test, this_obj); + }); + this.processing_callbacks = false; + if (this_obj.all_done()) { + this_obj.complete(); + } + }; + + Tests.prototype.complete = function() { + if (this.phase === this.phases.COMPLETE) { + return; + } + this.phase = this.phases.COMPLETE; + var this_obj = this; + this.tests.forEach( + function(x) + { + if (x.phase < x.phases.COMPLETE) { + this_obj.notify_result(x); + x.cleanup(); + x.phase = x.phases.COMPLETE; + } + } + ); + this.notify_complete(); + }; + + Tests.prototype.notify_complete = function() { + var this_obj = this; + if (this.status.status === null) { + this.status.status = this.status.OK; + } + + forEach (this.all_done_callbacks, + function(callback) + { + callback(this_obj.tests, this_obj.status); + }); + }; + + Tests.prototype.fetch_tests_from_worker = function(worker) { + if (this.phase >= this.phases.COMPLETE) { + return; + } + + this.pending_workers.push(new RemoteWorker(worker)); + }; + + function fetch_tests_from_worker(port) { + tests.fetch_tests_from_worker(port); + } + expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); + + function timeout() { + if (tests.timeout_length === null) { + tests.timeout(); + } + } + expose(timeout, 'timeout'); + + function add_start_callback(callback) { + tests.start_callbacks.push(callback); + } + + function add_test_state_callback(callback) { + tests.test_state_callbacks.push(callback); + } + + function add_result_callback(callback) { + tests.test_done_callbacks.push(callback); + } + + function add_completion_callback(callback) { + tests.all_done_callbacks.push(callback); + } + + expose(add_start_callback, 'add_start_callback'); + expose(add_test_state_callback, 'add_test_state_callback'); + expose(add_result_callback, 'add_result_callback'); + expose(add_completion_callback, 'add_completion_callback'); + + function remove(array, item) { + var index = array.indexOf(item); + if (index > -1) { + array.splice(index, 1); + } + } + + function remove_start_callback(callback) { + remove(tests.start_callbacks, callback); + } + + function remove_test_state_callback(callback) { + remove(tests.test_state_callbacks, callback); + } + + function remove_result_callback(callback) { + remove(tests.test_done_callbacks, callback); + } + + function remove_completion_callback(callback) { + remove(tests.all_done_callbacks, callback); + } + + /* + * Output listener + */ + + function Output() { + this.output_document = document; + this.output_node = null; + this.enabled = settings.output; + this.phase = this.INITIAL; + } + + Output.prototype.INITIAL = 0; + Output.prototype.STARTED = 1; + Output.prototype.HAVE_RESULTS = 2; + Output.prototype.COMPLETE = 3; + + Output.prototype.setup = function(properties) { + if (this.phase > this.INITIAL) { + return; + } + + //If output is disabled in testharnessreport.js the test shouldn't be + //able to override that + this.enabled = this.enabled && (properties.hasOwnProperty("output") ? + properties.output : settings.output); + }; + + Output.prototype.init = function(properties) { + if (this.phase >= this.STARTED) { + return; + } + if (properties.output_document) { + this.output_document = properties.output_document; + } else { + this.output_document = document; + } + this.phase = this.STARTED; + }; + + Output.prototype.resolve_log = function() { + var output_document; + if (typeof this.output_document === "function") { + output_document = this.output_document.apply(undefined); + } else { + output_document = this.output_document; + } + if (!output_document) { + return; + } + var node = output_document.getElementById("log"); + if (!node) { + if (!document.body || document.readyState == "loading") { + return; + } + node = output_document.createElement("div"); + node.id = "log"; + output_document.body.appendChild(node); + } + this.output_document = output_document; + this.output_node = node; + }; + + Output.prototype.show_status = function() { + if (this.phase < this.STARTED) { + this.init(); + } + if (!this.enabled) { + return; + } + if (this.phase < this.HAVE_RESULTS) { + this.resolve_log(); + this.phase = this.HAVE_RESULTS; + } + var done_count = tests.tests.length - tests.num_pending; + if (this.output_node) { + if (done_count < 100 || + (done_count < 1000 && done_count % 100 === 0) || + done_count % 1000 === 0) { + this.output_node.textContent = "Running, " + + done_count + " complete, " + + tests.num_pending + " remain"; + } + } + }; + + Output.prototype.show_results = function (tests, harness_status) { + if (this.phase >= this.COMPLETE) { + return; + } + if (!this.enabled) { + return; + } + if (!this.output_node) { + this.resolve_log(); + } + this.phase = this.COMPLETE; + + var log = this.output_node; + if (!log) { + return; + } + var output_document = this.output_document; + + while (log.lastChild) { + log.removeChild(log.lastChild); + } + + var harness_url = get_harness_url(); + if (harness_url !== null) { + var stylesheet = output_document.createElementNS(xhtml_ns, "link"); + stylesheet.setAttribute("rel", "stylesheet"); + stylesheet.setAttribute("href", harness_url + "testharness.css"); + var heads = output_document.getElementsByTagName("head"); + if (heads.length) { + heads[0].appendChild(stylesheet); + } + } + + var status_text_harness = {}; + status_text_harness[harness_status.OK] = "OK"; + status_text_harness[harness_status.ERROR] = "Error"; + status_text_harness[harness_status.TIMEOUT] = "Timeout"; + + var status_text = {}; + status_text[Test.prototype.PASS] = "Pass"; + status_text[Test.prototype.FAIL] = "Fail"; + status_text[Test.prototype.TIMEOUT] = "Timeout"; + status_text[Test.prototype.NOTRUN] = "Not Run"; + + var status_number = {}; + forEach(tests, + function(test) { + var status = status_text[test.status]; + if (status_number.hasOwnProperty(status)) { + status_number[status] += 1; + } else { + status_number[status] = 1; + } + }); + + function status_class(status) + { + return status.replace(/\s/g, '').toLowerCase(); + } + + var summary_template = ["section", {"id":"summary"}, + ["h2", {}, "Summary"], + function() + { + + var status = status_text_harness[harness_status.status]; + var rv = [["section", {}, + ["p", {}, + "Harness status: ", + ["span", {"class":status_class(status)}, + status + ], + ] + ]]; + + if (harness_status.status === harness_status.ERROR) { + rv[0].push(["pre", {}, harness_status.message]); + if (harness_status.stack) { + rv[0].push(["pre", {}, harness_status.stack]); + } + } + return rv; + }, + ["p", {}, "Found ${num_tests} tests"], + function() { + var rv = [["div", {}]]; + var i = 0; + while (status_text.hasOwnProperty(i)) { + if (status_number.hasOwnProperty(status_text[i])) { + var status = status_text[i]; + rv[0].push(["div", {"class":status_class(status)}, + ["label", {}, + ["input", {type:"checkbox", checked:"checked"}], + status_number[status] + " " + status]]); + } + i++; + } + return rv; + }, + ]; + + log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); + + forEach(output_document.querySelectorAll("section#summary label"), + function(element) + { + on_event(element, "click", + function(e) + { + if (output_document.getElementById("results") === null) { + e.preventDefault(); + return; + } + var result_class = element.parentNode.getAttribute("class"); + var style_element = output_document.querySelector("style#hide-" + result_class); + var input_element = element.querySelector("input"); + if (!style_element && !input_element.checked) { + style_element = output_document.createElementNS(xhtml_ns, "style"); + style_element.id = "hide-" + result_class; + style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}"; + output_document.body.appendChild(style_element); + } else if (style_element && input_element.checked) { + style_element.remove(); + } + }); + }); + + // This use of innerHTML plus manual escaping is not recommended in + // general, but is necessary here for performance. Using textContent + // on each individual <td> adds tens of seconds of execution time for + // large test suites (tens of thousands of tests). + function escape_html(s) + { + return s.replace(/\&/g, "&") + .replace(/</g, "<") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function has_assertions() + { + for (var i = 0; i < tests.length; i++) { + if (tests[i].properties.hasOwnProperty("assert")) { + return true; + } + } + return false; + } + + function get_assertion(test) + { + if (test.properties.hasOwnProperty("assert")) { + if (Array.isArray(test.properties.assert)) { + return test.properties.assert.join(' '); + } + return test.properties.assert; + } + return ''; + } + + log.appendChild(document.createElementNS(xhtml_ns, "section")); + var assertions = has_assertions(); + var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" + + "<thead><tr><th>Result</th><th>Test Name</th>" + + (assertions ? "<th>Assertion</th>" : "") + + "<th>Message</th></tr></thead>" + + "<tbody>"; + for (var i = 0; i < tests.length; i++) { + html += '<tr class="' + + escape_html(status_class(status_text[tests[i].status])) + + '"><td>' + + escape_html(status_text[tests[i].status]) + + "</td><td>" + + escape_html(tests[i].name) + + "</td><td>" + + (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") + + escape_html(tests[i].message ? tests[i].message : " ") + + (tests[i].stack ? "<pre>" + + escape_html(tests[i].stack) + + "</pre>": "") + + "</td></tr>"; + } + html += "</tbody></table>"; + try { + log.lastChild.innerHTML = html; + } catch (e) { + log.appendChild(document.createElementNS(xhtml_ns, "p")) + .textContent = "Setting innerHTML for the log threw an exception."; + log.appendChild(document.createElementNS(xhtml_ns, "pre")) + .textContent = html; + } + }; + + /* + * Template code + * + * A template is just a javascript structure. An element is represented as: + * + * [tag_name, {attr_name:attr_value}, child1, child2] + * + * the children can either be strings (which act like text nodes), other templates or + * functions (see below) + * + * A text node is represented as + * + * ["{text}", value] + * + * String values have a simple substitution syntax; ${foo} represents a variable foo. + * + * It is possible to embed logic in templates by using a function in a place where a + * node would usually go. The function must either return part of a template or null. + * + * In cases where a set of nodes are required as output rather than a single node + * with children it is possible to just use a list + * [node1, node2, node3] + * + * Usage: + * + * render(template, substitutions) - take a template and an object mapping + * variable names to parameters and return either a DOM node or a list of DOM nodes + * + * substitute(template, substitutions) - take a template and variable mapping object, + * make the variable substitutions and return the substituted template + * + */ + + function is_single_node(template) + { + return typeof template[0] === "string"; + } + + function substitute(template, substitutions) + { + if (typeof template === "function") { + var replacement = template(substitutions); + if (!replacement) { + return null; + } + + return substitute(replacement, substitutions); + } + + if (is_single_node(template)) { + return substitute_single(template, substitutions); + } + + return filter(map(template, function(x) { + return substitute(x, substitutions); + }), function(x) {return x !== null;}); + } + + function substitute_single(template, substitutions) + { + var substitution_re = /\$\{([^ }]*)\}/g; + + function do_substitution(input) { + var components = input.split(substitution_re); + var rv = []; + for (var i = 0; i < components.length; i += 2) { + rv.push(components[i]); + if (components[i + 1]) { + rv.push(String(substitutions[components[i + 1]])); + } + } + return rv; + } + + function substitute_attrs(attrs, rv) + { + rv[1] = {}; + for (var name in template[1]) { + if (attrs.hasOwnProperty(name)) { + var new_name = do_substitution(name).join(""); + var new_value = do_substitution(attrs[name]).join(""); + rv[1][new_name] = new_value; + } + } + } + + function substitute_children(children, rv) + { + for (var i = 0; i < children.length; i++) { + if (children[i] instanceof Object) { + var replacement = substitute(children[i], substitutions); + if (replacement !== null) { + if (is_single_node(replacement)) { + rv.push(replacement); + } else { + extend(rv, replacement); + } + } + } else { + extend(rv, do_substitution(String(children[i]))); + } + } + return rv; + } + + var rv = []; + rv.push(do_substitution(String(template[0])).join("")); + + if (template[0] === "{text}") { + substitute_children(template.slice(1), rv); + } else { + substitute_attrs(template[1], rv); + substitute_children(template.slice(2), rv); + } + + return rv; + } + + function make_dom_single(template, doc) + { + var output_document = doc || document; + var element; + if (template[0] === "{text}") { + element = output_document.createTextNode(""); + for (var i = 1; i < template.length; i++) { + element.data += template[i]; + } + } else { + element = output_document.createElementNS(xhtml_ns, template[0]); + for (var name in template[1]) { + if (template[1].hasOwnProperty(name)) { + element.setAttribute(name, template[1][name]); + } + } + for (var i = 2; i < template.length; i++) { + if (template[i] instanceof Object) { + var sub_element = make_dom(template[i]); + element.appendChild(sub_element); + } else { + var text_node = output_document.createTextNode(template[i]); + element.appendChild(text_node); + } + } + } + + return element; + } + + function make_dom(template, substitutions, output_document) + { + if (is_single_node(template)) { + return make_dom_single(template, output_document); + } + + return map(template, function(x) { + return make_dom_single(x, output_document); + }); + } + + function render(template, substitutions, output_document) + { + return make_dom(substitute(template, substitutions), output_document); + } + + /* + * Utility funcions + */ + function assert(expected_true, function_name, description, error, substitutions) + { + if (tests.tests.length === 0) { + tests.set_file_is_test(); + } + if (expected_true !== true) { + var msg = make_message(function_name, description, + error, substitutions); + throw new AssertionError(msg); + } + } + + function AssertionError(message) + { + this.message = message; + this.stack = this.get_stack(); + } + + AssertionError.prototype = Object.create(Error.prototype); + + AssertionError.prototype.get_stack = function() { + var stack = new Error().stack; + // IE11 does not initialize 'Error.stack' until the object is thrown. + if (!stack) { + try { + throw new Error(); + } catch (e) { + stack = e.stack; + } + } + + var lines = stack.split("\n"); + + // Create a pattern to match stack frames originating within testharness.js. These include the + // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21'). + var re = new RegExp((get_script_url() || "\\btestharness.js") + ":\\d+:\\d+"); + + // Some browsers include a preamble that specifies the type of the error object. Skip this by + // advancing until we find the first stack frame originating from testharness.js. + var i = 0; + while (!re.test(lines[i]) && i < lines.length) { + i++; + } + + // Then skip the top frames originating from testharness.js to begin the stack at the test code. + while (re.test(lines[i]) && i < lines.length) { + i++; + } + + // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified. + if (i >= lines.length) { + return stack; + } + + return lines.slice(i).join("\n"); + } + + function make_message(function_name, description, error, substitutions) + { + for (var p in substitutions) { + if (substitutions.hasOwnProperty(p)) { + substitutions[p] = format_value(substitutions[p]); + } + } + var node_form = substitute(["{text}", "${function_name}: ${description}" + error], + merge({function_name:function_name, + description:(description?description + " ":"")}, + substitutions)); + return node_form.slice(1).join(""); + } + + function filter(array, callable, thisObj) { + var rv = []; + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + var pass = callable.call(thisObj, array[i], i, array); + if (pass) { + rv.push(array[i]); + } + } + } + return rv; + } + + function map(array, callable, thisObj) + { + var rv = []; + rv.length = array.length; + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + rv[i] = callable.call(thisObj, array[i], i, array); + } + } + return rv; + } + + function extend(array, items) + { + Array.prototype.push.apply(array, items); + } + + function forEach(array, callback, thisObj) + { + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + callback.call(thisObj, array[i], i, array); + } + } + } + + function merge(a,b) + { + var rv = {}; + var p; + for (p in a) { + rv[p] = a[p]; + } + for (p in b) { + rv[p] = b[p]; + } + return rv; + } + + function expose(object, name) + { + var components = name.split("."); + var target = test_environment.global_scope(); + for (var i = 0; i < components.length - 1; i++) { + if (!(components[i] in target)) { + target[components[i]] = {}; + } + target = target[components[i]]; + } + target[components[components.length - 1]] = object; + } + + function is_same_origin(w) { + try { + 'random_prop' in w; + return true; + } catch (e) { + return false; + } + } + + /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */ + function get_script_url() + { + if (!('document' in self)) { + return undefined; + } + + var scripts = document.getElementsByTagName("script"); + for (var i = 0; i < scripts.length; i++) { + var src; + if (scripts[i].src) { + src = scripts[i].src; + } else if (scripts[i].href) { + //SVG case + src = scripts[i].href.baseVal; + } + + var matches = src && src.match(/^(.*\/|)testharness\.js$/); + if (matches) { + return src; + } + } + return undefined; + } + + /** Returns the URL path at which the files for testharness.js are assumed to reside (e.g., '/resources/'). + The path is derived from inspecting the 'src' of the <script> tag that included 'testharness.js'. */ + function get_harness_url() + { + var script_url = get_script_url(); + + // Exclude the 'testharness.js' file from the returned path, but '+ 1' to include the trailing slash. + return script_url ? script_url.slice(0, script_url.lastIndexOf('/') + 1) : undefined; + } + + function supports_post_message(w) + { + var supports; + var type; + // Given IE implements postMessage across nested iframes but not across + // windows or tabs, you can't infer cross-origin communication from the presence + // of postMessage on the current window object only. + // + // Touching the postMessage prop on a window can throw if the window is + // not from the same origin AND post message is not supported in that + // browser. So just doing an existence test here won't do, you also need + // to wrap it in a try..cacth block. + try { + type = typeof w.postMessage; + if (type === "function") { + supports = true; + } + + // IE8 supports postMessage, but implements it as a host object which + // returns "object" as its `typeof`. + else if (type === "object") { + supports = true; + } + + // This is the case where postMessage isn't supported AND accessing a + // window property across origins does NOT throw (e.g. old Safari browser). + else { + supports = false; + } + } catch (e) { + // This is the case where postMessage isn't supported AND accessing a + // window property across origins throws (e.g. old Firefox browser). + supports = false; + } + return supports; + } + + /** + * Setup globals + */ + + var tests = new Tests(); + + addEventListener("error", function(e) { + if (tests.file_is_test) { + var test = tests.tests[0]; + if (test.phase >= test.phases.HAS_RESULT) { + return; + } + test.set_status(test.FAIL, e.message, e.stack); + test.phase = test.phases.HAS_RESULT; + test.done(); + done(); + } else if (!tests.allow_uncaught_exception) { + tests.status.status = tests.status.ERROR; + tests.status.message = e.message; + tests.status.stack = e.stack; + } + }); + + test_environment.on_tests_ready(); + +})(); +// vim: set expandtab shiftwidth=4 tabstop=4: |