* 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 ==
*
*
* 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.
*
* 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
*
* 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
* 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 ==
*
* 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 ==
*
* 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:
*
* 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:
*
* 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
* 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)
* 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 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
{
//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++;
}
}
- 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;
}
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);
});
}
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);
* 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});
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);
}
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});
}
"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]});
};
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)
}
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)
//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});
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,
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
}
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;
this.timeout_id = null;
this.is_done = false;
+ this.properties = properties;
this.timeout_length = properties.timeout ? properties.timeout : settings.test_timeout;
this.message = null;
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)
{
try
{
- func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+ return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
}
catch(e)
{
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
};
};
+ 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;
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 = [];
};
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;
this.timeout_length = settings.timeout;
this.timeout_id = null;
- this.set_timeout();
this.start_callbacks = [];
this.test_done_callbacks = [];
this_obj.complete();
}
});
- this.properties = {};
+
+ this.set_timeout();
}
Tests.prototype.setup = function(func, properties)
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)
{
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() {
Tests.prototype.push = function(test)
{
if (this.phase < this.phases.HAVE_TESTS) {
- this.notify_start();
+ this.start();
}
this.num_pending++;
this.tests.push(test);
{
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
{
}
}
}
+ if (supports_post_message(w) && w !== self)
+ {
+ w.postMessage({
+ type: "start",
+ properties: this_obj.properties
+ }, "*");
+ }
});
};
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
{
}
}
}
+ if (supports_post_message(w) && w !== self)
+ {
+ w.postMessage({
+ type: "result",
+ test: test.structured_clone()
+ }, "*");
+ }
});
this.processing_callbacks = false;
if (this_obj.all_done())
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();
};
{
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;
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
{
}
}
}
+ 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);
}
*/
function Output() {
- this.output_document = null;
+ this.output_document = document;
this.output_node = null;
this.done_count = 0;
this.enabled = settings.output;
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;
}
};
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);
.replace(/'/g, "'");
}
- 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="'
+ '"><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();
{
return make_dom(substitute(template, substitutions), output_document);
}
- expose(render, "template.render");
/*
* Utility funcions
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: