[doNotMerge][serviceworker][DPTTIZEN-3127, update 1 tc and related js from upstream] 12/209112/1
authorzhongyuan <zy123.yuan@samsung.com>
Tue, 2 Jul 2019 08:45:04 +0000 (16:45 +0800)
committerzhongyuan <zy123.yuan@samsung.com>
Tue, 2 Jul 2019 08:45:04 +0000 (16:45 +0800)
Change-Id: I253bec8c6372f7f75e979333d34982a8262ed02c

common/tct-serviceworkers-w3c-tests/resources/extendable-event-waituntil.js
common/tct-serviceworkers-w3c-tests/resources/test-helpers.sub.js [new file with mode: 0644]
common/tct-serviceworkers-w3c-tests/resources/testharness.js
common/tct-serviceworkers-w3c-tests/resources/testharnessreport.js
common/tct-serviceworkers-w3c-tests/serviceworkers/ExtendableEvent_waitUntil.html

index c981020e6b0f17413f294aaea1becc696b910540..20a9eb023f6213c793e148b2eefba7eab81ce79c 100755 (executable)
@@ -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 (file)
index 0000000..612409f
--- /dev/null
@@ -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 <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);
+    });
+}
+
index 6fb5a6518f95a6b50ab26f0b3a45d98c51b0c918..20318b0bd08336cd12bf88a6151fdd47989f48d6 100755 (executable)
@@ -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].
                 "</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>";
@@ -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 <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.
         //
@@ -2330,22 +2672,25 @@ policies and contribution forms [3].
 
     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();
 
index d7df7e2fb532b5317ac7e09c260cca246d28906e..4036e56a95eca02ac052d8d392dcdb2b44b89d76 100755 (executable)
-/*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:
index c8187331d208745b75628ceb9a64b69bc3f9c313..20d2377cff27d69c7089bfc59a9c370cdfba4116 100755 (executable)
@@ -23,7 +23,9 @@ Authors:
 <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>
@@ -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.');
+
 </script>
 </body>
 </html>