Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / LayoutTests / resources / testharness.js
index 0a685ed..c2e6013 100644 (file)
@@ -33,8 +33,8 @@ policies and contribution forms [3].
  * the tests have run.
  *
  * 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
+ *       to create tests after the load event, see "Determining when all tests
+ *       are complete", below
  *
  * == Synchronous Tests ==
  *
@@ -49,8 +49,8 @@ policies and contribution forms [3].
  *
  * The function passed in is run in the test() call.
  *
- * properties is an object that overrides default test properties. The recognised properties
- * are:
+ * properties is an object that overrides default test properties. The
+ * recognised properties are:
  *    timeout - the test timeout in ms
  *
  * e.g.
@@ -58,6 +58,19 @@ policies and contribution forms [3].
  *
  * 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 of the part of the specification 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
@@ -81,6 +94,19 @@ policies and contribution forms [3].
  *
  * 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
@@ -124,12 +150,18 @@ policies and contribution forms [3].
  *           tests to complete (this is different to the per-test timeout
  *           because async tests do not start their timer until .step is called)
  *
- * explicit_done - Wait for an explicit call to done() before declaring all tests
- *                 complete (see below)
+ * explicit_done - Wait for an explicit call to done() before declaring all
+ *                 tests complete (see below)
  *
- * 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
+ * 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).
  *
  * == Determining when all tests are complete ==
  *
@@ -138,10 +170,10 @@ policies and contribution forms [3].
  * 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.
+ * 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 ==
  *
@@ -152,7 +184,7 @@ policies and contribution forms [3].
  * 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)
+ * generate_tests(test_function, parameter_lists, properties)
  *
  * For example:
  *
@@ -169,6 +201,9 @@ policies and contribution forms [3].
  * 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:
@@ -200,7 +235,7 @@ policies and contribution forms [3].
  *
  * 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 browsing context:
+ * certain functions in each ancestor and opener browsing context:
  *
  * start - start_callback
  * result - result_callback
@@ -209,6 +244,22 @@ policies and contribution forms [3].
  * 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)
@@ -236,9 +287,25 @@ policies and contribution forms [3].
  * 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
  *
@@ -254,19 +321,28 @@ policies and contribution forms [3].
  *   assert that property property_name on object is readonly
  *
  * assert_throws(code, func, description)
- *   code - a DOMException/RangeException code as a string, e.g. "HIERARCHY_REQUEST_ERR"
+ *   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 that func throws a DOMException or RangeException (as appropriate)
- *   with the given code.  If an object is passed for code instead of a string,
- *   checks that the thrown exception has a property called "name" that matches
- *   the property of code called "name".  Note, this function will probably be
- *   rewritten sometime to make more sense.
- *
  * 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
@@ -323,7 +399,7 @@ policies and contribution forms [3].
     {
         //Don't use document.title to work around an Opera bug in XHTML documents
         var prefix = document.getElementsByTagName("title").length > 0 ?
-                         document.getElementsByTagName("title")[0].firstChild.data :
+                         document.getElementsByTagName("title")[0].textContent :
                          "Untitled";
         var suffix = name_counter > 0 ? " " + name_counter : "";
         name_counter++;
@@ -341,11 +417,19 @@ policies and contribution forms [3].
         }
     }
 
-    function async_test(name, properties)
+    function async_test(func, name, properties)
     {
+        if (typeof func !== "function") {
+            properties = name;
+            name = func;
+            func = null;
+        }
         var test_name = name ? name : next_default_name();
         properties = properties ? properties : {};
         var test_obj = new Test(test_name, properties);
+        if (func) {
+            test_obj.step(func, test_obj, test_obj);
+        }
         return test_obj;
     }
 
@@ -369,14 +453,16 @@ policies and contribution forms [3].
         tests.end_wait();
     }
 
-    function generate_tests(func, args) {
-        forEach(args, function(x)
+    function generate_tests(func, args, properties) {
+        forEach(args, function(x, i)
                 {
                     var name = x[0];
                     test(function()
                          {
                              func.apply(this, x.slice(1));
-                         }, name);
+                         },
+                         name,
+                         Array.isArray(properties) ? properties[i] : properties);
                 });
     }
 
@@ -404,62 +490,71 @@ policies and contribution forms [3].
         return s;
     }
 
-    function format_string(str) {
-        for (var i = 0; i < 32; i++) {
-            var replace = "\\";
-            switch (i) {
-            case 0: replace += "0"; break;
-            case 1: replace += "x01"; break;
-            case 2: replace += "x02"; break;
-            case 3: replace += "x03"; break;
-            case 4: replace += "x04"; break;
-            case 5: replace += "x05"; break;
-            case 6: replace += "x06"; break;
-            case 7: replace += "x07"; break;
-            case 8: replace += "b"; break;
-            case 9: replace += "t"; break;
-            case 10: replace += "n"; break;
-            case 11: replace += "v"; break;
-            case 12: replace += "f"; break;
-            case 13: replace += "r"; break;
-            case 14: replace += "x0e"; break;
-            case 15: replace += "x0f"; break;
-            case 16: replace += "x10"; break;
-            case 17: replace += "x11"; break;
-            case 18: replace += "x12"; break;
-            case 19: replace += "x13"; break;
-            case 20: replace += "x14"; break;
-            case 21: replace += "x15"; break;
-            case 22: replace += "x16"; break;
-            case 23: replace += "x17"; break;
-            case 24: replace += "x18"; break;
-            case 25: replace += "x19"; break;
-            case 26: replace += "x1a"; break;
-            case 27: replace += "x1b"; break;
-            case 28: replace += "x1c"; break;
-            case 29: replace += "x1d"; break;
-            case 30: replace += "x1e"; break;
-            case 31: replace += "x1f"; break;
-            }
-            str = str.replace(RegExp(String.fromCharCode(i), "g"), replace);
-        }
-        return str.replace(/"/g, '\\"')
-    }
-
     /*
      * Convert a value to a nice, human-readable string
      */
-    function format_value(val)
+    function format_value(val, seen)
     {
+        if (!seen) {
+            seen = [];
+        }
+        if (typeof val === "object" && val !== null)
+        {
+            if (seen.indexOf(val) >= 0)
+            {
+                return "[...]";
+            }
+            seen.push(val);
+        }
         if (Array.isArray(val))
         {
-            return "[" + val.map(format_value).join(", ") + "]";
+            return "[" + val.map(function(x) {return format_value(x, seen)}).join(", ") + "]";
         }
 
         switch (typeof val)
         {
         case "string":
-            return '"' + format_string(val) + '"';
+            val = val.replace("\\", "\\\\");
+            for (var i = 0; i < 32; i++)
+            {
+                var replace = "\\";
+                switch (i) {
+                case 0: replace += "0"; break;
+                case 1: replace += "x01"; break;
+                case 2: replace += "x02"; break;
+                case 3: replace += "x03"; break;
+                case 4: replace += "x04"; break;
+                case 5: replace += "x05"; break;
+                case 6: replace += "x06"; break;
+                case 7: replace += "x07"; break;
+                case 8: replace += "b"; break;
+                case 9: replace += "t"; break;
+                case 10: replace += "n"; break;
+                case 11: replace += "v"; break;
+                case 12: replace += "f"; break;
+                case 13: replace += "r"; break;
+                case 14: replace += "x0e"; break;
+                case 15: replace += "x0f"; break;
+                case 16: replace += "x10"; break;
+                case 17: replace += "x11"; break;
+                case 18: replace += "x12"; break;
+                case 19: replace += "x13"; break;
+                case 20: replace += "x14"; break;
+                case 21: replace += "x15"; break;
+                case 22: replace += "x16"; break;
+                case 23: replace += "x17"; break;
+                case 24: replace += "x18"; break;
+                case 25: replace += "x19"; break;
+                case 26: replace += "x1a"; break;
+                case 27: replace += "x1b"; break;
+                case 28: replace += "x1c"; break;
+                case 29: replace += "x1d"; break;
+                case 30: replace += "x1e"; break;
+                case 31: replace += "x1f"; break;
+                }
+                val = val.replace(RegExp(String.fromCharCode(i), "g"), replace);
+            }
+            return '"' + val.replace(/"/g, '\\"') + '"';
         case "boolean":
         case "undefined":
             return String(val);
@@ -562,6 +657,13 @@ policies and contribution forms [3].
           * Test if two primitives are equal or two objects
           * are the same object
           */
+        if (typeof actual != typeof expected)
+        {
+            assert(false, "assert_equals", description,
+                          "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
+                          {expected:expected, actual:actual});
+            return;
+        }
         assert(same_value(actual, expected), "assert_equals", description,
                                              "expected ${expected} but got ${actual}",
                                              {expected:expected, actual:actual});
@@ -591,7 +693,7 @@ policies and contribution forms [3].
     function assert_object_equals(actual, expected, description)
     {
          //This needs to be improved a great deal
-         function check_equal(expected, actual, stack)
+         function check_equal(actual, expected, stack)
          {
              stack.push(actual);
 
@@ -610,7 +712,7 @@ policies and contribution forms [3].
                  }
                  else
                  {
-                     assert(actual[p] === expected[p], "assert_object_equals", description,
+                     assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
                                                        "property ${p} expected ${expected} got ${actual}",
                                                        {p:p, expected:expected, actual:actual});
                  }
@@ -641,7 +743,7 @@ policies and contribution forms [3].
                    "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(expected[i] === actual[i],
+            assert(same_value(expected[i], actual[i]),
                    "assert_array_equals", description,
                    "property ${i}, expected ${expected} but got ${actual}",
                    {i:i, expected:expected[i], actual:actual[i]});
@@ -666,6 +768,74 @@ policies and contribution forms [3].
     };
     expose(assert_approx_equals, "assert_approx_equals");
 
+    function assert_less_than(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is less than another
+         */
+        assert(typeof actual === "number",
+               "assert_less_than", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual < expected,
+               "assert_less_than", description,
+               "expected a number less than ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    };
+    expose(assert_less_than, "assert_less_than");
+
+    function assert_greater_than(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is greater than another
+         */
+        assert(typeof actual === "number",
+               "assert_greater_than", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual > expected,
+               "assert_greater_than", description,
+               "expected a number greater than ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    };
+    expose(assert_greater_than, "assert_greater_than");
+
+    function assert_less_than_equal(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is less than or equal to another
+         */
+        assert(typeof actual === "number",
+               "assert_less_than_equal", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual <= expected,
+               "assert_less_than", description,
+               "expected a number less than or equal to ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    };
+    expose(assert_less_than_equal, "assert_less_than_equal");
+
+    function assert_greater_than_equal(actual, expected, description)
+    {
+        /*
+         * Test if a primitive number is greater than or equal to another
+         */
+        assert(typeof actual === "number",
+               "assert_greater_than_equal", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(actual >= expected,
+               "assert_greater_than_equal", description,
+               "expected a number greater than or equal to ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    };
+    expose(assert_greater_than_equal, "assert_greater_than_equal");
+
     function assert_regexp_match(actual, expected, description) {
         /*
          * Test if a string (actual) matches a regexp (expected)
@@ -677,6 +847,12 @@ policies and contribution forms [3].
     }
     expose(assert_regexp_match, "assert_regexp_match");
 
+    function assert_class_string(object, class_string, description) {
+        assert_equals({}.toString.call(object), "[object " + class_string + "]",
+                      description);
+    }
+    expose(assert_class_string, "assert_class_string");
+
 
     function _assert_own_property(name) {
         return function(object, property_name, description)
@@ -729,7 +905,7 @@ policies and contribution forms [3].
              //Note that this can have side effects in the case where
              //the property has PutForwards
              object[property_name] = initial_value + "a"; //XXX use some other value here?
-             assert(object[property_name] === initial_value,
+             assert(same_value(object[property_name], initial_value),
                     "assert_readonly", description,
                     "changing property ${p} succeeded",
                     {p:property_name});
@@ -754,6 +930,10 @@ policies and contribution forms [3].
             if (e instanceof AssertionError) {
                 throw(e);
             }
+            if (code === null)
+            {
+                return;
+            }
             if (typeof code === "object")
             {
                 assert(typeof e == "object" && "name" in e && e.name == code.name,
@@ -764,40 +944,78 @@ policies and contribution forms [3].
                                      expected_name:code.name});
                 return;
             }
-            var required_props = {};
-            required_props.code = {
-                INDEX_SIZE_ERR: 1,
-                HIERARCHY_REQUEST_ERR: 3,
-                WRONG_DOCUMENT_ERR: 4,
-                INVALID_CHARACTER_ERR: 5,
-                NO_MODIFICATION_ALLOWED_ERR: 7,
-                NOT_FOUND_ERR: 8,
-                NOT_SUPPORTED_ERR: 9,
-                INVALID_STATE_ERR: 11,
-                SYNTAX_ERR: 12,
-                INVALID_MODIFICATION_ERR: 13,
-                NAMESPACE_ERR: 14,
-                INVALID_ACCESS_ERR: 15,
-                TYPE_MISMATCH_ERR: 17,
-                SECURITY_ERR: 18,
-                NETWORK_ERR: 19,
-                ABORT_ERR: 20,
-                URL_MISMATCH_ERR: 21,
-                QUOTA_EXCEEDED_ERR: 22,
-                TIMEOUT_ERR: 23,
-                INVALID_NODE_TYPE_ERR: 24,
-                DATA_CLONE_ERR: 25,
-            }[code];
-            if (required_props.code === undefined)
+
+            var code_name_map = {
+                INDEX_SIZE_ERR: 'IndexSizeError',
+                HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+                WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+                INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+                NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+                NOT_FOUND_ERR: 'NotFoundError',
+                NOT_SUPPORTED_ERR: 'NotSupportedError',
+                INVALID_STATE_ERR: 'InvalidStateError',
+                SYNTAX_ERR: 'SyntaxError',
+                INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+                NAMESPACE_ERR: 'NamespaceError',
+                INVALID_ACCESS_ERR: 'InvalidAccessError',
+                TYPE_MISMATCH_ERR: 'TypeMismatchError',
+                SECURITY_ERR: 'SecurityError',
+                NETWORK_ERR: 'NetworkError',
+                ABORT_ERR: 'AbortError',
+                URL_MISMATCH_ERR: 'URLMismatchError',
+                QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+                TIMEOUT_ERR: 'TimeoutError',
+                INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+                DATA_CLONE_ERR: 'DataCloneError'
+            };
+
+            var name = code in code_name_map ? code_name_map[code] : code;
+
+            var name_code_map = {
+                IndexSizeError: 1,
+                HierarchyRequestError: 3,
+                WrongDocumentError: 4,
+                InvalidCharacterError: 5,
+                NoModificationAllowedError: 7,
+                NotFoundError: 8,
+                NotSupportedError: 9,
+                InvalidStateError: 11,
+                SyntaxError: 12,
+                InvalidModificationError: 13,
+                NamespaceError: 14,
+                InvalidAccessError: 15,
+                TypeMismatchError: 17,
+                SecurityError: 18,
+                NetworkError: 19,
+                AbortError: 20,
+                URLMismatchError: 21,
+                QuotaExceededError: 22,
+                TimeoutError: 23,
+                InvalidNodeTypeError: 24,
+                DataCloneError: 25,
+
+                UnknownError: 0,
+                ConstraintError: 0,
+                DataError: 0,
+                TransactionInactiveError: 0,
+                ReadOnlyError: 0,
+                VersionError: 0
+            };
+
+            if (!(name in name_code_map))
             {
                 throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
             }
-            required_props[code] = required_props.code;
-            //Uncomment this when the latest version of every browser
-            //actually implements the spec; otherwise it just creates
-            //zillions of failures.  Also do required_props.type.
-            //required_props.name = code;
-            //
+
+            var required_props = { code: name_code_map[name] };
+
+            if (required_props.code === 0
+            || ("name" in e && e.name !== e.name.toUpperCase() && e.name !== "DOMException"))
+            {
+                // New style exception: also test the name property.
+                required_props.name = name;
+            }
+
             //We'd like to test that e instanceof the appropriate interface,
             //but we can't, because we don't know what window it was created
             //in.  It might be an instanceof the appropriate interface on some
@@ -825,6 +1043,27 @@ policies and contribution forms [3].
     }
     expose(assert_unreached, "assert_unreached");
 
+    function assert_any(assert_func, actual, expected_array)
+    {
+        var args = [].slice.call(arguments, 3)
+        var errors = []
+        var passed = false;
+        forEach(expected_array,
+                function(expected)
+                {
+                    try {
+                        assert_func.apply(this, [actual, expected].concat(args))
+                        passed = true;
+                    } catch(e) {
+                        errors.push(e.message);
+                    }
+                });
+        if (!passed) {
+            throw new AssertionError(errors.join("\n\n"));
+        }
+    }
+    expose(assert_any, "assert_any");
+
     function Test(name, properties)
     {
         this.name = name;
@@ -832,6 +1071,7 @@ policies and contribution forms [3].
         this.timeout_id = null;
         this.is_done = false;
 
+        this.properties = properties;
         this.timeout_length = properties.timeout ? properties.timeout : settings.test_timeout;
 
         this.message = null;
@@ -842,13 +1082,29 @@ policies and contribution forms [3].
         tests.push(this);
     }
 
-    Test.prototype = {
+    Test.statuses = {
         PASS:0,
         FAIL:1,
         TIMEOUT:2,
         NOTRUN:3
     };
 
+    Test.prototype = merge({}, Test.statuses);
+
+    Test.prototype.structured_clone = function()
+    {
+        if(!this._structured_clone)
+        {
+            var msg = this.message;
+            msg = msg ? String(msg) : msg;
+            this._structured_clone = merge({
+                name:String(this.name),
+                status:this.status,
+                message:msg
+            }, Test.statuses);
+        }
+        return this._structured_clone;
+    };
 
     Test.prototype.step = function(func, this_obj)
     {
@@ -873,7 +1129,7 @@ policies and contribution forms [3].
 
         try
         {
-            func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+            return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
         }
         catch(e)
         {
@@ -884,7 +1140,7 @@ policies and contribution forms [3].
                 return;
             }
             this.status = this.FAIL;
-            this.message = e.message;
+            this.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
@@ -915,6 +1171,23 @@ policies and contribution forms [3].
         };
     };
 
+    Test.prototype.step_func_done = function(func, this_obj)
+    {
+        var test_this = this;
+
+        if (arguments.length === 1)
+        {
+            this_obj = test_this;
+        }
+
+        return function()
+        {
+            test_this.step.apply(test_this, [func, this_obj].concat(
+                Array.prototype.slice.call(arguments)));
+            test_this.done();
+        };
+    };
+
     Test.prototype.set_timeout = function()
     {
         var this_obj = this;
@@ -956,12 +1229,29 @@ policies and contribution forms [3].
         this.status = null;
         this.message = null;
     }
-    TestsStatus.prototype = {
+
+    TestsStatus.statuses = {
         OK:0,
         ERROR:1,
         TIMEOUT:2
     };
 
+    TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+    TestsStatus.prototype.structured_clone = function()
+    {
+        if(!this._structured_clone)
+        {
+            var msg = this.message;
+            msg = msg ? String(msg) : msg;
+            this._structured_clone = merge({
+                status:this.status,
+                message:msg
+            }, TestsStatus.statuses);
+        }
+        return this._structured_clone;
+    };
+
     function Tests()
     {
         this.tests = [];
@@ -976,6 +1266,8 @@ policies and contribution forms [3].
         };
         this.phase = this.phases.INITIAL;
 
+        this.properties = {};
+
         //All tests can't be done until the load event fires
         this.all_loaded = false;
         this.wait_for_finish = false;
@@ -983,7 +1275,6 @@ policies and contribution forms [3].
 
         this.timeout_length = settings.timeout;
         this.timeout_id = null;
-        this.set_timeout();
 
         this.start_callbacks = [];
         this.test_done_callbacks = [];
@@ -1002,7 +1293,8 @@ policies and contribution forms [3].
                          this_obj.complete();
                      }
                  });
-        this.properties = {};
+
+        this.set_timeout();
     }
 
     Tests.prototype.setup = function(func, properties)
@@ -1027,12 +1319,14 @@ policies and contribution forms [3].
         if (properties.timeout)
         {
             this.timeout_length = properties.timeout;
-            this.set_timeout();
         }
         if (properties.explicit_done)
         {
             this.wait_for_finish = true;
         }
+        if (properties.explicit_timeout) {
+            this.timeout_length = null;
+        }
 
         if (func)
         {
@@ -1045,15 +1339,19 @@ policies and contribution forms [3].
                 this.status.message = e;
             };
         }
+        this.set_timeout();
     };
 
     Tests.prototype.set_timeout = function()
     {
         var this_obj = this;
         clearTimeout(this.timeout_id);
-        this.timeout_id = setTimeout(function() {
-                                         this_obj.timeout();
-                                     }, this.timeout_length);
+        if (this.timeout_length !== null)
+        {
+            this.timeout_id = setTimeout(function() {
+                                             this_obj.timeout();
+                                         }, this.timeout_length);
+        }
     };
 
     Tests.prototype.timeout = function() {
@@ -1072,7 +1370,7 @@ policies and contribution forms [3].
     Tests.prototype.push = function(test)
     {
         if (this.phase < this.phases.HAVE_TESTS) {
-            this.notify_start();
+            this.start();
         }
         this.num_pending++;
         this.tests.push(test);
@@ -1095,10 +1393,10 @@ policies and contribution forms [3].
                  {
                      callback(this_obj.properties);
                  });
-        forEach(ancestor_windows(),
-                function(w)
+        forEach_windows(
+                function(w, is_same_origin)
                 {
-                    if(w.start_callback)
+                    if(is_same_origin && w.start_callback)
                     {
                         try
                         {
@@ -1112,6 +1410,13 @@ policies and contribution forms [3].
                             }
                         }
                     }
+                    if (supports_post_message(w) && w !== self)
+                    {
+                        w.postMessage({
+                            type: "start",
+                            properties: this_obj.properties
+                        }, "*");
+                    }
                 });
     };
 
@@ -1135,10 +1440,10 @@ policies and contribution forms [3].
                     callback(test, this_obj);
                 });
 
-        forEach(ancestor_windows(),
-                function(w)
+        forEach_windows(
+                function(w, is_same_origin)
                 {
-                    if(w.result_callback)
+                    if(is_same_origin && w.result_callback)
                     {
                         try
                         {
@@ -1151,6 +1456,13 @@ policies and contribution forms [3].
                             }
                         }
                     }
+                    if (supports_post_message(w) && w !== self)
+                    {
+                        w.postMessage({
+                            type: "result",
+                            test: test.structured_clone()
+                        }, "*");
+                    }
                 });
         this.processing_callbacks = false;
         if (this_obj.all_done())
@@ -1164,6 +1476,16 @@ policies and contribution forms [3].
             return;
         }
         this.phase = this.phases.COMPLETE;
+        var this_obj = this;
+        this.tests.forEach(
+            function(x)
+            {
+                if(x.status === x.NOTRUN)
+                {
+                    this_obj.notify_result(x);
+                }
+            }
+        );
         this.notify_complete();
     };
 
@@ -1171,6 +1493,11 @@ policies and contribution forms [3].
     {
         clearTimeout(this.timeout_id);
         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;
@@ -1182,10 +1509,10 @@ policies and contribution forms [3].
                      callback(this_obj.tests, this_obj.status);
                  });
 
-        forEach(ancestor_windows(),
-                function(w)
+        forEach_windows(
+                function(w, is_same_origin)
                 {
-                    if(w.completion_callback)
+                    if(is_same_origin && w.completion_callback)
                     {
                         try
                         {
@@ -1199,11 +1526,27 @@ policies and contribution forms [3].
                             }
                         }
                     }
+                    if (supports_post_message(w) && w !== self)
+                    {
+                        w.postMessage({
+                            type: "complete",
+                            tests: tests,
+                            status: this_obj.status.structured_clone()
+                        }, "*");
+                    }
                 });
     };
 
     var tests = new Tests();
 
+    function timeout() {
+        if (tests.timeout_length === null)
+        {
+            tests.timeout();
+        }
+    }
+    expose(timeout, 'timeout');
+
     function add_start_callback(callback) {
         tests.start_callbacks.push(callback);
     }
@@ -1227,7 +1570,7 @@ policies and contribution forms [3].
     */
 
     function Output() {
-      this.output_document = null;
+      this.output_document = document;
       this.output_node = null;
       this.done_count = 0;
       this.enabled = settings.output;
@@ -1265,11 +1608,22 @@ policies and contribution forms [3].
 
     Output.prototype.resolve_log = function()
     {
-        if (!this.output_document) {
+        var output_document;
+        if (typeof this.output_document === "function")
+        {
+            output_document = this.output_document.apply(undefined);
+        } else
+        {
+            output_document = this.output_document;
+        }
+        if (!output_document)
+        {
             return;
         }
-        var node = this.output_document.getElementById("log");
-        if (node) {
+        var node = output_document.getElementById("log");
+        if (node)
+        {
+            this.output_document = output_document;
             this.output_node = node;
         }
     };
@@ -1398,7 +1752,7 @@ policies and contribution forms [3].
                                  if (!style_element && !input_element.checked) {
                                      style_element = output_document.createElementNS(xhtml_ns, "style");
                                      style_element.id = "hide-" + result_class;
-                                     style_element.innerHTML = "table#results > tbody > tr."+result_class+"{display:none}";
+                                     style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}";
                                      output_document.body.appendChild(style_element);
                                  } else if (style_element && input_element.checked) {
                                      style_element.parentNode.removeChild(style_element);
@@ -1418,9 +1772,33 @@ policies and contribution forms [3].
                 .replace(/'/g, "&#39;");
         }
 
-        log.appendChild(document.createElement("section"));
-        var html = "<h2>Details</h2><table id='results'>"
-            + "<thead><tr><th>Result</th><th>Test Name</th><th>Message</th></tr></thead>"
+        function has_assertions()
+        {
+            for (var i = 0; i < tests.length; i++) {
+                if (tests[i].properties.hasOwnProperty("assert")) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function get_assertion(test)
+        {
+            if (test.properties.hasOwnProperty("assert")) {
+                if (Array.isArray(test.properties.assert)) {
+                    return test.properties.assert.join(' ');
+                }
+                return test.properties.assert;
+            }
+            return '';
+        }
+
+        log.appendChild(document.createElementNS(xhtml_ns, "section"));
+        var assertions = has_assertions();
+        var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">"
+            + "<thead><tr><th>Result</th><th>Test Name</th>"
+            + (assertions ? "<th>Assertion</th>" : "")
+            + "<th>Message</th></tr></thead>"
             + "<tbody>";
         for (var i = 0; i < tests.length; i++) {
             html += '<tr class="'
@@ -1428,12 +1806,21 @@ policies and contribution forms [3].
                 + '"><td>'
                 + escape_html(status_text[tests[i].status])
                 + "</td><td>"
-                + escape_html(format_string(tests[i].name))
+                + escape_html(tests[i].name)
                 + "</td><td>"
-                + escape_html(tests[i].message ? format_string(tests[i].message) : " ")
+                + (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "")
+                + escape_html(tests[i].message ? tests[i].message : " ")
                 + "</td></tr>";
         }
-        log.lastChild.innerHTML = html + "</tbody></table>";
+        html += "</tbody></table>";
+        try {
+            log.lastChild.innerHTML = html;
+        } catch (e) {
+            log.appendChild(document.createElementNS(xhtml_ns, "p"))
+               .textContent = "Setting innerHTML for the log threw an exception.";
+            log.appendChild(document.createElementNS(xhtml_ns, "pre"))
+               .textContent = html;
+        }
     };
 
     var output = new Output();
@@ -1634,7 +2021,6 @@ policies and contribution forms [3].
     {
         return make_dom(substitute(template, substitutions), output_document);
     }
-    expose(render, "template.render");
 
     /*
      * Utility funcions
@@ -1741,22 +2127,107 @@ policies and contribution forms [3].
         target[components[components.length - 1]] = object;
     }
 
- function ancestor_windows() {
-     //Get the windows [self ... top] as an array
-     if ("result_cache" in ancestor_windows)
-     {
-         return ancestor_windows.result_cache;
-     }
-     var rv = [self];
-     var w = self;
-     while (w != w.parent)
-     {
-         w = w.parent;
-         rv.push(w);
-     }
-     ancestor_windows.result_cache = rv;
-     return rv;
- }
+    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;
+            return true;
+        } catch(e) {
+            return false;
+        }
+    }
+
+    function supports_post_message(w)
+    {
+        var supports;
+        var type;
+        // Given IE  implements postMessage across nested iframes but not across
+        // windows or tabs, you can't infer cross-origin communication from the presence
+        // of postMessage on the current window object only.
+        //
+        // Touching the postMessage prop on a window can throw if the window is
+        // not from the same origin AND post message is not supported in that
+        // browser. So just doing an existence test here won't do, you also need
+        // to wrap it in a try..cacth block.
+        try
+        {
+            type = typeof w.postMessage;
+            if (type === "function")
+            {
+                supports = true;
+            }
+            // IE8 supports postMessage, but implements it as a host object which
+            // returns "object" as its `typeof`.
+            else if (type === "object")
+            {
+                supports = true;
+            }
+            // This is the case where postMessage isn't supported AND accessing a
+            // window property across origins does NOT throw (e.g. old Safari browser).
+            else
+            {
+                supports = false;
+            }
+        }
+        catch(e) {
+            // This is the case where postMessage isn't supported AND accessing a
+            // window property across origins throws (e.g. old Firefox browser).
+            supports = false;
+        }
+        return supports;
+    }
 })();
-// vim: set expandtab shiftwidth=4 tabstop=4:
\ No newline at end of file
+// vim: set expandtab shiftwidth=4 tabstop=4: