});
}
+function rejectPromise() {
+ return new Promise(function(resolve, reject) {
+ // Make sure the oninstall/onactivate callback returns first.
+ Promise.resolve().then(reject);
+ });
+}
+
+function stripScopeName(url) {
+ return url.split('/').slice(-1)[0];
+}
+
oninstall = function(e) {
- e.waitUntil(fulfillPromise());
-};
\ No newline at end of file
+ switch (stripScopeName(self.location.href)) {
+ case 'install-fulfilled':
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'install-rejected':
+ e.waitUntil(rejectPromise());
+ break;
+ case 'install-multiple-fulfilled':
+ e.waitUntil(fulfillPromise());
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'install-reject-precedence':
+ // Three "extend lifetime promises" are needed to verify that the user
+ // agent waits for all promises to settle even in the event of rejection.
+ // The first promise is fulfilled on demand by the client, the second is
+ // immediately scheduled for rejection, and the third is fulfilled on
+ // demand by the client (but only after the first promise has been
+ // fulfilled).
+ //
+ // User agents which simply expose `Promise.all` semantics in this case
+ // (by entering the "redundant state" following the rejection of the
+ // second promise but prior to the fulfillment of the third) can be
+ // identified from the client context.
+ e.waitUntil(fulfillPromise());
+ e.waitUntil(rejectPromise());
+ e.waitUntil(fulfillPromise());
+ break;
+ }
+};
+
+onactivate = function(e) {
+ switch (stripScopeName(self.location.href)) {
+ case 'activate-fulfilled':
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'activate-rejected':
+ e.waitUntil(rejectPromise());
+ break;
+ }
+};
--- /dev/null
+// Adapter for testharness.js-style tests with Service Workers
+
+function service_worker_unregister_and_register(test, url, scope) {
+ if (!scope || scope.length == 0)
+ return Promise.reject(new Error('tests must define a scope'));
+
+ var options = { scope: scope };
+ return service_worker_unregister(test, scope)
+ .then(function() {
+ return navigator.serviceWorker.register(url, options);
+ })
+ .catch(unreached_rejection(test,
+ 'unregister and register should not fail'));
+}
+
+// This unregisters the registration that precisely matches scope. Use this
+// when unregistering by scope. If no registration is found, it just resolves.
+function service_worker_unregister(test, scope) {
+ var absoluteScope = (new URL(scope, window.location).href);
+ return navigator.serviceWorker.getRegistration(scope)
+ .then(function(registration) {
+ if (registration && registration.scope === absoluteScope)
+ return registration.unregister();
+ })
+ .catch(unreached_rejection(test, 'unregister should not fail'));
+}
+
+function service_worker_unregister_and_done(test, scope) {
+ return service_worker_unregister(test, scope)
+ .then(test.done.bind(test));
+}
+
+function unreached_fulfillment(test, prefix) {
+ return test.step_func(function(result) {
+ var error_prefix = prefix || 'unexpected fulfillment';
+ assert_unreached(error_prefix + ': ' + result);
+ });
+}
+
+// Rejection-specific helper that provides more details
+function unreached_rejection(test, prefix) {
+ return test.step_func(function(error) {
+ var reason = error.message || error.name || error;
+ var error_prefix = prefix || 'unexpected rejection';
+ assert_unreached(error_prefix + ': ' + reason);
+ });
+}
+
+// Adds an iframe to the document and returns a promise that resolves to the
+// iframe when it finishes loading. The caller is responsible for removing the
+// iframe later if needed.
+function with_iframe(url) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.className = 'test-iframe';
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function normalizeURL(url) {
+ return new URL(url, self.location).toString().replace(/#.*$/, '');
+}
+
+function wait_for_update(test, registration) {
+ if (!registration || registration.unregister == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_update must be passed a ServiceWorkerRegistration'));
+ }
+
+ return new Promise(test.step_func(function(resolve) {
+ registration.addEventListener('updatefound', test.step_func(function() {
+ resolve(registration.installing);
+ }));
+ }));
+}
+
+function wait_for_state(test, worker, state) {
+ if (!worker || worker.state == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_state must be passed a ServiceWorker'));
+ }
+ if (worker.state === state)
+ return Promise.resolve(state);
+
+ if (state === 'installing') {
+ switch (worker.state) {
+ case 'installed':
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return Promise.reject(new Error(
+ 'worker is ' + worker.state + ' but waiting for ' + state));
+ }
+ }
+
+ if (state === 'installed') {
+ switch (worker.state) {
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return Promise.reject(new Error(
+ 'worker is ' + worker.state + ' but waiting for ' + state));
+ }
+ }
+
+ if (state === 'activating') {
+ switch (worker.state) {
+ case 'activated':
+ case 'redundant':
+ return Promise.reject(new Error(
+ 'worker is ' + worker.state + ' but waiting for ' + state));
+ }
+ }
+
+ if (state === 'activated') {
+ switch (worker.state) {
+ case 'redundant':
+ return Promise.reject(new Error(
+ 'worker is ' + worker.state + ' but waiting for ' + state));
+ }
+ }
+
+ return new Promise(test.step_func(function(resolve, reject) {
+ worker.addEventListener('statechange', test.step_func(function() {
+ if (worker.state === state)
+ resolve(state);
+ }));
+ test.step_timeout(() => {
+ reject("wait_for_state timed out, waiting for state " + state + ", worker state is " + worker.state);
+ }, 10000);
+ }));
+}
+
+// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
+// is the service worker script URL. This function:
+// - Instantiates a new test with the description specified in |description|.
+// The test will succeed if the specified service worker can be successfully
+// registered and installed.
+// - Creates a new ServiceWorker registration with a scope unique to the current
+// document URL. Note that this doesn't allow more than one
+// service_worker_test() to be run from the same document.
+// - Waits for the new worker to begin installing.
+// - Imports tests results from tests running inside the ServiceWorker.
+function service_worker_test(url, description) {
+ // If the document URL is https://example.com/document and the script URL is
+ // https://example.com/script/worker.js, then the scope would be
+ // https://example.com/script/scope/document.
+ var scope = new URL('scope' + window.location.pathname,
+ new URL(url, window.location)).toString();
+ promise_test(function(test) {
+ return service_worker_unregister_and_register(test, url, scope)
+ .then(function(registration) {
+ add_completion_callback(function() {
+ registration.unregister();
+ });
+ return wait_for_update(test, registration)
+ .then(function(worker) {
+ return fetch_tests_from_worker(worker);
+ });
+ });
+ }, description);
+}
+
+function base_path() {
+ return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+
+function test_login(test, origin, username, password, cookie) {
+ return new Promise(function(resolve, reject) {
+ with_iframe(
+ origin + base_path() +
+ 'resources/fetch-access-control-login.html')
+ .then(test.step_func(function(frame) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = test.step_func(function() {
+ frame.remove();
+ resolve();
+ });
+ frame.contentWindow.postMessage(
+ {username: username, password: password, cookie: cookie},
+ origin, [channel.port2]);
+ }));
+ });
+}
+
+function test_websocket(test, frame, url) {
+ return new Promise(function(resolve, reject) {
+ var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
+ var openCalled = false;
+ ws.addEventListener('open', test.step_func(function(e) {
+ assert_equals(ws.readyState, 1, "The WebSocket should be open");
+ openCalled = true;
+ ws.close();
+ }), true);
+
+ ws.addEventListener('close', test.step_func(function(e) {
+ assert_true(openCalled, "The WebSocket should be closed after being opened");
+ resolve();
+ }), true);
+
+ ws.addEventListener('error', reject);
+ });
+}
+
+function login(test) {
+ var host_info = get_host_info();
+ return test_login(test, host_info.HTTP_REMOTE_ORIGIN,
+ 'username1s', 'password1s', 'cookie1')
+ .then(function() {
+ return test_login(test, host_info.HTTP_ORIGIN,
+ 'username2s', 'password2s', 'cookie2');
+ });
+}
+
+function login_https(test) {
+ var host_info = get_host_info();
+ return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
+ 'username1s', 'password1s', 'cookie1')
+ .then(function() {
+ return test_login(test, host_info.HTTPS_ORIGIN,
+ 'username2s', 'password2s', 'cookie2');
+ });
+}
+
+function websocket(test, frame) {
+ return test_websocket(test, frame, get_websocket_url());
+}
+
+function get_websocket_url() {
+ return 'wss://{{host}}:{{ports[wss][0]}}/echo';
+}
+
+// The navigator.serviceWorker.register() method guarantees that the newly
+// installing worker is available as registration.installing when its promise
+// resolves. However some tests test installation using a <link> element where
+// it is possible for the installing worker to have already become the waiting
+// or active worker. So this method is used to get the newest worker when these
+// tests need access to the ServiceWorker itself.
+function get_newest_worker(registration) {
+ if (registration.installing)
+ return registration.installing;
+ if (registration.waiting)
+ return registration.waiting;
+ if (registration.active)
+ return registration.active;
+}
+
+function register_using_link(script, options) {
+ var scope = options.scope;
+ var link = document.createElement('link');
+ link.setAttribute('rel', 'serviceworker');
+ link.setAttribute('href', script);
+ link.setAttribute('scope', scope);
+ document.getElementsByTagName('head')[0].appendChild(link);
+ return new Promise(function(resolve, reject) {
+ link.onload = resolve;
+ link.onerror = reject;
+ })
+ .then(() => navigator.serviceWorker.getRegistration(scope));
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.sandbox = sandbox;
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
var settings = {
output:true,
harness_timeout:{
- "normal":90000,
+ "normal":10000,
"long":60000
},
- test_timeout:null
+ test_timeout:null,
+ message_events: ["start", "test_state", "result", "completion"]
};
var xhtml_ns = "http://www.w3.org/1999/xhtml";
this.output_handler = null;
this.all_loaded = false;
var this_obj = this;
+ this.message_events = [];
+
+ this.message_functions = {
+ start: [add_start_callback, remove_start_callback,
+ function (properties) {
+ this_obj._dispatch("start_callback", [properties],
+ {type: "start", properties: properties});
+ }],
+
+ test_state: [add_test_state_callback, remove_test_state_callback,
+ function(test) {
+ this_obj._dispatch("test_state_callback", [test],
+ {type: "test_state",
+ test: test.structured_clone()});
+ }],
+ result: [add_result_callback, remove_result_callback,
+ function (test) {
+ this_obj.output_handler.show_status();
+ this_obj._dispatch("result_callback", [test],
+ {type: "result",
+ test: test.structured_clone()});
+ }],
+ completion: [add_completion_callback, remove_completion_callback,
+ function (tests, harness_status) {
+ var cloned_tests = map(tests, function(test) {
+ return test.structured_clone();
+ });
+ this_obj._dispatch("completion_callback", [tests, harness_status],
+ {type: "complete",
+ tests: cloned_tests,
+ status: harness_status.structured_clone()});
+ }]
+ }
+
on_event(window, 'load', function() {
this_obj.all_loaded = true;
});
WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
this._forEach_windows(
- function(w, is_same_origin) {
- if (is_same_origin && selector in w) {
+ function(w, same_origin) {
+ if (same_origin) {
try {
- w[selector].apply(undefined, callback_args);
- } catch (e) {
- if (debug) {
- throw e;
+ var has_selector = selector in w;
+ } catch(e) {
+ // If document.domain was set at some point same_origin can be
+ // wrong and the above will fail.
+ has_selector = false;
+ }
+ if (has_selector) {
+ try {
+ w[selector].apply(undefined, callback_args);
+ } catch (e) {
+ if (debug) {
+ throw e;
+ }
}
}
}
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() });
});
+ this.setup_messages(settings.message_events);
};
+ WindowTestEnvironment.prototype.setup_messages = function(new_events) {
+ var this_obj = this;
+ forEach(settings.message_events, function(x) {
+ var current_dispatch = this_obj.message_events.indexOf(x) !== -1;
+ var new_dispatch = new_events.indexOf(x) !== -1;
+ if (!current_dispatch && new_dispatch) {
+ this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
+ } else if (current_dispatch && !new_dispatch) {
+ this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
+ }
+ });
+ this.message_events = new_events;
+ }
+
WindowTestEnvironment.prototype.next_default_test_name = function() {
//Don't use document.title to work around an Opera bug in XHTML documents
var title = document.getElementsByTagName("title")[0];
WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
this.output_handler.setup(properties);
+ if (properties.hasOwnProperty("message_events")) {
+ this.setup_messages(properties.message_events);
+ }
};
WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
self.addEventListener("connect",
function(message_event) {
this_obj._add_message_port(message_event.source);
- });
+ }, false);
}
SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
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();
+ if (event.ports && event.ports[0]) {
+ // If a MessageChannel was passed, then use it to
+ // send results back to the main window. This
+ // allows the tests to work even if the browser
+ // does not fully support MessageEvent.source in
+ // ServiceWorkers yet.
+ this_obj._add_message_port(event.ports[0]);
+ event.ports[0].start();
+ } else {
+ // If there is no MessageChannel, then attempt to
+ // use the MessageEvent.source to send results
+ // back to the main window.
+ this_obj._add_message_port(event.source);
+ }
}
- });
+ }, false);
// The oninstall event is received after the service worker script and
// all imported scripts have been fetched and executed. It's the
self instanceof ServiceWorkerGlobalScope) {
return new ServiceWorkerTestEnvironment();
}
+ if ('WorkerGlobalScope' in self &&
+ self instanceof WorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+
throw new Error("Unsupported test environment");
}
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});
- }));
+ // If there is no promise tests queue make one.
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+ tests.promise_tests = tests.promise_tests.then(function() {
+ var donePromise = new Promise(function(resolve) {
+ test.add_cleanup(resolve);
+ });
+ var promise = test.step(func, test, test);
+ test.step(function() {
+ assert_not_equals(promise, undefined);
+ });
+ Promise.resolve(promise).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});
+ }));
+ return donePromise;
+ });
+ }
+
+ function promise_rejects(test, expected, promise, description) {
+ return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) {
+ assert_throws(expected, function() { throw e }, description);
+ });
+ }
+
+ /**
+ * This constructor helper allows DOM events to be handled using Promises,
+ * which can make it a lot easier to test a very specific series of events,
+ * including ensuring that unexpected events are not fired at any point.
+ */
+ function EventWatcher(test, watchedNode, eventTypes)
+ {
+ if (typeof eventTypes == 'string') {
+ eventTypes = [eventTypes];
+ }
+
+ var waitingFor = null;
+
+ var eventHandler = test.step_func(function(evt) {
+ assert_true(!!waitingFor,
+ 'Not expecting event, but got ' + evt.type + ' event');
+ assert_equals(evt.type, waitingFor.types[0],
+ 'Expected ' + waitingFor.types[0] + ' event, but got ' +
+ evt.type + ' event instead');
+ if (waitingFor.types.length > 1) {
+ // Pop first event from array
+ waitingFor.types.shift();
+ return;
+ }
+ // We need to null out waitingFor before calling the resolve function
+ // since the Promise's resolve handlers may call wait_for() which will
+ // need to set waitingFor.
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ resolveFunc(evt);
+ });
+
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.addEventListener(eventTypes[i], eventHandler, false);
+ }
+
+ /**
+ * Returns a Promise that will resolve after the specified event or
+ * series of events has occured.
+ */
+ this.wait_for = function(types) {
+ if (waitingFor) {
+ return Promise.reject('Already waiting for an event or events');
+ }
+ if (typeof types == 'string') {
+ types = [types];
+ }
+ return new Promise(function(resolve, reject) {
+ waitingFor = {
+ types: types,
+ resolve: resolve,
+ reject: reject
+ };
+ });
+ };
+
+ function stop_watching() {
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
+ }
+ };
+
+ test.add_cleanup(stop_watching);
+
+ return this;
}
+ expose(EventWatcher, 'EventWatcher');
function setup(func_or_properties, maybe_properties)
{
object.addEventListener(event, callback, false);
}
+ function step_timeout(f, t) {
+ var outer_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(function() {
+ f.apply(outer_this, args);
+ }, t * tests.timeout_multiplier);
+ }
+
expose(test, 'test');
expose(async_test, 'async_test');
expose(promise_test, 'promise_test');
+ expose(promise_rejects, 'promise_rejects');
expose(generate_tests, 'generate_tests');
expose(setup, 'setup');
expose(done, 'done');
expose(on_event, 'on_event');
+ expose(step_timeout, 'step_timeout');
/*
* Return a string truncated to the given length, with ... added at the end
// 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 {
+ var has_node_properties = ("nodeType" in object &&
+ "nodeName" in object &&
+ "nodeValue" in object &&
+ "childNodes" in object);
+ } catch (e) {
+ // We're probably cross-origin, which means we aren't a node
+ return false;
+ }
+
+ if (has_node_properties) {
try {
object.nodeType;
} catch (e) {
return false;
}
+ var replacements = {
+ "0": "0",
+ "1": "x01",
+ "2": "x02",
+ "3": "x03",
+ "4": "x04",
+ "5": "x05",
+ "6": "x06",
+ "7": "x07",
+ "8": "b",
+ "9": "t",
+ "10": "n",
+ "11": "v",
+ "12": "f",
+ "13": "r",
+ "14": "x0e",
+ "15": "x0f",
+ "16": "x10",
+ "17": "x11",
+ "18": "x12",
+ "19": "x13",
+ "20": "x14",
+ "21": "x15",
+ "22": "x16",
+ "23": "x17",
+ "24": "x18",
+ "25": "x19",
+ "26": "x1a",
+ "27": "x1b",
+ "28": "x1c",
+ "29": "x1d",
+ "30": "x1e",
+ "31": "x1f",
+ "0xfffd": "ufffd",
+ "0xfffe": "ufffe",
+ "0xffff": "uffff",
+ };
+
/*
* Convert a value to a nice, human-readable string
*/
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);
+ for (var p in replacements) {
+ var replace = "\\" + replacements[p];
+ val = val.replace(RegExp(String.fromCharCode(p), "g"), replace);
}
return '"' + val.replace(/"/g, '\\"') + '"';
case "boolean":
/* falls through */
default:
- return typeof val + ' "' + truncate(String(val), 60) + '"';
+ try {
+ return typeof val + ' "' + truncate(String(val), 1000) + '"';
+ } catch(e) {
+ return ("[stringifying object threw " + String(e) +
+ " with type " + String(typeof e) + "]");
+ }
}
}
expose(format_value, "format_value");
}
expose(assert_greater_than, "assert_greater_than");
+ function assert_between_exclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between two others
+ */
+ assert(typeof actual === "number",
+ "assert_between_exclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > lower && actual < upper,
+ "assert_between_exclusive", description,
+ "expected a number greater than ${lower} " +
+ "and less than ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose(assert_between_exclusive, "assert_between_exclusive");
+
function assert_less_than_equal(actual, expected, description)
{
/*
{type_actual:typeof actual});
assert(actual <= expected,
- "assert_less_than", description,
+ "assert_less_than_equal", description,
"expected a number less than or equal to ${expected} but got ${actual}",
{expected:expected, actual:actual});
}
}
expose(assert_greater_than_equal, "assert_greater_than_equal");
+ function assert_between_inclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between to two others or equal to either of them
+ */
+ assert(typeof actual === "number",
+ "assert_between_inclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= lower && actual <= upper,
+ "assert_between_inclusive", description,
+ "expected a number greater than or equal to ${lower} " +
+ "and less than or equal to ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose(assert_between_inclusive, "assert_between_inclusive");
+
function assert_regexp_match(actual, expected, description) {
/*
* Test if a string (actual) matches a regexp (expected)
function _assert_own_property(name) {
return function(object, property_name, description)
{
- assert(property_name in object,
+ assert(object.hasOwnProperty(property_name),
name, description,
"expected property ${p} missing", {p:property_name});
};
function _assert_inherits(name) {
return function (object, property_name, description)
{
- assert(typeof object === "object",
+ assert(typeof object === "object" || typeof object === "function",
name, description,
"provided value is not an object");
NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
NOT_FOUND_ERR: 'NotFoundError',
NOT_SUPPORTED_ERR: 'NotSupportedError',
+ INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
INVALID_STATE_ERR: 'InvalidStateError',
SYNTAX_ERR: 'SyntaxError',
INVALID_MODIFICATION_ERR: 'InvalidModificationError',
NoModificationAllowedError: 7,
NotFoundError: 8,
NotSupportedError: 9,
+ InUseAttributeError: 10,
InvalidStateError: 11,
SyntaxError: 12,
InvalidModificationError: 13,
InvalidNodeTypeError: 24,
DataCloneError: 25,
+ EncodingError: 0,
+ NotReadableError: 0,
UnknownError: 0,
ConstraintError: 0,
DataError: 0,
TransactionInactiveError: 0,
ReadOnlyError: 0,
- VersionError: 0
+ VersionError: 0,
+ OperationError: 0,
+ NotAllowedError: 0
};
if (!(name in name_code_map)) {
var required_props = { code: name_code_map[name] };
if (required_props.code === 0 ||
- ("name" in e && e.name !== e.name.toUpperCase() && e.name !== "DOMException")) {
+ (typeof e == "object" &&
+ "name" in e &&
+ e.name !== e.name.toUpperCase() &&
+ e.name !== "DOMException")) {
// New style exception: also test the name property.
required_props.name = name;
}
}
this.message = null;
+ this.stack = null;
this.steps = [];
}
this._structured_clone.status = this.status;
this._structured_clone.message = this.message;
+ this._structured_clone.stack = this.stack;
this._structured_clone.index = this.index;
return this._structured_clone;
};
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);
+ var message = String((typeof e === "object" && e !== null) ? e.message : e);
+ var stack = e.stack ? e.stack : null;
+
+ this.set_status(this.FAIL, message, stack);
this.phase = this.phases.HAS_RESULT;
this.done();
}
});
};
+ Test.prototype.step_timeout = function(f, timeout) {
+ var test_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(this.step_func(function() {
+ return f.apply(test_this, args);
+ }), timeout * tests.timeout_multiplier);
+ }
+
Test.prototype.add_cleanup = function(callback) {
this.cleanup_callbacks.push(callback);
};
}
};
- Test.prototype.set_status = function(status, message)
+ Test.prototype.set_status = function(status, message, stack)
{
this.status = status;
this.message = message;
+ this.stack = stack ? stack : null;
};
Test.prototype.timeout = function()
RemoteTest.prototype.structured_clone = function() {
var clone = {};
Object.keys(this).forEach(
- function(key) {
+ (function(key) {
if (typeof(this[key]) === "object") {
clone[key] = merge({}, this[key]);
} else {
clone[key] = this[key];
}
- });
+ }).bind(this));
clone.phases = merge({}, this.phases);
return clone;
};
RemoteTest.prototype.update_state_from = function(clone) {
this.status = clone.status;
this.message = clone.message;
+ this.stack = clone.stack;
if (this.phase === this.phases.INITIAL) {
this.phase = this.phases.STARTED;
}
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]);
+ if (window.MessageChannel) {
+ // The ServiceWorker's implicit MessagePort is currently not
+ // reliably accessible from the ServiceWorkerGlobalScope due to
+ // Blink setting MessageEvent.source to null for messages sent
+ // via ServiceWorker.postMessage(). Until that's resolved,
+ // create an explicit MessageChannel and pass one end to the
+ // worker.
+ var message_channel = new MessageChannel();
+ message_port = message_channel.port1;
+ message_port.start();
+ worker.postMessage({type: "connect"}, [message_channel.port2]);
+ } else {
+ // If MessageChannel is not available, then try the
+ // ServiceWorker.postMessage() approach using MessageEvent.source
+ // on the other end.
+ message_port = navigator.serviceWorker;
+ worker.postMessage({type: "connect"});
+ }
} else if (is_shared_worker(worker)) {
message_port = worker.port;
} else {
this.worker_done({
status: {
status: tests.status.ERROR,
- message: "Error in worker" + filename + ": " + message
+ message: "Error in worker" + filename + ": " + message,
+ stack: error.stack
}
});
error.preventDefault();
data.status.status !== data.status.OK) {
tests.status.status = data.status.status;
tests.status.message = data.status.message;
+ tests.status.stack = data.status.stack;
}
this.running = false;
this.worker = null;
{
this.status = null;
this.message = null;
+ this.stack = null;
}
TestsStatus.statuses = {
msg = msg ? String(msg) : msg;
this._structured_clone = merge({
status:this.status,
- message:msg
+ message:msg,
+ stack:this.stack
}, TestsStatus.statuses);
}
return this._structured_clone;
} catch (e) {
this.status.status = this.status.ERROR;
this.status.message = String(e);
+ this.status.stack = e.stack ? e.stack : null;
}
}
this.set_timeout();
tests.test_state_callbacks.push(callback);
}
- function add_result_callback(callback)
- {
+ function add_result_callback(callback) {
tests.test_done_callbacks.push(callback);
}
- function add_completion_callback(callback)
- {
- tests.all_done_callbacks.push(callback);
+ function add_completion_callback(callback) {
+ tests.all_done_callbacks.push(callback);
}
expose(add_start_callback, 'add_start_callback');
expose(add_result_callback, 'add_result_callback');
expose(add_completion_callback, 'add_completion_callback');
+ function remove(array, item) {
+ var index = array.indexOf(item);
+ if (index > -1) {
+ array.splice(index, 1);
+ }
+ }
+
+ function remove_start_callback(callback) {
+ remove(tests.start_callbacks, callback);
+ }
+
+ function remove_test_state_callback(callback) {
+ remove(tests.test_state_callbacks, callback);
+ }
+
+ function remove_result_callback(callback) {
+ remove(tests.test_done_callbacks, callback);
+ }
+
+ function remove_completion_callback(callback) {
+ remove(tests.all_done_callbacks, callback);
+ }
+
/*
* Output listener
*/
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 harness_url = get_harness_url();
+ if (harness_url !== null) {
var stylesheet = output_document.createElementNS(xhtml_ns, "link");
stylesheet.setAttribute("rel", "stylesheet");
- stylesheet.setAttribute("href", script_prefix + "testharness.css");
+ stylesheet.setAttribute("href", harness_url + "testharness.css");
var heads = output_document.getElementsByTagName("head");
if (heads.length) {
heads[0].appendChild(stylesheet);
if (harness_status.status === harness_status.ERROR) {
rv[0].push(["pre", {}, harness_status.message]);
+ if (harness_status.stack) {
+ rv[0].push(["pre", {}, harness_status.stack]);
+ }
}
return rv;
},
"</td><td>" +
(assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") +
escape_html(tests[i].message ? tests[i].message : " ") +
+ (tests[i].stack ? "<pre>" +
+ escape_html(tests[i].stack) +
+ "</pre>": "") +
"</td></tr>";
}
html += "</tbody></table>";
function AssertionError(message)
{
this.message = message;
+ this.stack = this.get_stack();
}
- AssertionError.prototype.toString = function() {
- return this.message;
- };
+ AssertionError.prototype = Object.create(Error.prototype);
+
+ AssertionError.prototype.get_stack = function() {
+ var stack = new Error().stack;
+ // IE11 does not initialize 'Error.stack' until the object is thrown.
+ if (!stack) {
+ try {
+ throw new Error();
+ } catch (e) {
+ stack = e.stack;
+ }
+ }
+
+ // 'Error.stack' is not supported in all browsers/versions
+ if (!stack) {
+ return "(Stack trace unavailable)";
+ }
+
+ var lines = stack.split("\n");
+
+ // Create a pattern to match stack frames originating within testharness.js. These include the
+ // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
+ // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ // in case it contains RegExp characters.
+ var script_url = get_script_url();
+ var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js";
+ var re = new RegExp(re_text + ":\\d+:\\d+");
+
+ // Some browsers include a preamble that specifies the type of the error object. Skip this by
+ // advancing until we find the first stack frame originating from testharness.js.
+ var i = 0;
+ while (!re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Then skip the top frames originating from testharness.js to begin the stack at the test code.
+ while (re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified.
+ if (i >= lines.length) {
+ return stack;
+ }
+
+ return lines.slice(i).join("\n");
+ }
function make_message(function_name, description, error, substitutions)
{
Array.prototype.push.apply(array, items);
}
- function forEach (array, callback, thisObj)
+ function forEach(array, callback, thisObj)
{
for (var i = 0; i < array.length; i++) {
if (array.hasOwnProperty(i)) {
}
}
+ /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
+ function get_script_url()
+ {
+ if (!('document' in self)) {
+ return undefined;
+ }
+
+ var scripts = document.getElementsByTagName("script");
+ for (var i = 0; i < scripts.length; i++) {
+ var src;
+ if (scripts[i].src) {
+ src = scripts[i].src;
+ } else if (scripts[i].href) {
+ //SVG case
+ src = scripts[i].href.baseVal;
+ }
+
+ var matches = src && src.match(/^(.*\/|)testharness\.js$/);
+ if (matches) {
+ return src;
+ }
+ }
+ return undefined;
+ }
+
+ /** Returns the URL path at which the files for testharness.js are assumed to reside (e.g., '/resources/').
+ The path is derived from inspecting the 'src' of the <script> tag that included 'testharness.js'. */
+ function get_harness_url()
+ {
+ var script_url = get_script_url();
+
+ // Exclude the 'testharness.js' file from the returned path, but '+ 1' to include the trailing slash.
+ return script_url ? script_url.slice(0, script_url.lastIndexOf('/') + 1) : undefined;
+ }
+
function supports_post_message(w)
{
var supports;
var type;
- // Given IE implements postMessage across nested iframes but not across
+ // 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.
//
var tests = new Tests();
- addEventListener("error", function(e) {
+ var error_handler = 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.set_status(test.FAIL, e.message, e.stack);
test.phase = test.phases.HAS_RESULT;
test.done();
done();
} else if (!tests.allow_uncaught_exception) {
tests.status.status = tests.status.ERROR;
tests.status.message = e.message;
+ tests.status.stack = e.stack;
}
- });
+ };
+
+ addEventListener("error", error_handler, false);
+ addEventListener("unhandledrejection", function(e){ error_handler(e.reason); }, false);
test_environment.on_tests_ready();
-/*global add_completion_callback, setup */
/*
* This file is intended for vendors to implement
* code needed to integrate testharness.js tests with their own test systems.
*
- * The default implementation extracts metadata from the tests and validates
- * it against the cached version that should be present in the test source
- * file. If the cache is not found or is out of sync, source code suitable for
- * caching the metadata is optionally generated.
- *
- * The cached metadata is present for extraction by test processing tools that
- * are unable to execute javascript.
- *
- * Metadata is attached to tests via the properties parameter in the test
- * constructor. See testharness.js for details.
- *
- * Typically test system integration will attach callbacks when each test has
- * run, using add_result_callback(callback(test)), or when the whole test file
- * has completed, using
- * add_completion_callback(callback(tests, harness_status)).
+ * Typically such integration will attach callbacks when each test is
+ * has run, using add_result_callback(callback(test)), or when the whole test file has
+ * completed, using add_completion_callback(callback(tests, harness_status)).
*
* For more documentation about the callback functions and the
* parameters they are called with see testharness.js
*/
-
-
-var metadata_generator = {
-
- currentMetadata: {},
- cachedMetadata: false,
- metadataProperties: ['help', 'assert', 'author'],
-
- error: function(message) {
- var messageElement = document.createElement('p');
- messageElement.setAttribute('class', 'error');
- this.appendText(messageElement, message);
-
- var summary = document.getElementById('summary');
- if (summary) {
- summary.parentNode.insertBefore(messageElement, summary);
- }
- else {
- document.body.appendChild(messageElement);
- }
- },
-
- /**
- * Ensure property value has contact information
- */
- validateContact: function(test, propertyName) {
- var result = true;
- var value = test.properties[propertyName];
- var values = Array.isArray(value) ? value : [value];
- for (var index = 0; index < values.length; index++) {
- value = values[index];
- var re = /(\S+)(\s*)<(.*)>(.*)/;
- if (! re.test(value)) {
- re = /(\S+)(\s+)(http[s]?:\/\/)(.*)/;
- if (! re.test(value)) {
- this.error('Metadata property "' + propertyName +
- '" for test: "' + test.name +
- '" must have name and contact information ' +
- '("name <email>" or "name http(s)://")');
- result = false;
- }
- }
- }
- return result;
- },
-
- /**
- * Extract metadata from test object
- */
- extractFromTest: function(test) {
- var testMetadata = {};
- // filter out metadata from other properties in test
- for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
- metaIndex++) {
- var meta = this.metadataProperties[metaIndex];
- if (test.properties.hasOwnProperty(meta)) {
- if ('author' == meta) {
- this.validateContact(test, meta);
- }
- testMetadata[meta] = test.properties[meta];
- }
- }
- return testMetadata;
- },
-
- /**
- * Compare cached metadata to extracted metadata
- */
- validateCache: function() {
- for (var testName in this.currentMetadata) {
- if (! this.cachedMetadata.hasOwnProperty(testName)) {
- return false;
- }
- var testMetadata = this.currentMetadata[testName];
- var cachedTestMetadata = this.cachedMetadata[testName];
- delete this.cachedMetadata[testName];
-
- for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
- metaIndex++) {
- var meta = this.metadataProperties[metaIndex];
- if (cachedTestMetadata.hasOwnProperty(meta) &&
- testMetadata.hasOwnProperty(meta)) {
- if (Array.isArray(cachedTestMetadata[meta])) {
- if (! Array.isArray(testMetadata[meta])) {
- return false;
- }
- if (cachedTestMetadata[meta].length ==
- testMetadata[meta].length) {
- for (var index = 0;
- index < cachedTestMetadata[meta].length;
- index++) {
- if (cachedTestMetadata[meta][index] !=
- testMetadata[meta][index]) {
- return false;
- }
- }
- }
- else {
- return false;
- }
- }
- else {
- if (Array.isArray(testMetadata[meta])) {
- return false;
- }
- if (cachedTestMetadata[meta] != testMetadata[meta]) {
- return false;
- }
- }
- }
- else if (cachedTestMetadata.hasOwnProperty(meta) ||
- testMetadata.hasOwnProperty(meta)) {
- return false;
- }
- }
- }
- for (var testName in this.cachedMetadata) {
- return false;
- }
- return true;
- },
-
- appendText: function(elemement, text) {
- elemement.appendChild(document.createTextNode(text));
- },
-
- jsonifyArray: function(arrayValue, indent) {
- var output = '[';
-
- if (1 == arrayValue.length) {
- output += JSON.stringify(arrayValue[0]);
- }
- else {
- for (var index = 0; index < arrayValue.length; index++) {
- if (0 < index) {
- output += ',\n ' + indent;
- }
- output += JSON.stringify(arrayValue[index]);
- }
- }
- output += ']';
- return output;
- },
-
- jsonifyObject: function(objectValue, indent) {
- var output = '{';
- var value;
-
- var count = 0;
- for (var property in objectValue) {
- ++count;
- if (Array.isArray(objectValue[property]) ||
- ('object' == typeof(value))) {
- ++count;
- }
- }
- if (1 == count) {
- for (var property in objectValue) {
- output += ' "' + property + '": ' +
- JSON.stringify(objectValue[property]) +
- ' ';
- }
- }
- else {
- var first = true;
- for (var property in objectValue) {
- if (! first) {
- output += ',';
- }
- first = false;
- output += '\n ' + indent + '"' + property + '": ';
- value = objectValue[property];
- if (Array.isArray(value)) {
- output += this.jsonifyArray(value, indent +
- ' '.substr(0, 5 + property.length));
- }
- else if ('object' == typeof(value)) {
- output += this.jsonifyObject(value, indent + ' ');
- }
- else {
- output += JSON.stringify(value);
- }
- }
- if (1 < output.length) {
- output += '\n' + indent;
- }
- }
- output += '}';
- return output;
- },
-
- /**
- * Generate javascript source code for captured metadata
- * Metadata is in pretty-printed JSON format
- */
- generateSource: function() {
- var source =
- '<script id="metadata_cache">/*\n' +
- this.jsonifyObject(this.currentMetadata, '') + '\n' +
- '*/</script>\n';
- return source;
- },
-
- /**
- * Add element containing metadata source code
- */
- addSourceElement: function(event) {
- var sourceWrapper = document.createElement('div');
- sourceWrapper.setAttribute('id', 'metadata_source');
-
- var instructions = document.createElement('p');
- if (this.cachedMetadata) {
- this.appendText(instructions,
- 'Replace the existing <script id="metadata_cache"> element ' +
- 'in the test\'s <head> with the following:');
- }
- else {
- this.appendText(instructions,
- 'Copy the following into the <head> element of the test ' +
- 'or the test\'s metadata sidecar file:');
- }
- sourceWrapper.appendChild(instructions);
-
- var sourceElement = document.createElement('pre');
- this.appendText(sourceElement, this.generateSource());
-
- sourceWrapper.appendChild(sourceElement);
-
- var messageElement = document.getElementById('metadata_issue');
- messageElement.parentNode.insertBefore(sourceWrapper,
- messageElement.nextSibling);
- messageElement.parentNode.removeChild(messageElement);
-
- (event.preventDefault) ? event.preventDefault() :
- event.returnValue = false;
- },
-
- /**
- * Extract the metadata cache from the cache element if present
- */
- getCachedMetadata: function() {
- var cacheElement = document.getElementById('metadata_cache');
-
- if (cacheElement) {
- var cacheText = cacheElement.firstChild.nodeValue;
- var openBrace = cacheText.indexOf('{');
- var closeBrace = cacheText.lastIndexOf('}');
- if ((-1 < openBrace) && (-1 < closeBrace)) {
- cacheText = cacheText.slice(openBrace, closeBrace + 1);
- try {
- this.cachedMetadata = JSON.parse(cacheText);
- }
- catch (exc) {
- this.cachedMetadata = 'Invalid JSON in Cached metadata. ';
- }
- }
- else {
- this.cachedMetadata = 'Metadata not found in cache element. ';
- }
- }
- },
-
- /**
- * Main entry point, extract metadata from tests, compare to cached version
- * if present.
- * If cache not present or differs from extrated metadata, generate an error
- */
- process: function(tests) {
- for (var index = 0; index < tests.length; index++) {
- var test = tests[index];
- if (this.currentMetadata.hasOwnProperty(test.name)) {
- this.error('Duplicate test name: ' + test.name);
- }
- else {
- this.currentMetadata[test.name] = this.extractFromTest(test);
- }
- }
-
- this.getCachedMetadata();
-
- var message = null;
- var messageClass = 'warning';
- var showSource = false;
-
- if (0 === tests.length) {
- if (this.cachedMetadata) {
- message = 'Cached metadata present but no tests. ';
- }
- }
- else if (1 === tests.length) {
- if (this.cachedMetadata) {
- message = 'Single test files should not have cached metadata. ';
- }
- else {
- var testMetadata = this.currentMetadata[tests[0].name];
- for (var meta in testMetadata) {
- if (testMetadata.hasOwnProperty(meta)) {
- message = 'Single tests should not have metadata. ' +
- 'Move metadata to <head>. ';
- break;
- }
- }
- }
- }
- else {
- if (this.cachedMetadata) {
- messageClass = 'error';
- if ('string' == typeof(this.cachedMetadata)) {
- message = this.cachedMetadata;
- showSource = true;
- }
- else if (! this.validateCache()) {
- message = 'Cached metadata out of sync. ';
- showSource = true;
- }
- }
- }
-
- if (message) {
- var messageElement = document.createElement('p');
- messageElement.setAttribute('id', 'metadata_issue');
- messageElement.setAttribute('class', messageClass);
- this.appendText(messageElement, message);
-
- if (showSource) {
- var link = document.createElement('a');
- this.appendText(link, 'Click for source code.');
- link.setAttribute('href', '#');
- link.setAttribute('onclick',
- 'metadata_generator.addSourceElement(event)');
- messageElement.appendChild(link);
- }
-
- var summary = document.getElementById('summary');
- if (summary) {
- summary.parentNode.insertBefore(messageElement, summary);
- }
- else {
- var log = document.getElementById('log');
- if (log) {
- log.appendChild(messageElement);
- }
- }
- }
- },
-
- setup: function() {
- add_completion_callback(
- function (tests, harness_status) {
- metadata_generator.process(tests, harness_status);
- });
+// Setup for WebKit JavaScript tests
+if (self.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+ testRunner.setCanOpenWindows();
+ testRunner.grantWebNotificationPermission("http://localhost:8800");
+ // Let's restrict calling testharness timeout() to wptserve tests for the moment.
+ // That will limit the impact to a small number of tests.
+ // The risk is that testharness timeout() might be called to late on slow bots to finish properly.
+ if (testRunner.timeout && (location.port == 8800 || location.port == 9443))
+ setTimeout(timeout, testRunner.timeout * 0.9);
+
+ // Make WebAudio map to webkitWebAudio for WPT tests
+ if (location.port == 8800 || location.port == 9443) {
+ self.AudioContext = self.webkitAudioContext;
+ self.OfflineAudioContext = self.webkitOfflineAudioContext;
}
-};
+}
-metadata_generator.setup();
+if (self.internals && internals.setICECandidateFiltering)
+ internals.setICECandidateFiltering(false);
+
+// Function used to convert the test status code into
+// the corresponding string
+function convertResult(resultStatus)
+{
+ if(resultStatus == 0)
+ return("PASS");
+ else if(resultStatus == 1)
+ return("FAIL");
+ else if(resultStatus == 2)
+ return("TIMEOUT");
+ else
+ return("NOTRUN");
+}
-/* If the parent window has a testharness_properties object,
- * we use this to provide the test settings. This is used by the
- * default in-browser runner to configure the timeout and the
- * rendering of results
- */
-try {
- if (window.opener && "testharness_properties" in window.opener) {
- /* If we pass the testharness_properties object as-is here without
- * JSON stringifying and reparsing it, IE fails & emits the message
- * "Could not complete the operation due to error 80700019".
- */
- setup(JSON.parse(JSON.stringify(window.opener.testharness_properties)));
- }
-} catch (e) {
+if (self.testRunner) {
+ /* Disable the default output of testharness.js. The default output formats
+ * test results into an HTML table. When that table is dumped as text, no
+ * spacing between cells is preserved, and it is therefore not readable. By
+ * setting output to false, the HTML table will not be created
+ */
+ setup({"output": false, "explicit_timeout": true});
+
+ /* Using a callback function, test results will be added to the page in a
+ * manner that allows dumpAsText to produce readable test results
+ */
+ add_completion_callback(function (tests, harness_status) {
+ var results = document.createElement("pre");
+ var resultStr = "\n";
+
+ // Sanitizes the given text for display in test results.
+ function sanitize(text) {
+ if (!text) {
+ return "";
+ }
+ text = text.replace(/\0/g, "\\0");
+ return text.replace(/\r/g, "\\r");
+ }
+
+ if(harness_status.status != 0)
+ resultStr += "Harness Error (" + convertResult(harness_status.status) + "), message = " + harness_status.message + "\n\n";
+
+ for (var i = 0; i < tests.length; i++) {
+ var message = sanitize(tests[i].message);
+ if (tests[i].status == 1 && !tests[i].dumpStack) {
+ // Remove stack for failed tests for proper string comparison without file paths.
+ // For a test to dump the stack set its dumpStack attribute to true.
+ var stackIndex = message.indexOf("(stack:");
+ if (stackIndex > 0)
+ message = message.substr(0, stackIndex);
+ }
+ resultStr += convertResult(tests[i].status) + " " + sanitize(tests[i].name) + " " + message + "\n";
+ }
+
+ results.innerText = resultStr;
+ var log = document.getElementById("log");
+ if (log)
+ log.appendChild(results);
+ else
+ document.body.appendChild(results);
+
+ // Wait for any other completion callbacks, which may eliminate test elements
+ // from the page and therefore reduce the output.
+ setTimeout(function () {
+ testRunner.notifyDone();
+ }, 0);
+ });
}
-// vim: set expandtab shiftwidth=4 tabstop=4:
<title>ExtendableEvent_waitUntil</title>
<meta charset="utf-8"/>
<script src="support/unitcommon.js"></script>
-<script src="../resources/test-helpers.js"></script>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
</head>
<body>
<div id="log"></div>
}
function syncWorker(test, worker, obj) {
- var channel = new MessageChannel();
- channel.port1.onmessage = test.step_func(function(e) {
- var message = e.data;
- assert_equals(message, 'SYNC', 'Should receive sync message from worker.');
- obj.synced = true;
- channel.port1.postMessage('ACK');
+ var channel = new MessageChannel();
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = test.step_func(function(e) {
+ var message = e.data;
+ assert_equals(message, 'SYNC',
+ 'Should receive sync message from worker.');
+ obj.synced = true;
+ channel.port1.postMessage('ACK');
+ resolve();
+ });
});
- worker.postMessage({port: channel.port2}, [channel.port2]);
}
async_test(function(t) {
- var scope = '../resources/install-fulfilled', onRegister, obj;
- onRegister = function(worker) {
- obj = {};
+ // Passing scope as the test switch for worker script.
+ var scope = '../resources/install-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
wait_for_state(t, worker, 'installed')
- .then(function() {
- assert_true(obj.synced, 'state should be "installed" after the waitUntil promise for "oninstall" is fulfilled.');
- service_worker_unregister_and_done(t, scope);
+ .then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "installed" after the waitUntil promise ' +
+ 'for "oninstall" is fulfilled.');
+ service_worker_unregister_and_done(t, scope);
})
- .catch(unreached_rejection(t));
+ .catch(unreached_rejection(t));
syncWorker(t, worker, obj);
- };
+ };
runTest(t, scope, onRegister);
-}, document.title);
-
+ }, 'Test install event waitUntil fulfilled');
+
+async_test(function(t) {
+ var scope = '../resources/install-multiple-fulfilled';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+ wait_for_state(t, worker, 'installed')
+ .then(function() {
+ assert_true(
+ obj1.synced && obj2.synced,
+ 'state should be "installed" after all waitUntil promises ' +
+ 'for "oninstall" are fulfilled.');
+ service_worker_unregister_and_done(t, scope);
+ })
+ .catch(unreached_rejection(t));
+ syncWorker(t, worker, obj1);
+ syncWorker(t, worker, obj2);
+ };
+ runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent multiple waitUntil fulfilled.');
+
+async_test(function(t) {
+ var scope = '../resources/install-reject-precedence';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+ wait_for_state(t, worker, 'redundant')
+ .then(function() {
+ assert_true(
+ obj1.synced,
+ 'The "redundant" state was entered after the first "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ assert_true(
+ obj2.synced,
+ 'The "redundant" state was entered after the third "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ service_worker_unregister_and_done(t, scope);
+ })
+ .catch(unreached_rejection(t));
+
+ syncWorker(t, worker, obj1)
+ .then(function() {
+ syncWorker(t, worker, obj2);
+ });
+ };
+ runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent waitUntil reject precedence.');
+
+async_test(function(t) {
+ var scope = '../resources/activate-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
+ wait_for_state(t, worker, 'activating')
+ .then(function() {
+ syncWorker(t, worker, obj);
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "activated" after the waitUntil promise ' +
+ 'for "onactivate" is fulfilled.');
+ service_worker_unregister_and_done(t, scope);
+ })
+ .catch(unreached_rejection(t));
+ };
+ runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil fulfilled');
+
+async_test(function(t) {
+ var scope = '../resources/install-rejected';
+ var onRegister = function(worker) {
+ wait_for_state(t, worker, 'redundant')
+ .then(function() {
+ service_worker_unregister_and_done(t, scope);
+ })
+ .catch(unreached_rejection(t));
+ };
+ runTest(t, scope, onRegister);
+ }, 'Test install event waitUntil rejected');
+
+async_test(function(t) {
+ var scope = '../resources/activate-rejected';
+ var onRegister = function(worker) {
+ wait_for_state(t, worker, 'activated')
+ .then(function() {
+ service_worker_unregister_and_done(t, scope);
+ })
+ .catch(unreached_rejection(t));
+ };
+ runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil rejected.');
+
</script>
</body>
</html>