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: |