| Index: LayoutTests/http/tests/w3c/resources/testharness.js
|
| diff --git a/LayoutTests/http/tests/w3c/resources/testharness.js b/LayoutTests/http/tests/w3c/resources/testharness.js
|
| index 63453cea93d54e614e612dea56ef531986f036cc..67b0f406c08fd9f634892eec50fb1f918d17e57c 100644
|
| --- a/LayoutTests/http/tests/w3c/resources/testharness.js
|
| +++ b/LayoutTests/http/tests/w3c/resources/testharness.js
|
| @@ -10,479 +10,428 @@ policies and contribution forms [3].
|
| [3] http://www.w3.org/2004/10/27-testcases
|
| */
|
|
|
| -/*
|
| - * == Introduction ==
|
| - *
|
| - * This file provides a framework for writing testcases. It is intended to
|
| - * provide a convenient API for making common assertions, and to work both
|
| - * for testing synchronous and asynchronous DOM features in a way that
|
| - * promotes clear, robust, tests.
|
| - *
|
| - * == Basic Usage ==
|
| - *
|
| - * To use this file, import the script and the testharnessreport script into
|
| - * the test document:
|
| -
|
| -<!doctype html>
|
| -<title></title>
|
| -<script src="/resources/testharness.js"></script>
|
| -<script src="/resources/testharnessreport.js"></script>
|
| -
|
| - * Within each file one may define one or more tests. Each test is atomic
|
| - * in the sense that a single test has a single result (pass/fail/timeout).
|
| - * Within each test one may have a number of asserts. The test fails at the
|
| - * first failing assert, and the remainder of the test is (typically) not run.
|
| - *
|
| - * If the file containing the tests is a HTML file, a table containing the test
|
| - * results will be added to the document after all tests have run. By default this
|
| - * will be added to a div element with id=log if it exists, or a new div element
|
| - * appended to document.body if it does not.
|
| - *
|
| - * NOTE: By default tests must be created before the load event fires. For ways
|
| - * to create tests after the load event, see "Determining when all tests
|
| - * are complete", below
|
| - *
|
| - * == Synchronous Tests ==
|
| - *
|
| - * To create a synchronous test use the test() function:
|
| - *
|
| - * test(test_function, name, properties)
|
| - *
|
| - * test_function is a function that contains the code to test. For example a
|
| - * trivial passing test would be:
|
| - *
|
| - * test(function() {assert_true(true)}, "assert_true with true")
|
| - *
|
| - * The function passed in is run in the test() call.
|
| - *
|
| - * properties is an object that overrides default test properties. The
|
| - * recognised properties are:
|
| - * timeout - the test timeout in ms
|
| - *
|
| - * e.g.
|
| - * test(test_function, "Sample test", {timeout:1000})
|
| - *
|
| - * would run test_function with a timeout of 1s.
|
| - *
|
| - * Additionally, test-specific metadata can be passed in the properties. These
|
| - * are used when the individual test has different metadata from that stored
|
| - * in the <head>.
|
| - * The recognized metadata properties are:
|
| - *
|
| - * help - The url or an array of urls of the part(s) of the specification(s)
|
| - * being tested
|
| - *
|
| - * assert - A human readable description of what the test is attempting
|
| - * to prove
|
| - *
|
| - * author - Name and contact information for the author of the test in the
|
| - * format: "Name <email_addr>" or "Name http://contact/url"
|
| - *
|
| - * == Asynchronous Tests ==
|
| - *
|
| - * Testing asynchronous features is somewhat more complex since the result of
|
| - * a test may depend on one or more events or other callbacks. The API provided
|
| - * for testing these features is indended to be rather low-level but hopefully
|
| - * applicable to many situations.
|
| - *
|
| - * To create a test, one starts by getting a Test object using async_test:
|
| - *
|
| - * async_test(name, properties)
|
| - *
|
| - * e.g.
|
| - * var t = async_test("Simple async test")
|
| - *
|
| - * Assertions can be added to the test by calling the step method of the test
|
| - * object with a function containing the test assertions:
|
| - *
|
| - * t.step(function() {assert_true(true)});
|
| - *
|
| - * When all the steps are complete, the done() method must be called:
|
| - *
|
| - * t.done();
|
| - *
|
| - * As a convenience, async_test can also takes a function as first argument.
|
| - * This function is called with the test object as both its `this` object and
|
| - * first argument. The above example can be rewritten as:
|
| - *
|
| - * async_test(function(t) {
|
| - * object.some_event = function() {
|
| - * t.step(function (){assert_true(true); t.done();});
|
| - * };
|
| - * }, "Simple async test");
|
| - *
|
| - * which avoids cluttering the global scope with references to async
|
| - * tests instances.
|
| - *
|
| - * The properties argument is identical to that for test().
|
| - *
|
| - * In many cases it is convenient to run a step in response to an event or a
|
| - * callback. A convenient method of doing this is through the step_func method
|
| - * which returns a function that, when called runs a test step. For example
|
| - *
|
| - * object.some_event = t.step_func(function(e) {assert_true(e.a)});
|
| - *
|
| - * For asynchronous callbacks that should never execute, unreached_func can
|
| - * be used. For example:
|
| - *
|
| - * object.some_event = t.unreached_func("some_event should not fire");
|
| - *
|
| - * == Single Page Tests ==
|
| - *
|
| - * Sometimes, particularly when dealing with asynchronous behaviour,
|
| - * having exactly one test per page is desirable, and the overhead of
|
| - * wrapping everything in functions for isolation becomes
|
| - * burdensome. For these cases testharness.js support "single page
|
| - * tests".
|
| - *
|
| - * In order for a test to be interpreted as a "single page" test, the
|
| - * it must simply not call test() or async_test() anywhere on the page, and
|
| - * must call the done() function to indicate that the test is complete. All
|
| - * the assert_* functions are avaliable as normal, but are called without
|
| - * the normal step function wrapper. For example:
|
| - *
|
| - * <!doctype html>
|
| - * <title>Example single-page test</title>
|
| - * <script src="/resources/testharness.js"></script>
|
| - * <script src="/resources/testharnessreport.js"></script>
|
| - * <body>
|
| - * <script>
|
| - * assert_equals(document.body, document.getElementsByTagName("body")[0])
|
| - * done()
|
| - * </script>
|
| - *
|
| - * The test title for sinple page tests is always taken from document.title.
|
| - *
|
| - * == Making assertions ==
|
| - *
|
| - * Functions for making assertions start assert_
|
| - * The best way to get a list is to look in this file for functions names
|
| - * matching that pattern. The general signature is
|
| - *
|
| - * assert_something(actual, expected, description)
|
| - *
|
| - * although not all assertions precisely match this pattern e.g. assert_true
|
| - * only takes actual and description as arguments.
|
| - *
|
| - * The description parameter is used to present more useful error messages when
|
| - * a test fails
|
| - *
|
| - * NOTE: All asserts must be located in a test() or a step of an async_test().
|
| - * asserts outside these places won't be detected correctly by the harness
|
| - * and may cause a file to stop testing.
|
| - *
|
| - * == Cleanup ==
|
| - *
|
| - * Occasionally tests may create state that will persist beyond the test itself.
|
| - * In order to ensure that tests are independent, such state should be cleaned
|
| - * up once the test has a result. This can be achieved by adding cleanup
|
| - * callbacks to the test. Such callbacks are registered using the add_cleanup
|
| - * function on the test object. All registered callbacks will be run as soon as
|
| - * the test result is known. For example
|
| - * test(function() {
|
| - * window.some_global = "example";
|
| - * this.add_cleanup(function() {delete window.some_global});
|
| - * assert_true(false);
|
| - * });
|
| - *
|
| - * == Harness Timeout ==
|
| - *
|
| - * The overall harness admits two timeout values "normal" (the
|
| - * default) and "long", used for tests which have an unusually long
|
| - * runtime. After the timeout is reached, the harness will stop
|
| - * waiting for further async tests to complete. By default the
|
| - * timeouts are set to 10s and 60s, respectively, but may be changed
|
| - * when the test is run on hardware with different performance
|
| - * characteristics to a common desktop computer. In order to opt-in
|
| - * to the longer test timeout, the test must specify a meta element:
|
| - * <meta name="timeout" content="long">
|
| - *
|
| - * Occasionally tests may have a race between the harness timing out and
|
| - * a particular test failing; typically when the test waits for some event
|
| - * that never occurs. In this case it is possible to use test.force_timeout()
|
| - * in place of assert_unreached(), to immediately fail the test but with a
|
| - * status of "timeout". This should only be used as a last resort when it is
|
| - * not possible to make the test reliable in some other way.
|
| - *
|
| - * == Setup ==
|
| - *
|
| - * Sometimes tests require non-trivial setup that may fail. For this purpose
|
| - * there is a setup() function, that may be called with one or two arguments.
|
| - * The two argument version is:
|
| - *
|
| - * setup(func, properties)
|
| - *
|
| - * The one argument versions may omit either argument.
|
| - * func is a function to be run synchronously. setup() becomes a no-op once
|
| - * any tests have returned results. Properties are global properties of the test
|
| - * harness. Currently recognised properties are:
|
| - *
|
| - *
|
| - * explicit_done - Wait for an explicit call to done() before declaring all
|
| - * tests complete (see below; implicitly true for single page
|
| - * tests)
|
| - *
|
| - * output_document - The document to which results should be logged. By default
|
| - * this is the current document but could be an ancestor
|
| - * document in some cases e.g. a SVG test loaded in an HTML
|
| - * wrapper
|
| - *
|
| - * explicit_timeout - disable file timeout; only stop waiting for results
|
| - * when the timeout() function is called (typically for
|
| - * use when integrating with some existing test framework
|
| - * that has its own timeout mechanism).
|
| - *
|
| - * allow_uncaught_exception - don't treat an uncaught exception as an error;
|
| - * needed when e.g. testing the window.onerror
|
| - * handler.
|
| - *
|
| - * timeout_multiplier - Multiplier to apply to per-test timeouts.
|
| - *
|
| - * == Determining when all tests are complete ==
|
| - *
|
| - * By default the test harness will assume there are no more results to come
|
| - * when:
|
| - * 1) There are no Test objects that have been created but not completed
|
| - * 2) The load event on the document has fired
|
| - *
|
| - * This behaviour can be overridden by setting the explicit_done property to
|
| - * true in a call to setup(). If explicit_done is true, the test harness will
|
| - * not assume it is done until the global done() function is called. Once done()
|
| - * is called, the two conditions above apply like normal.
|
| - *
|
| - * == Generating tests ==
|
| - *
|
| - * NOTE: this functionality may be removed
|
| - *
|
| - * There are scenarios in which is is desirable to create a large number of
|
| - * (synchronous) tests that are internally similar but vary in the parameters
|
| - * used. To make this easier, the generate_tests function allows a single
|
| - * function to be called with each set of parameters in a list:
|
| - *
|
| - * generate_tests(test_function, parameter_lists, properties)
|
| - *
|
| - * For example:
|
| - *
|
| - * generate_tests(assert_equals, [
|
| - * ["Sum one and one", 1+1, 2],
|
| - * ["Sum one and zero", 1+0, 1]
|
| - * ])
|
| - *
|
| - * Is equivalent to:
|
| - *
|
| - * test(function() {assert_equals(1+1, 2)}, "Sum one and one")
|
| - * test(function() {assert_equals(1+0, 1)}, "Sum one and zero")
|
| - *
|
| - * Note that the first item in each parameter list corresponds to the name of
|
| - * the test.
|
| - *
|
| - * The properties argument is identical to that for test(). This may be a
|
| - * single object (used for all generated tests) or an array.
|
| - *
|
| - * == Callback API ==
|
| - *
|
| - * The framework provides callbacks corresponding to 3 events:
|
| - *
|
| - * start - happens when the first Test is created
|
| - * result - happens when a test result is recieved
|
| - * complete - happens when all results are recieved
|
| - *
|
| - * The page defining the tests may add callbacks for these events by calling
|
| - * the following methods:
|
| - *
|
| - * add_start_callback(callback) - callback called with no arguments
|
| - * add_result_callback(callback) - callback called with a test argument
|
| - * add_completion_callback(callback) - callback called with an array of tests
|
| - * and an status object
|
| - *
|
| - * tests have the following properties:
|
| - * status: A status code. This can be compared to the PASS, FAIL, TIMEOUT and
|
| - * NOTRUN properties on the test object
|
| - * message: A message indicating the reason for failure. In the future this
|
| - * will always be a string
|
| - *
|
| - * The status object gives the overall status of the harness. It has the
|
| - * following properties:
|
| - * status: Can be compared to the OK, ERROR and TIMEOUT properties
|
| - * message: An error message set when the status is ERROR
|
| - *
|
| - * == External API ==
|
| - *
|
| - * In order to collect the results of multiple pages containing tests, the test
|
| - * harness will, when loaded in a nested browsing context, attempt to call
|
| - * certain functions in each ancestor and opener browsing context:
|
| - *
|
| - * start - start_callback
|
| - * result - result_callback
|
| - * complete - completion_callback
|
| - *
|
| - * These are given the same arguments as the corresponding internal callbacks
|
| - * described above.
|
| - *
|
| - * == External API through cross-document messaging ==
|
| - *
|
| - * Where supported, the test harness will also send messages using
|
| - * cross-document messaging to each ancestor and opener browsing context. Since
|
| - * it uses the wildcard keyword (*), cross-origin communication is enabled and
|
| - * script on different origins can collect the results.
|
| - *
|
| - * This API follows similar conventions as those described above only slightly
|
| - * modified to accommodate message event API. Each message is sent by the harness
|
| - * is passed a single vanilla object, available as the `data` property of the
|
| - * event object. These objects are structures as follows:
|
| - *
|
| - * start - { type: "start" }
|
| - * result - { type: "result", test: Test }
|
| - * complete - { type: "complete", tests: [Test, ...], status: TestsStatus }
|
| - *
|
| - * == List of assertions ==
|
| - *
|
| - * assert_true(actual, description)
|
| - * asserts that /actual/ is strictly true
|
| - *
|
| - * assert_false(actual, description)
|
| - * asserts that /actual/ is strictly false
|
| - *
|
| - * assert_equals(actual, expected, description)
|
| - * asserts that /actual/ is the same value as /expected/
|
| - *
|
| - * assert_not_equals(actual, expected, description)
|
| - * asserts that /actual/ is a different value to /expected/. Yes, this means
|
| - * that "expected" is a misnomer
|
| - *
|
| - * assert_in_array(actual, expected, description)
|
| - * asserts that /expected/ is an Array, and /actual/ is equal to one of the
|
| - * members -- expected.indexOf(actual) != -1
|
| - *
|
| - * assert_array_equals(actual, expected, description)
|
| - * asserts that /actual/ and /expected/ have the same length and the value of
|
| - * each indexed property in /actual/ is the strictly equal to the corresponding
|
| - * property value in /expected/
|
| - *
|
| - * assert_approx_equals(actual, expected, epsilon, description)
|
| - * asserts that /actual/ is a number within +/- /epsilon/ of /expected/
|
| - *
|
| - * assert_less_than(actual, expected, description)
|
| - * asserts that /actual/ is a number less than /expected/
|
| - *
|
| - * assert_greater_than(actual, expected, description)
|
| - * asserts that /actual/ is a number greater than /expected/
|
| - *
|
| - * assert_less_than_equal(actual, expected, description)
|
| - * asserts that /actual/ is a number less than or equal to /expected/
|
| - *
|
| - * assert_greater_than_equal(actual, expected, description)
|
| - * asserts that /actual/ is a number greater than or equal to /expected/
|
| - *
|
| - * assert_regexp_match(actual, expected, description)
|
| - * asserts that /actual/ matches the regexp /expected/
|
| - *
|
| - * assert_class_string(object, class_name, description)
|
| - * asserts that the class string of /object/ as returned in
|
| - * Object.prototype.toString is equal to /class_name/.
|
| - *
|
| - * assert_own_property(object, property_name, description)
|
| - * assert that object has own property property_name
|
| - *
|
| - * assert_inherits(object, property_name, description)
|
| - * assert that object does not have an own property named property_name
|
| - * but that property_name is present in the prototype chain for object
|
| - *
|
| - * assert_idl_attribute(object, attribute_name, description)
|
| - * assert that an object that is an instance of some interface has the
|
| - * attribute attribute_name following the conditions specified by WebIDL
|
| - *
|
| - * assert_readonly(object, property_name, description)
|
| - * assert that property property_name on object is readonly
|
| - *
|
| - * assert_throws(code, func, description)
|
| - * code - the expected exception:
|
| - * o string: the thrown exception must be a DOMException with the given
|
| - * name, e.g., "TimeoutError" (for compatibility with existing
|
| - * tests, a constant is also supported, e.g., "TIMEOUT_ERR")
|
| - * o object: the thrown exception must have a property called "name" that
|
| - * matches code.name
|
| - * o null: allow any exception (in general, one of the options above
|
| - * should be used)
|
| - * func - a function that should throw
|
| - *
|
| - * assert_unreached(description)
|
| - * asserts if called. Used to ensure that some codepath is *not* taken e.g.
|
| - * an event does not fire.
|
| - *
|
| - * assert_any(assert_func, actual, expected_array, extra_arg_1, ... extra_arg_N)
|
| - * asserts that one assert_func(actual, expected_array_N, extra_arg1, ..., extra_arg_N)
|
| - * is true for some expected_array_N in expected_array. This only works for assert_func
|
| - * with signature assert_func(actual, expected, args_1, ..., args_N). Note that tests
|
| - * with multiple allowed pass conditions are bad practice unless the spec specifically
|
| - * allows multiple behaviours. Test authors should not use this method simply to hide
|
| - * UA bugs.
|
| - *
|
| - * assert_exists(object, property_name, description)
|
| - * *** deprecated ***
|
| - * asserts that object has an own property property_name
|
| - *
|
| - * assert_not_exists(object, property_name, description)
|
| - * *** deprecated ***
|
| - * assert that object does not have own property property_name
|
| - */
|
| +/* Documentation is in docs/api.md */
|
|
|
| (function ()
|
| {
|
| var debug = false;
|
| - // default timeout is 5 minutes, to let LayoutTests timeout first
|
| + // default timeout is 10 seconds, test can override if needed
|
| var settings = {
|
| output:true,
|
| harness_timeout:{
|
| - "normal":300000,
|
| - "long":300000
|
| + "normal":10000,
|
| + "long":60000
|
| },
|
| test_timeout:null
|
| };
|
|
|
| var xhtml_ns = "http://www.w3.org/1999/xhtml";
|
|
|
| - // script_prefix is used by Output.prototype.show_results() to figure out
|
| - // where to get testharness.css from. It's enclosed in an extra closure to
|
| - // not pollute the library's namespace with variables like "src".
|
| - var script_prefix = null;
|
| - (function ()
|
| - {
|
| - 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;
|
| - }
|
| + /*
|
| + * TestEnvironment is an abstraction for the environment in which the test
|
| + * harness is used. Each implementation of a test environment has to provide
|
| + * the following interface:
|
| + *
|
| + * interface TestEnvironment {
|
| + * // Invoked after the global 'tests' object has been created and it's
|
| + * // safe to call add_*_callback() to register event handlers.
|
| + * void on_tests_ready();
|
| + *
|
| + * // Invoked after setup() has been called to notify the test environment
|
| + * // of changes to the test harness properties.
|
| + * void on_new_harness_properties(object properties);
|
| + *
|
| + * // Should return a new unique default test name.
|
| + * DOMString next_default_test_name();
|
| + *
|
| + * // Should return the test harness timeout duration in milliseconds.
|
| + * float test_timeout();
|
| + *
|
| + * // Should return the global scope object.
|
| + * object global_scope();
|
| + * };
|
| + */
|
|
|
| - if (src && src.slice(src.length - "testharness.js".length) === "testharness.js") {
|
| - script_prefix = src.slice(0, src.length - "testharness.js".length);
|
| - break;
|
| + /*
|
| + * A test environment with a DOM. The global object is 'window'. By default
|
| + * test results are displayed in a table. Any parent windows receive
|
| + * callbacks or messages via postMessage() when test events occur. See
|
| + * apisample11.html and apisample12.html.
|
| + */
|
| + function WindowTestEnvironment() {
|
| + this.name_counter = 0;
|
| + this.window_cache = null;
|
| + this.output_handler = null;
|
| + this.all_loaded = false;
|
| + var this_obj = this;
|
| + on_event(window, 'load', function() {
|
| + this_obj.all_loaded = true;
|
| + });
|
| + }
|
| +
|
| + WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
|
| + this._forEach_windows(
|
| + function(w, same_origin) {
|
| + if (same_origin) {
|
| + try {
|
| + var has_selector = selector in w;
|
| + } catch(e) {
|
| + // If document.domain was set at some point same_origin can be
|
| + // wrong and the above will fail.
|
| + has_selector = false;
|
| + }
|
| + if (has_selector) {
|
| + try {
|
| + w[selector].apply(undefined, callback_args);
|
| + } catch (e) {
|
| + if (debug) {
|
| + throw e;
|
| + }
|
| + }
|
| + }
|
| + }
|
| + if (supports_post_message(w) && w !== self) {
|
| + w.postMessage(message_arg, "*");
|
| + }
|
| + });
|
| + };
|
| +
|
| + WindowTestEnvironment.prototype._forEach_windows = function(callback) {
|
| + // Iterate of the the windows [self ... top, opener]. The callback is passed
|
| + // two objects, the first one is the windows object itself, the second one
|
| + // is a boolean indicating whether or not its on the same origin as the
|
| + // current window.
|
| + var cache = this.window_cache;
|
| + if (!cache) {
|
| + cache = [[self, true]];
|
| + var w = self;
|
| + var i = 0;
|
| + var so;
|
| + var origins = location.ancestorOrigins;
|
| + while (w != w.parent) {
|
| + w = w.parent;
|
| + // In WebKit, calls to parent windows' properties that aren't on the same
|
| + // origin cause an error message to be displayed in the error console but
|
| + // don't throw an exception. This is a deviation from the current HTML5
|
| + // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504
|
| + // The problem with WebKit's behavior is that it pollutes the error console
|
| + // with error messages that can't be caught.
|
| + //
|
| + // This issue can be mitigated by relying on the (for now) proprietary
|
| + // `location.ancestorOrigins` property which returns an ordered list of
|
| + // the origins of enclosing windows. See:
|
| + // http://trac.webkit.org/changeset/113945.
|
| + if (origins) {
|
| + so = (location.origin == origins[i]);
|
| + } else {
|
| + so = is_same_origin(w);
|
| + }
|
| + cache.push([w, so]);
|
| + i++;
|
| + }
|
| + w = window.opener;
|
| + if (w) {
|
| + // window.opener isn't included in the `location.ancestorOrigins` prop.
|
| + // We'll just have to deal with a simple check and an error msg on WebKit
|
| + // browsers in this case.
|
| + cache.push([w, is_same_origin(w)]);
|
| }
|
| + this.window_cache = cache;
|
| }
|
| - })();
|
|
|
| - /*
|
| - * API functions
|
| - */
|
| + forEach(cache,
|
| + function(a) {
|
| + callback.apply(null, a);
|
| + });
|
| + };
|
|
|
| - var name_counter = 0;
|
| - function next_default_name()
|
| - {
|
| + WindowTestEnvironment.prototype.on_tests_ready = function() {
|
| + var output = new Output();
|
| + this.output_handler = output;
|
| +
|
| + var this_obj = this;
|
| + add_start_callback(function (properties) {
|
| + this_obj.output_handler.init(properties);
|
| + this_obj._dispatch("start_callback", [properties],
|
| + { type: "start", properties: properties });
|
| + });
|
| + add_test_state_callback(function(test) {
|
| + this_obj.output_handler.show_status();
|
| + this_obj._dispatch("test_state_callback", [test],
|
| + { type: "test_state", test: test.structured_clone() });
|
| + });
|
| + add_result_callback(function (test) {
|
| + this_obj.output_handler.show_status();
|
| + this_obj._dispatch("result_callback", [test],
|
| + { type: "result", test: test.structured_clone() });
|
| + });
|
| + add_completion_callback(function (tests, harness_status) {
|
| + this_obj.output_handler.show_results(tests, harness_status);
|
| + var cloned_tests = map(tests, function(test) { return test.structured_clone(); });
|
| + this_obj._dispatch("completion_callback", [tests, harness_status],
|
| + { type: "complete", tests: cloned_tests,
|
| + status: harness_status.structured_clone() });
|
| + });
|
| + };
|
| +
|
| + WindowTestEnvironment.prototype.next_default_test_name = function() {
|
| //Don't use document.title to work around an Opera bug in XHTML documents
|
| var title = document.getElementsByTagName("title")[0];
|
| var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled";
|
| - var suffix = name_counter > 0 ? " " + name_counter : "";
|
| - name_counter++;
|
| + var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
|
| + this.name_counter++;
|
| return prefix + suffix;
|
| + };
|
| +
|
| + WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
|
| + this.output_handler.setup(properties);
|
| + };
|
| +
|
| + WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
|
| + on_event(window, 'load', callback);
|
| + };
|
| +
|
| + WindowTestEnvironment.prototype.test_timeout = function() {
|
| + var metas = document.getElementsByTagName("meta");
|
| + for (var i = 0; i < metas.length; i++) {
|
| + if (metas[i].name == "timeout") {
|
| + if (metas[i].content == "long") {
|
| + return settings.harness_timeout.long;
|
| + }
|
| + break;
|
| + }
|
| + }
|
| + return settings.harness_timeout.normal;
|
| + };
|
| +
|
| + WindowTestEnvironment.prototype.global_scope = function() {
|
| + return window;
|
| + };
|
| +
|
| + /*
|
| + * Base TestEnvironment implementation for a generic web worker.
|
| + *
|
| + * Workers accumulate test results. One or more clients can connect and
|
| + * retrieve results from a worker at any time.
|
| + *
|
| + * WorkerTestEnvironment supports communicating with a client via a
|
| + * MessagePort. The mechanism for determining the appropriate MessagePort
|
| + * for communicating with a client depends on the type of worker and is
|
| + * implemented by the various specializations of WorkerTestEnvironment
|
| + * below.
|
| + *
|
| + * A client document using testharness can use fetch_tests_from_worker() to
|
| + * retrieve results from a worker. See apisample16.html.
|
| + */
|
| + function WorkerTestEnvironment() {
|
| + this.name_counter = 0;
|
| + this.all_loaded = true;
|
| + this.message_list = [];
|
| + this.message_ports = [];
|
| + }
|
| +
|
| + WorkerTestEnvironment.prototype._dispatch = function(message) {
|
| + this.message_list.push(message);
|
| + for (var i = 0; i < this.message_ports.length; ++i)
|
| + {
|
| + this.message_ports[i].postMessage(message);
|
| + }
|
| + };
|
| +
|
| + // The only requirement is that port has a postMessage() method. It doesn't
|
| + // have to be an instance of a MessagePort, and often isn't.
|
| + WorkerTestEnvironment.prototype._add_message_port = function(port) {
|
| + this.message_ports.push(port);
|
| + for (var i = 0; i < this.message_list.length; ++i)
|
| + {
|
| + port.postMessage(this.message_list[i]);
|
| + }
|
| + };
|
| +
|
| + WorkerTestEnvironment.prototype.next_default_test_name = function() {
|
| + var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
|
| + this.name_counter++;
|
| + return "Untitled" + suffix;
|
| + };
|
| +
|
| + WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
|
| +
|
| + WorkerTestEnvironment.prototype.on_tests_ready = function() {
|
| + var this_obj = this;
|
| + add_start_callback(
|
| + function(properties) {
|
| + this_obj._dispatch({
|
| + type: "start",
|
| + properties: properties,
|
| + });
|
| + });
|
| + add_test_state_callback(
|
| + function(test) {
|
| + this_obj._dispatch({
|
| + type: "test_state",
|
| + test: test.structured_clone()
|
| + });
|
| + });
|
| + add_result_callback(
|
| + function(test) {
|
| + this_obj._dispatch({
|
| + type: "result",
|
| + test: test.structured_clone()
|
| + });
|
| + });
|
| + add_completion_callback(
|
| + function(tests, harness_status) {
|
| + this_obj._dispatch({
|
| + type: "complete",
|
| + tests: map(tests,
|
| + function(test) {
|
| + return test.structured_clone();
|
| + }),
|
| + status: harness_status.structured_clone()
|
| + });
|
| + });
|
| + };
|
| +
|
| + WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
|
| +
|
| + WorkerTestEnvironment.prototype.test_timeout = function() {
|
| + // Tests running in a worker don't have a default timeout. I.e. all
|
| + // worker tests behave as if settings.explicit_timeout is true.
|
| + return null;
|
| + };
|
| +
|
| + WorkerTestEnvironment.prototype.global_scope = function() {
|
| + return self;
|
| + };
|
| +
|
| + /*
|
| + * Dedicated web workers.
|
| + * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
|
| + *
|
| + * This class is used as the test_environment when testharness is running
|
| + * inside a dedicated worker.
|
| + */
|
| + function DedicatedWorkerTestEnvironment() {
|
| + WorkerTestEnvironment.call(this);
|
| + // self is an instance of DedicatedWorkerGlobalScope which exposes
|
| + // a postMessage() method for communicating via the message channel
|
| + // established when the worker is created.
|
| + this._add_message_port(self);
|
| }
|
| + DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
|
| +
|
| + DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
|
| + WorkerTestEnvironment.prototype.on_tests_ready.call(this);
|
| + // In the absence of an onload notification, we a require dedicated
|
| + // workers to explicitly signal when the tests are done.
|
| + tests.wait_for_finish = true;
|
| + };
|
| +
|
| + /*
|
| + * Shared web workers.
|
| + * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
|
| + *
|
| + * This class is used as the test_environment when testharness is running
|
| + * inside a shared web worker.
|
| + */
|
| + function SharedWorkerTestEnvironment() {
|
| + WorkerTestEnvironment.call(this);
|
| + var this_obj = this;
|
| + // Shared workers receive message ports via the 'onconnect' event for
|
| + // each connection.
|
| + self.addEventListener("connect",
|
| + function(message_event) {
|
| + this_obj._add_message_port(message_event.source);
|
| + });
|
| + }
|
| + SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
|
| +
|
| + SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
|
| + WorkerTestEnvironment.prototype.on_tests_ready.call(this);
|
| + // In the absence of an onload notification, we a require shared
|
| + // workers to explicitly signal when the tests are done.
|
| + tests.wait_for_finish = true;
|
| + };
|
| +
|
| + /*
|
| + * Service workers.
|
| + * http://www.w3.org/TR/service-workers/
|
| + *
|
| + * This class is used as the test_environment when testharness is running
|
| + * inside a service worker.
|
| + */
|
| + function ServiceWorkerTestEnvironment() {
|
| + WorkerTestEnvironment.call(this);
|
| + this.all_loaded = false;
|
| + this.on_loaded_callback = null;
|
| + var this_obj = this;
|
| + self.addEventListener("message",
|
| + function(event) {
|
| + if (event.data.type && event.data.type === "connect") {
|
| + this_obj._add_message_port(event.ports[0]);
|
| + event.ports[0].start();
|
| + }
|
| + });
|
| +
|
| + // The oninstall event is received after the service worker script and
|
| + // all imported scripts have been fetched and executed. It's the
|
| + // equivalent of an onload event for a document. All tests should have
|
| + // been added by the time this event is received, thus it's not
|
| + // necessary to wait until the onactivate event.
|
| + on_event(self, "install",
|
| + function(event) {
|
| + this_obj.all_loaded = true;
|
| + if (this_obj.on_loaded_callback) {
|
| + this_obj.on_loaded_callback();
|
| + }
|
| + });
|
| + }
|
| + ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
|
| +
|
| + ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
|
| + if (this.all_loaded) {
|
| + callback();
|
| + } else {
|
| + this.on_loaded_callback = callback;
|
| + }
|
| + };
|
| +
|
| + function create_test_environment() {
|
| + if ('document' in self) {
|
| + return new WindowTestEnvironment();
|
| + }
|
| + if ('DedicatedWorkerGlobalScope' in self &&
|
| + self instanceof DedicatedWorkerGlobalScope) {
|
| + return new DedicatedWorkerTestEnvironment();
|
| + }
|
| + if ('SharedWorkerGlobalScope' in self &&
|
| + self instanceof SharedWorkerGlobalScope) {
|
| + return new SharedWorkerTestEnvironment();
|
| + }
|
| + if ('ServiceWorkerGlobalScope' in self &&
|
| + self instanceof ServiceWorkerGlobalScope) {
|
| + return new ServiceWorkerTestEnvironment();
|
| + }
|
| + throw new Error("Unsupported test environment");
|
| + }
|
| +
|
| + var test_environment = create_test_environment();
|
| +
|
| + function is_shared_worker(worker) {
|
| + return 'SharedWorker' in self && worker instanceof SharedWorker;
|
| + }
|
| +
|
| + function is_service_worker(worker) {
|
| + return 'ServiceWorker' in self && worker instanceof ServiceWorker;
|
| + }
|
| +
|
| + /*
|
| + * API functions
|
| + */
|
|
|
| function test(func, name, properties)
|
| {
|
| - var test_name = name ? name : next_default_name();
|
| + var test_name = name ? name : test_environment.next_default_test_name();
|
| properties = properties ? properties : {};
|
| var test_obj = new Test(test_name, properties);
|
| test_obj.step(func, test_obj, test_obj);
|
| @@ -498,7 +447,7 @@ policies and contribution forms [3].
|
| name = func;
|
| func = null;
|
| }
|
| - var test_name = name ? name : next_default_name();
|
| + var test_name = name ? name : test_environment.next_default_test_name();
|
| properties = properties ? properties : {};
|
| var test_obj = new Test(test_name, properties);
|
| if (func) {
|
| @@ -507,6 +456,97 @@ policies and contribution forms [3].
|
| return test_obj;
|
| }
|
|
|
| + function promise_test(func, name, properties) {
|
| + var test = async_test(name, properties);
|
| + Promise.resolve(test.step(func, test, test))
|
| + .then(
|
| + function() {
|
| + test.done();
|
| + })
|
| + .catch(test.step_func(
|
| + function(value) {
|
| + if (value instanceof AssertionError) {
|
| + throw value;
|
| + }
|
| + assert(false, "promise_test", null,
|
| + "Unhandled rejection with value: ${value}", {value:value});
|
| + }));
|
| + }
|
| +
|
| + function promise_rejects(test, expected, promise) {
|
| + return promise.then(test.unreached_func("Should have rejected.")).catch(function(e) {
|
| + assert_throws(expected, function() { throw e });
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * This constructor helper allows DOM events to be handled using Promises,
|
| + * which can make it a lot easier to test a very specific series of events,
|
| + * including ensuring that unexpected events are not fired at any point.
|
| + */
|
| + function EventWatcher(test, watchedNode, eventTypes)
|
| + {
|
| + if (typeof eventTypes == 'string') {
|
| + eventTypes = [eventTypes];
|
| + }
|
| +
|
| + var waitingFor = null;
|
| +
|
| + var eventHandler = test.step_func(function(evt) {
|
| + assert_true(!!waitingFor,
|
| + 'Not expecting event, but got ' + evt.type + ' event');
|
| + assert_equals(evt.type, waitingFor.types[0],
|
| + 'Expected ' + waitingFor.types[0] + ' event, but got ' +
|
| + evt.type + ' event instead');
|
| + if (waitingFor.types.length > 1) {
|
| + // Pop first event from array
|
| + waitingFor.types.shift();
|
| + return;
|
| + }
|
| + // We need to null out waitingFor before calling the resolve function
|
| + // since the Promise's resolve handlers may call wait_for() which will
|
| + // need to set waitingFor.
|
| + var resolveFunc = waitingFor.resolve;
|
| + waitingFor = null;
|
| + resolveFunc(evt);
|
| + });
|
| +
|
| + for (var i = 0; i < eventTypes.length; i++) {
|
| + watchedNode.addEventListener(eventTypes[i], eventHandler);
|
| + }
|
| +
|
| + /**
|
| + * Returns a Promise that will resolve after the specified event or
|
| + * series of events has occured.
|
| + */
|
| + this.wait_for = function(types) {
|
| + if (waitingFor) {
|
| + return Promise.reject('Already waiting for an event or events');
|
| + }
|
| + if (typeof types == 'string') {
|
| + types = [types];
|
| + }
|
| + return new Promise(function(resolve, reject) {
|
| + waitingFor = {
|
| + types: types,
|
| + resolve: resolve,
|
| + reject: reject
|
| + };
|
| + });
|
| + };
|
| +
|
| + function stop_watching() {
|
| + for (var i = 0; i < eventTypes.length; i++) {
|
| + watchedNode.removeEventListener(eventTypes[i], eventHandler);
|
| + }
|
| + };
|
| +
|
| + test.add_cleanup(stop_watching);
|
| +
|
| + return this;
|
| + }
|
| + expose(EventWatcher, 'EventWatcher');
|
| +
|
| function setup(func_or_properties, maybe_properties)
|
| {
|
| var func = null;
|
| @@ -520,7 +560,7 @@ policies and contribution forms [3].
|
| properties = func_or_properties;
|
| }
|
| tests.setup(func, properties);
|
| - output.setup(properties);
|
| + test_environment.on_new_harness_properties(properties);
|
| }
|
|
|
| function done() {
|
| @@ -553,6 +593,8 @@ policies and contribution forms [3].
|
|
|
| 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');
|
| @@ -816,7 +858,7 @@ policies and contribution forms [3].
|
| for (var i = 0; i < actual.length; i++) {
|
| assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
|
| "assert_array_equals", description,
|
| - "property ${i}, property expected to be $expected but was $actual",
|
| + "property ${i}, property expected to be ${expected} but was ${actual}",
|
| {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
|
| actual:actual.hasOwnProperty(i) ? "present" : "missing"});
|
| assert(same_value(expected[i], actual[i]),
|
| @@ -878,6 +920,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)
|
| {
|
| /*
|
| @@ -889,7 +949,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});
|
| }
|
| @@ -912,6 +972,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)
|
| @@ -1063,12 +1141,15 @@ 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,
|
| };
|
|
|
| if (!(name in name_code_map)) {
|
| @@ -1078,7 +1159,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;
|
| }
|
| @@ -1137,26 +1221,22 @@ policies and contribution forms [3].
|
| }
|
| this.name = name;
|
|
|
| - this.phases = {
|
| - INITIAL:0,
|
| - STARTED:1,
|
| - HAS_RESULT:2,
|
| - COMPLETE:3
|
| - };
|
| this.phase = this.phases.INITIAL;
|
|
|
| this.status = this.NOTRUN;
|
| this.timeout_id = null;
|
| + this.index = null;
|
|
|
| this.properties = properties;
|
| var timeout = properties.timeout ? properties.timeout : settings.test_timeout;
|
| - if (timeout != null) {
|
| + if (timeout !== null) {
|
| this.timeout_length = timeout * tests.timeout_multiplier;
|
| } else {
|
| this.timeout_length = null;
|
| }
|
|
|
| this.message = null;
|
| + this.stack = null;
|
|
|
| this.steps = [];
|
|
|
| @@ -1174,6 +1254,13 @@ policies and contribution forms [3].
|
|
|
| Test.prototype = merge({}, Test.statuses);
|
|
|
| + Test.prototype.phases = {
|
| + INITIAL:0,
|
| + STARTED:1,
|
| + HAS_RESULT:2,
|
| + COMPLETE:3
|
| + };
|
| +
|
| Test.prototype.structured_clone = function()
|
| {
|
| if (!this._structured_clone) {
|
| @@ -1181,10 +1268,13 @@ policies and contribution forms [3].
|
| msg = msg ? String(msg) : msg;
|
| this._structured_clone = merge({
|
| name:String(this.name),
|
| - status:this.status,
|
| - message:msg
|
| + properties:merge({}, this.properties),
|
| }, Test.statuses);
|
| }
|
| + this._structured_clone.status = this.status;
|
| + this._structured_clone.message = this.message;
|
| + this._structured_clone.stack = this.stack;
|
| + this._structured_clone.index = this.index;
|
| return this._structured_clone;
|
| };
|
|
|
| @@ -1198,6 +1288,7 @@ policies and contribution forms [3].
|
| this.set_status(this.TIMEOUT, "Test timed out");
|
|
|
| tests.started = true;
|
| + tests.notify_test_state(this);
|
|
|
| if (this.timeout_id === null) {
|
| this.set_timeout();
|
| @@ -1215,15 +1306,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();
|
| }
|
| @@ -1276,7 +1362,7 @@ policies and contribution forms [3].
|
| Test.prototype.force_timeout = function() {
|
| this.set_status(this.TIMEOUT);
|
| this.phase = this.phases.HAS_RESULT;
|
| - }
|
| + };
|
|
|
| Test.prototype.set_timeout = function()
|
| {
|
| @@ -1289,10 +1375,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()
|
| @@ -1313,10 +1400,6 @@ policies and contribution forms [3].
|
| this.set_status(this.PASS, null);
|
| }
|
|
|
| - if (this.status == this.NOTRUN) {
|
| - alert(this.phase);
|
| - }
|
| -
|
| this.phase = this.phases.COMPLETE;
|
|
|
| clearTimeout(this.timeout_id);
|
| @@ -1332,6 +1415,148 @@ policies and contribution forms [3].
|
| };
|
|
|
| /*
|
| + * A RemoteTest object mirrors a Test object on a remote worker. The
|
| + * associated RemoteWorker updates the RemoteTest object in response to
|
| + * received events. In turn, the RemoteTest object replicates these events
|
| + * on the local document. This allows listeners (test result reporting
|
| + * etc..) to transparently handle local and remote events.
|
| + */
|
| + function RemoteTest(clone) {
|
| + var this_obj = this;
|
| + Object.keys(clone).forEach(
|
| + function(key) {
|
| + this_obj[key] = clone[key];
|
| + });
|
| + this.index = null;
|
| + this.phase = this.phases.INITIAL;
|
| + this.update_state_from(clone);
|
| + tests.push(this);
|
| + }
|
| +
|
| + RemoteTest.prototype.structured_clone = function() {
|
| + var clone = {};
|
| + Object.keys(this).forEach(
|
| + function(key) {
|
| + if (typeof(this[key]) === "object") {
|
| + clone[key] = merge({}, this[key]);
|
| + } else {
|
| + clone[key] = this[key];
|
| + }
|
| + });
|
| + clone.phases = merge({}, this.phases);
|
| + return clone;
|
| + };
|
| +
|
| + RemoteTest.prototype.cleanup = function() {};
|
| + RemoteTest.prototype.phases = Test.prototype.phases;
|
| + RemoteTest.prototype.update_state_from = function(clone) {
|
| + this.status = clone.status;
|
| + this.message = clone.message;
|
| + this.stack = clone.stack;
|
| + if (this.phase === this.phases.INITIAL) {
|
| + this.phase = this.phases.STARTED;
|
| + }
|
| + };
|
| + RemoteTest.prototype.done = function() {
|
| + this.phase = this.phases.COMPLETE;
|
| + }
|
| +
|
| + /*
|
| + * A RemoteWorker listens for test events from a worker. These events are
|
| + * then used to construct and maintain RemoteTest objects that mirror the
|
| + * tests running on the remote worker.
|
| + */
|
| + function RemoteWorker(worker) {
|
| + this.running = true;
|
| + this.tests = new Array();
|
| +
|
| + var this_obj = this;
|
| + worker.onerror = function(error) { this_obj.worker_error(error); };
|
| +
|
| + var message_port;
|
| +
|
| + if (is_service_worker(worker)) {
|
| + // The ServiceWorker's implicit MessagePort is currently not
|
| + // reliably accessible from the ServiceWorkerGlobalScope due to
|
| + // Blink setting MessageEvent.source to null for messages sent via
|
| + // ServiceWorker.postMessage(). Until that's resolved, create an
|
| + // explicit MessageChannel and pass one end to the worker.
|
| + var message_channel = new MessageChannel();
|
| + message_port = message_channel.port1;
|
| + message_port.start();
|
| + worker.postMessage({type: "connect"}, [message_channel.port2]);
|
| + } else if (is_shared_worker(worker)) {
|
| + message_port = worker.port;
|
| + } else {
|
| + message_port = worker;
|
| + }
|
| +
|
| + // Keeping a reference to the worker until worker_done() is seen
|
| + // prevents the Worker object and its MessageChannel from going away
|
| + // before all the messages are dispatched.
|
| + this.worker = worker;
|
| +
|
| + message_port.onmessage =
|
| + function(message) {
|
| + if (this_obj.running && (message.data.type in this_obj.message_handlers)) {
|
| + this_obj.message_handlers[message.data.type].call(this_obj, message.data);
|
| + }
|
| + };
|
| + }
|
| +
|
| + RemoteWorker.prototype.worker_error = function(error) {
|
| + var message = error.message || String(error);
|
| + var filename = (error.filename ? " " + error.filename: "");
|
| + // FIXME: Display worker error states separately from main document
|
| + // error state.
|
| + this.worker_done({
|
| + status: {
|
| + status: tests.status.ERROR,
|
| + message: "Error in worker" + filename + ": " + message,
|
| + stack: error.stack
|
| + }
|
| + });
|
| + error.preventDefault();
|
| + };
|
| +
|
| + RemoteWorker.prototype.test_state = function(data) {
|
| + var remote_test = this.tests[data.test.index];
|
| + if (!remote_test) {
|
| + remote_test = new RemoteTest(data.test);
|
| + this.tests[data.test.index] = remote_test;
|
| + }
|
| + remote_test.update_state_from(data.test);
|
| + tests.notify_test_state(remote_test);
|
| + };
|
| +
|
| + RemoteWorker.prototype.test_done = function(data) {
|
| + var remote_test = this.tests[data.test.index];
|
| + remote_test.update_state_from(data.test);
|
| + remote_test.done();
|
| + tests.result(remote_test);
|
| + };
|
| +
|
| + RemoteWorker.prototype.worker_done = function(data) {
|
| + if (tests.status.status === null &&
|
| + data.status.status !== data.status.OK) {
|
| + tests.status.status = data.status.status;
|
| + tests.status.message = data.status.message;
|
| + tests.status.stack = data.status.stack;
|
| + }
|
| + this.running = false;
|
| + this.worker = null;
|
| + if (tests.all_done()) {
|
| + tests.complete();
|
| + }
|
| + };
|
| +
|
| + RemoteWorker.prototype.message_handlers = {
|
| + test_state: RemoteWorker.prototype.test_state,
|
| + result: RemoteWorker.prototype.test_done,
|
| + complete: RemoteWorker.prototype.worker_done
|
| + };
|
| +
|
| + /*
|
| * Harness
|
| */
|
|
|
| @@ -1339,6 +1564,7 @@ policies and contribution forms [3].
|
| {
|
| this.status = null;
|
| this.message = null;
|
| + this.stack = null;
|
| }
|
|
|
| TestsStatus.statuses = {
|
| @@ -1356,7 +1582,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;
|
| @@ -1378,8 +1605,6 @@ policies and contribution forms [3].
|
|
|
| this.properties = {};
|
|
|
| - //All tests can't be done until the load event fires
|
| - this.all_loaded = false;
|
| this.wait_for_finish = false;
|
| this.processing_callbacks = false;
|
|
|
| @@ -1388,26 +1613,25 @@ policies and contribution forms [3].
|
| this.file_is_test = false;
|
|
|
| this.timeout_multiplier = 1;
|
| - this.timeout_length = this.get_timeout();
|
| + this.timeout_length = test_environment.test_timeout();
|
| this.timeout_id = null;
|
|
|
| this.start_callbacks = [];
|
| + this.test_state_callbacks = [];
|
| this.test_done_callbacks = [];
|
| this.all_done_callbacks = [];
|
|
|
| + this.pending_workers = [];
|
| +
|
| this.status = new TestsStatus();
|
|
|
| var this_obj = this;
|
|
|
| - on_event(window, "load",
|
| - function()
|
| - {
|
| - this_obj.all_loaded = true;
|
| - if (this_obj.all_done())
|
| - {
|
| - this_obj.complete();
|
| - }
|
| - });
|
| + test_environment.add_on_loaded_callback(function() {
|
| + if (this_obj.all_done()) {
|
| + this_obj.complete();
|
| + }
|
| + });
|
|
|
| this.set_timeout();
|
| }
|
| @@ -1449,6 +1673,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();
|
| @@ -1464,19 +1689,6 @@ policies and contribution forms [3].
|
| async_test();
|
| };
|
|
|
| - Tests.prototype.get_timeout = function() {
|
| - var metas = document.getElementsByTagName("meta");
|
| - for (var i = 0; i < metas.length; i++) {
|
| - if (metas[i].name == "timeout") {
|
| - if (metas[i].content == "long") {
|
| - return settings.harness_timeout.long;
|
| - }
|
| - break;
|
| - }
|
| - }
|
| - return settings.harness_timeout.normal;
|
| - };
|
| -
|
| Tests.prototype.set_timeout = function() {
|
| var this_obj = this;
|
| clearTimeout(this.timeout_id);
|
| @@ -1488,7 +1700,7 @@ policies and contribution forms [3].
|
| };
|
|
|
| Tests.prototype.timeout = function() {
|
| - if (this.status.status == this.status.OK) {
|
| + if (this.status.status === null) {
|
| this.status.status = this.status.TIMEOUT;
|
| }
|
| this.complete();
|
| @@ -1508,12 +1720,23 @@ policies and contribution forms [3].
|
| this.start();
|
| }
|
| this.num_pending++;
|
| - this.tests.push(test);
|
| + test.index = this.tests.push(test);
|
| + this.notify_test_state(test);
|
| + };
|
| +
|
| + Tests.prototype.notify_test_state = function(test) {
|
| + var this_obj = this;
|
| + forEach(this.test_state_callbacks,
|
| + function(callback) {
|
| + callback(test, this_obj);
|
| + });
|
| };
|
|
|
| Tests.prototype.all_done = function() {
|
| - return (this.tests.length > 0 && this.all_loaded && this.num_pending === 0 &&
|
| - !this.wait_for_finish && !this.processing_callbacks);
|
| + return (this.tests.length > 0 && test_environment.all_loaded &&
|
| + this.num_pending === 0 && !this.wait_for_finish &&
|
| + !this.processing_callbacks &&
|
| + !this.pending_workers.some(function(w) { return w.running; }));
|
| };
|
|
|
| Tests.prototype.start = function() {
|
| @@ -1528,25 +1751,6 @@ policies and contribution forms [3].
|
| {
|
| callback(this_obj.properties);
|
| });
|
| - forEach_windows(
|
| - function(w, is_same_origin)
|
| - {
|
| - if (is_same_origin && w.start_callback) {
|
| - try {
|
| - w.start_callback(this_obj.properties);
|
| - } catch (e) {
|
| - if (debug) {
|
| - throw e;
|
| - }
|
| - }
|
| - }
|
| - if (supports_post_message(w) && w !== self) {
|
| - w.postMessage({
|
| - type: "start",
|
| - properties: this_obj.properties
|
| - }, "*");
|
| - }
|
| - });
|
| };
|
|
|
| Tests.prototype.result = function(test)
|
| @@ -1567,26 +1771,6 @@ policies and contribution forms [3].
|
| {
|
| callback(test, this_obj);
|
| });
|
| -
|
| - forEach_windows(
|
| - function(w, is_same_origin)
|
| - {
|
| - if (is_same_origin && w.result_callback) {
|
| - try {
|
| - w.result_callback(test);
|
| - } catch (e) {
|
| - if (debug) {
|
| - throw e;
|
| - }
|
| - }
|
| - }
|
| - if (supports_post_message(w) && w !== self) {
|
| - w.postMessage({
|
| - type: "result",
|
| - test: test.structured_clone()
|
| - }, "*");
|
| - }
|
| - });
|
| this.processing_callbacks = false;
|
| if (this_obj.all_done()) {
|
| this_obj.complete();
|
| @@ -1602,24 +1786,18 @@ policies and contribution forms [3].
|
| this.tests.forEach(
|
| function(x)
|
| {
|
| - if (x.status === x.NOTRUN) {
|
| + if (x.phase < x.phases.COMPLETE) {
|
| this_obj.notify_result(x);
|
| x.cleanup();
|
| + x.phase = x.phases.COMPLETE;
|
| }
|
| }
|
| );
|
| this.notify_complete();
|
| };
|
|
|
| - Tests.prototype.notify_complete = function()
|
| - {
|
| - clearTimeout(this.timeout_id);
|
| + Tests.prototype.notify_complete = function() {
|
| var this_obj = this;
|
| - var tests = map(this_obj.tests,
|
| - function(test)
|
| - {
|
| - return test.structured_clone();
|
| - });
|
| if (this.status.status === null) {
|
| this.status.status = this.status.OK;
|
| }
|
| @@ -1629,47 +1807,20 @@ policies and contribution forms [3].
|
| {
|
| callback(this_obj.tests, this_obj.status);
|
| });
|
| -
|
| - forEach_windows(
|
| - function(w, is_same_origin)
|
| - {
|
| - if (is_same_origin && w.completion_callback) {
|
| - try {
|
| - w.completion_callback(this_obj.tests, this_obj.status);
|
| - } catch (e) {
|
| - if (debug) {
|
| - throw e;
|
| - }
|
| - }
|
| - }
|
| - if (supports_post_message(w) && w !== self) {
|
| - w.postMessage({
|
| - type: "complete",
|
| - tests: tests,
|
| - status: this_obj.status.structured_clone()
|
| - }, "*");
|
| - }
|
| - });
|
| };
|
|
|
| - var tests = new Tests();
|
| -
|
| - addEventListener("error", function(e) {
|
| - if (tests.file_is_test) {
|
| - var test = tests.tests[0];
|
| - if (test.phase >= test.phases.HAS_RESULT) {
|
| - return;
|
| - }
|
| - var message = e.message;
|
| - test.set_status(test.FAIL, message);
|
| - test.phase = test.phases.HAS_RESULT;
|
| - test.done();
|
| - done();
|
| - } else if (!tests.allow_uncaught_exception) {
|
| - tests.status.status = tests.status.ERROR;
|
| - tests.status.message = e.message;
|
| + Tests.prototype.fetch_tests_from_worker = function(worker) {
|
| + if (this.phase >= this.phases.COMPLETE) {
|
| + return;
|
| }
|
| - });
|
| +
|
| + this.pending_workers.push(new RemoteWorker(worker));
|
| + };
|
| +
|
| + function fetch_tests_from_worker(port) {
|
| + tests.fetch_tests_from_worker(port);
|
| + }
|
| + expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
|
|
|
| function timeout() {
|
| if (tests.timeout_length === null) {
|
| @@ -1682,6 +1833,10 @@ policies and contribution forms [3].
|
| tests.start_callbacks.push(callback);
|
| }
|
|
|
| + function add_test_state_callback(callback) {
|
| + tests.test_state_callbacks.push(callback);
|
| + }
|
| +
|
| function add_result_callback(callback)
|
| {
|
| tests.test_done_callbacks.push(callback);
|
| @@ -1693,6 +1848,7 @@ policies and contribution forms [3].
|
| }
|
|
|
| expose(add_start_callback, 'add_start_callback');
|
| + expose(add_test_state_callback, 'add_test_state_callback');
|
| expose(add_result_callback, 'add_result_callback');
|
| expose(add_completion_callback, 'add_completion_callback');
|
|
|
| @@ -1703,7 +1859,6 @@ policies and contribution forms [3].
|
| function Output() {
|
| this.output_document = document;
|
| this.output_node = null;
|
| - this.done_count = 0;
|
| this.enabled = settings.output;
|
| this.phase = this.INITIAL;
|
| }
|
| @@ -1748,10 +1903,11 @@ policies and contribution forms [3].
|
| }
|
| var node = output_document.getElementById("log");
|
| if (!node) {
|
| - if (!document.body) {
|
| + if (!document.body || document.readyState == "loading") {
|
| return;
|
| }
|
| node = output_document.createElement("div");
|
| + node.id = "log";
|
| output_document.body.appendChild(node);
|
| }
|
| this.output_document = output_document;
|
| @@ -1769,13 +1925,13 @@ policies and contribution forms [3].
|
| this.resolve_log();
|
| this.phase = this.HAVE_RESULTS;
|
| }
|
| - this.done_count++;
|
| + var done_count = tests.tests.length - tests.num_pending;
|
| if (this.output_node) {
|
| - if (this.done_count < 100 ||
|
| - (this.done_count < 1000 && this.done_count % 100 === 0) ||
|
| - this.done_count % 1000 === 0) {
|
| + if (done_count < 100 ||
|
| + (done_count < 1000 && done_count % 100 === 0) ||
|
| + done_count % 1000 === 0) {
|
| this.output_node.textContent = "Running, " +
|
| - this.done_count + " complete, " +
|
| + done_count + " complete, " +
|
| tests.num_pending + " remain";
|
| }
|
| }
|
| @@ -1803,7 +1959,25 @@ policies and contribution forms [3].
|
| log.removeChild(log.lastChild);
|
| }
|
|
|
| - if (script_prefix != null) {
|
| + var script_prefix = null;
|
| + var scripts = document.getElementsByTagName("script");
|
| + for (var i = 0; i < scripts.length; i++) {
|
| + var src;
|
| + if (scripts[i].src) {
|
| + src = scripts[i].src;
|
| + } else if (scripts[i].href) {
|
| + //SVG case
|
| + src = scripts[i].href.baseVal;
|
| + }
|
| +
|
| + var matches = src && src.match(/^(.*\/|)testharness\.js$/);
|
| + if (matches) {
|
| + script_prefix = matches[1];
|
| + break;
|
| + }
|
| + }
|
| +
|
| + if (script_prefix !== null) {
|
| var stylesheet = output_document.createElementNS(xhtml_ns, "link");
|
| stylesheet.setAttribute("rel", "stylesheet");
|
| stylesheet.setAttribute("href", script_prefix + "testharness.css");
|
| @@ -1844,22 +2018,23 @@ policies and contribution forms [3].
|
| ["h2", {}, "Summary"],
|
| function()
|
| {
|
| - if (harness_status.status === harness_status.OK) {
|
| - return null;
|
| - }
|
|
|
| var status = status_text_harness[harness_status.status];
|
| - var rv = [["p", {"class":status_class(status)}]];
|
| + var rv = [["section", {},
|
| + ["p", {},
|
| + "Harness status: ",
|
| + ["span", {"class":status_class(status)},
|
| + status
|
| + ],
|
| + ]
|
| + ]];
|
|
|
| if (harness_status.status === harness_status.ERROR) {
|
| - rv[0].push("Harness encountered an error:");
|
| - rv.push(["pre", {}, harness_status.message]);
|
| - } else if (harness_status.status === harness_status.TIMEOUT) {
|
| - rv[0].push("Harness timed out.");
|
| - } else {
|
| - rv[0].push("Harness got an unexpected status.");
|
| + rv[0].push(["pre", {}, harness_status.message]);
|
| + if (harness_status.stack) {
|
| + rv[0].push(["pre", {}, harness_status.stack]);
|
| + }
|
| }
|
| -
|
| return rv;
|
| },
|
| ["p", {}, "Found ${num_tests} tests"],
|
| @@ -1877,7 +2052,8 @@ policies and contribution forms [3].
|
| i++;
|
| }
|
| return rv;
|
| - }];
|
| + },
|
| + ];
|
|
|
| log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
|
|
|
| @@ -1955,6 +2131,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>";
|
| @@ -1968,11 +2147,6 @@ policies and contribution forms [3].
|
| }
|
| };
|
|
|
| - var output = new Output();
|
| - add_start_callback(function (properties) {output.init(properties);});
|
| - add_result_callback(function () {output.show_status();});
|
| - add_completion_callback(function (tests, harness_status) {output.show_results(tests, harness_status);});
|
| -
|
| /*
|
| * Template code
|
| *
|
| @@ -2121,8 +2295,6 @@ policies and contribution forms [3].
|
| return element;
|
| }
|
|
|
| -
|
| -
|
| function make_dom(template, substitutions, output_document)
|
| {
|
| if (is_single_node(template)) {
|
| @@ -2157,11 +2329,26 @@ 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 lines = new Error().stack.split("\n");
|
| + var rv = [];
|
| + var re = /\/resources\/testharness\.js/;
|
| + var i = 0;
|
| + // Fire remove any preamble that doesn't match the regexp
|
| + while (!re.test(lines[i])) {
|
| + i++
|
| + }
|
| + // Then remove top frames in testharness.js itself
|
| + while (re.test(lines[i])) {
|
| + i++
|
| + }
|
| + return lines.slice(i).join("\n");
|
| + }
|
|
|
| function make_message(function_name, description, error, substitutions)
|
| {
|
| @@ -2232,7 +2419,7 @@ policies and contribution forms [3].
|
| function expose(object, name)
|
| {
|
| var components = name.split(".");
|
| - var target = window;
|
| + var target = test_environment.global_scope();
|
| for (var i = 0; i < components.length - 1; i++) {
|
| if (!(components[i] in target)) {
|
| target[components[i]] = {};
|
| @@ -2242,56 +2429,6 @@ policies and contribution forms [3].
|
| target[components[components.length - 1]] = object;
|
| }
|
|
|
| - function forEach_windows(callback) {
|
| - // Iterate of the the windows [self ... top, opener]. The callback is passed
|
| - // two objects, the first one is the windows object itself, the second one
|
| - // is a boolean indicating whether or not its on the same origin as the
|
| - // current window.
|
| - var cache = forEach_windows.result_cache;
|
| - if (!cache) {
|
| - cache = [[self, true]];
|
| - var w = self;
|
| - var i = 0;
|
| - var so;
|
| - var origins = location.ancestorOrigins;
|
| - while (w != w.parent) {
|
| - w = w.parent;
|
| - // In WebKit, calls to parent windows' properties that aren't on the same
|
| - // origin cause an error message to be displayed in the error console but
|
| - // don't throw an exception. This is a deviation from the current HTML5
|
| - // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504
|
| - // The problem with WebKit's behavior is that it pollutes the error console
|
| - // with error messages that can't be caught.
|
| - //
|
| - // This issue can be mitigated by relying on the (for now) proprietary
|
| - // `location.ancestorOrigins` property which returns an ordered list of
|
| - // the origins of enclosing windows. See:
|
| - // http://trac.webkit.org/changeset/113945.
|
| - if (origins) {
|
| - so = (location.origin == origins[i]);
|
| - } else {
|
| - so = is_same_origin(w);
|
| - }
|
| - cache.push([w, so]);
|
| - i++;
|
| - }
|
| - w = window.opener;
|
| - if (w) {
|
| - // window.opener isn't included in the `location.ancestorOrigins` prop.
|
| - // We'll just have to deal with a simple check and an error msg on WebKit
|
| - // browsers in this case.
|
| - cache.push([w, is_same_origin(w)]);
|
| - }
|
| - forEach_windows.result_cache = cache;
|
| - }
|
| -
|
| - forEach(cache,
|
| - function(a)
|
| - {
|
| - callback.apply(null, a);
|
| - });
|
| - }
|
| -
|
| function is_same_origin(w) {
|
| try {
|
| 'random_prop' in w;
|
| @@ -2337,5 +2474,31 @@ policies and contribution forms [3].
|
| }
|
| return supports;
|
| }
|
| +
|
| + /**
|
| + * Setup globals
|
| + */
|
| +
|
| + var tests = new Tests();
|
| +
|
| + addEventListener("error", function(e) {
|
| + if (tests.file_is_test) {
|
| + var test = tests.tests[0];
|
| + if (test.phase >= test.phases.HAS_RESULT) {
|
| + return;
|
| + }
|
| + test.set_status(test.FAIL, e.message, e.stack);
|
| + test.phase = test.phases.HAS_RESULT;
|
| + test.done();
|
| + done();
|
| + } else if (!tests.allow_uncaught_exception) {
|
| + tests.status.status = tests.status.ERROR;
|
| + tests.status.message = e.message;
|
| + tests.status.stack = e.stack;
|
| + }
|
| + });
|
| +
|
| + test_environment.on_tests_ready();
|
| +
|
| })();
|
| // vim: set expandtab shiftwidth=4 tabstop=4:
|
|
|