From: zhongyuan Date: Tue, 2 Jul 2019 08:45:04 +0000 (+0800) Subject: [doNotMerge][serviceworker][DPTTIZEN-3127, update 1 tc and related js from upstream] X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=605a5d9c212b305638588db0e8d2f32c49b0e2b5;p=test%2Ftct%2Fweb%2Fapi.git [doNotMerge][serviceworker][DPTTIZEN-3127, update 1 tc and related js from upstream] Change-Id: I253bec8c6372f7f75e979333d34982a8262ed02c --- diff --git a/common/tct-serviceworkers-w3c-tests/resources/extendable-event-waituntil.js b/common/tct-serviceworkers-w3c-tests/resources/extendable-event-waituntil.js index c981020e6..20a9eb023 100755 --- a/common/tct-serviceworkers-w3c-tests/resources/extendable-event-waituntil.js +++ b/common/tct-serviceworkers-w3c-tests/resources/extendable-event-waituntil.js @@ -33,6 +33,55 @@ function fulfillPromise() { }); } +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; + } +}; diff --git a/common/tct-serviceworkers-w3c-tests/resources/test-helpers.sub.js b/common/tct-serviceworkers-w3c-tests/resources/test-helpers.sub.js new file mode 100644 index 000000000..612409fbc --- /dev/null +++ b/common/tct-serviceworkers-w3c-tests/resources/test-helpers.sub.js @@ -0,0 +1,273 @@ +// 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 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); + }); +} + diff --git a/common/tct-serviceworkers-w3c-tests/resources/testharness.js b/common/tct-serviceworkers-w3c-tests/resources/testharness.js index 6fb5a6518..20318b0bd 100755 --- a/common/tct-serviceworkers-w3c-tests/resources/testharness.js +++ b/common/tct-serviceworkers-w3c-tests/resources/testharness.js @@ -19,10 +19,11 @@ policies and contribution forms [3]. 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"; @@ -64,6 +65,40 @@ policies and contribution forms [3]. 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; }); @@ -71,13 +106,22 @@ policies and contribution forms [3]. 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; + } } } } @@ -141,30 +185,39 @@ policies and contribution forms [3]. 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]; @@ -176,6 +229,9 @@ policies and contribution forms [3]. 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) { @@ -333,7 +389,7 @@ policies and contribution forms [3]. self.addEventListener("connect", function(message_event) { this_obj._add_message_port(message_event.source); - }); + }, false); } SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); @@ -359,10 +415,22 @@ policies and contribution forms [3]. 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 @@ -403,6 +471,11 @@ policies and contribution forms [3]. self instanceof ServiceWorkerGlobalScope) { return new ServiceWorkerTestEnvironment(); } + if ('WorkerGlobalScope' in self && + self instanceof WorkerGlobalScope) { + return new DedicatedWorkerTestEnvironment(); + } + throw new Error("Unsupported test environment"); } @@ -449,20 +522,107 @@ policies and contribution forms [3]. 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) { @@ -508,13 +668,23 @@ policies and contribution forms [3]. 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 @@ -537,10 +707,17 @@ policies and contribution forms [3]. // 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) { @@ -553,6 +730,44 @@ policies and contribution forms [3]. 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 */ @@ -574,43 +789,9 @@ policies and contribution forms [3]. 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": @@ -658,7 +839,12 @@ policies and contribution forms [3]. /* 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"); @@ -836,6 +1022,24 @@ policies and contribution forms [3]. } 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) { /* @@ -847,7 +1051,7 @@ policies and contribution forms [3]. {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}); } @@ -870,6 +1074,24 @@ policies and contribution forms [3]. } 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) @@ -891,7 +1113,7 @@ policies and contribution forms [3]. 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}); }; @@ -910,7 +1132,7 @@ policies and contribution forms [3]. 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"); @@ -980,6 +1202,7 @@ policies and contribution forms [3]. 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', @@ -1006,6 +1229,7 @@ policies and contribution forms [3]. NoModificationAllowedError: 7, NotFoundError: 8, NotSupportedError: 9, + InUseAttributeError: 10, InvalidStateError: 11, SyntaxError: 12, InvalidModificationError: 13, @@ -1021,12 +1245,16 @@ policies and contribution forms [3]. 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)) { @@ -1036,7 +1264,10 @@ policies and contribution forms [3]. 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; } @@ -1110,6 +1341,7 @@ policies and contribution forms [3]. } this.message = null; + this.stack = null; this.steps = []; @@ -1146,6 +1378,7 @@ policies and contribution forms [3]. } 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; }; @@ -1178,15 +1411,10 @@ policies and contribution forms [3]. 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(); } @@ -1232,6 +1460,14 @@ policies and contribution forms [3]. }); }; + 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); }; @@ -1252,10 +1488,11 @@ policies and contribution forms [3]. } }; - 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() @@ -1312,13 +1549,13 @@ policies and contribution forms [3]. 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; }; @@ -1328,6 +1565,7 @@ policies and contribution forms [3]. 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; } @@ -1351,15 +1589,24 @@ policies and contribution forms [3]. 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 { @@ -1387,7 +1634,8 @@ policies and contribution forms [3]. 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(); @@ -1415,6 +1663,7 @@ policies and contribution forms [3]. 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; @@ -1437,6 +1686,7 @@ policies and contribution forms [3]. { this.status = null; this.message = null; + this.stack = null; } TestsStatus.statuses = { @@ -1454,7 +1704,8 @@ policies and contribution forms [3]. 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; @@ -1544,6 +1795,7 @@ policies and contribution forms [3]. } catch (e) { this.status.status = this.status.ERROR; this.status.message = String(e); + this.status.stack = e.stack ? e.stack : null; } } this.set_timeout(); @@ -1707,14 +1959,12 @@ policies and contribution forms [3]. 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'); @@ -1722,6 +1972,29 @@ policies and contribution forms [3]. 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 */ @@ -1829,28 +2102,11 @@ policies and contribution forms [3]. 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); @@ -1901,6 +2157,9 @@ policies and contribution forms [3]. 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; }, @@ -1998,6 +2257,9 @@ policies and contribution forms [3]. "" + (assertions ? escape_html(get_assertion(tests[i])) + "" : "") + escape_html(tests[i].message ? tests[i].message : " ") + + (tests[i].stack ? "
" +
+                 escape_html(tests[i].stack) +
+                 "
": "") + ""; } html += ""; @@ -2193,11 +2455,56 @@ policies and contribution forms [3]. 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) { @@ -2243,7 +2550,7 @@ policies and contribution forms [3]. 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)) { @@ -2287,11 +2594,46 @@ policies and contribution forms [3]. } } + /** Returns the 'src' URL of the first \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 - + + +
@@ -44,31 +46,133 @@ function runTest(test, scope, onRegister) { } 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.'); +