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