/* Distributed under both the W3C Test Suite License [1] and the W3C 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the policies and contribution forms [3]. [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license [3] http://www.w3.org/2004/10/27-testcases */ /* * == Introducion == * 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 into the test document: * * * 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 with an element of id "log" * this will be populated with a table containing the test results after all * the tests have run. * * == Synchronous Tests == * * To create a sunchronous test use the test() function: * * test(test_function, name) * * 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. * * == 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: * * 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(); * * == 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 */ (function () { var debug = false; // default timeout is 5 seconds, test can override if needed var default_timeout = 5000; // tests either pass, fail or timeout var status = { PASS: 0, FAIL: 1, TIMEOUT: 2 }; expose(status, 'status'); /* * API functions */ var name_counter = 0; function next_default_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 : "Untitled"; var suffix = name_counter > 0 ? " " + name_counter : ""; name_counter++; return prefix + suffix; } function test(func, name, properties) { var test_name = name ? name : next_default_name(); properties = properties ? properties : {}; var test_obj = new Test(test_name, properties); test_obj.step(func); if (test_obj.status === null) { test_obj.done(); } } function async_test(name, properties) { var test_name = name ? name : next_default_name(); properties = properties ? properties : {}; var test_obj = new Test(test_name, properties); return test_obj; } function on_event(object, event, callback) { object.addEventListener(event, callback, false); } expose(test, 'test'); expose(async_test, 'async_test'); expose(on_event, 'on_event'); /* * Assertions */ function assert_true(actual, description) { var message = make_message("assert_true", description, "expected true got ${actual}", {actual:actual}); assert(actual === true, message); }; expose(assert_true, "assert_true"); function assert_false(actual, description) { var message = make_message("assert_false", description, "expected false got ${actual}", {actual:actual}); assert(actual === false, message); }; expose(assert_false, "assert_false"); function assert_equals(actual, expected, description) { /* * Test if two primitives are equal or two objects * are the same object */ var message = make_message("assert_equals", description, [["{text}", "expected "], ["span", {"class":"expected"}, String(expected)], ["{text}", "got "], ["span", {"class":"actual"}, String(actual)]]); if (expected !== expected) { //NaN case assert(actual !== actual, message); } else { //typical case assert(actual === expected, message); } }; expose(assert_equals, "assert_equals"); function assert_object_equals(actual, expected, description) { //This needs to be improved a great deal function check_equal(expected, actual, stack) { stack.push(actual); for (p in actual) { var message = make_message( "assert_object_equals", description, "unexpected property ${p}", {p:p}); assert(expected.hasOwnProperty(p), message); if (typeof actual[p] === "object" && actual[p] !== null) { if (stack.indexOf(actual[p]) === -1) { check_equal(actual[p], expected[p], stack); } } else { message = make_message( "assert_object_equals", description, "property ${p} expected ${expected} got ${actual}", {p:p, expected:expected, actual:actual}); assert(actual[p] === expected[p], message); } } for (p in expected) { var message = make_message( "assert_object_equals", description, "expected property ${p} missing", {p:p}); assert(actual.hasOwnProperty(p), message); } stack.pop(); } check_equal(actual, expected, []); }; expose(assert_object_equals, "assert_object_equals"); function assert_array_equals(actual, expected, description) { var message = make_message( "assert_array_equals", description, "lengths differ, expected ${expected} got ${actual}", {expected:expected.length, actual:actual.length}); assert(actual.length === expected.length, message); for (var i=0; i < actual.length; i++) { message = make_message( "assert_array_equals", description, "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(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), message); message = make_message( "assert_array_equals", description, "property ${i}, expected ${expected} but got ${actual}", {i:i, expected:expected[i], actual:actual[i]}); assert(expected[i] === actual[i], message); } } expose(assert_array_equals, "assert_array_equals"); function assert_exists(object, property_name, description) { var message = make_message( "assert_exists", description, "expected property ${p} missing", {p:property_name}); assert(object.hasOwnProperty(property_name), message); }; expose(assert_exists, "assert_exists"); function assert_not_exists(object, property_name, description) { var message = make_message( "assert_not_exists", description, "unexpected property ${p} found", {p:property_name}); assert(!object.hasOwnProperty(property_name), message); }; expose(assert_not_exists, "assert_not_exists"); function assert_readonly(object, property_name, description) { var initial_value = object[property_name]; try { var message = make_message( "assert_readonly", description, "deleting property ${p} succeeded", {p:property_name}); assert(delete object[property_name] === false, message); assert(object[property_name] === initial_value, message); //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? message = make_message("assert_readonly", description, "changing property ${p} succeeded", {p:property_name}); assert(object[property_name] === initial_value, message); } finally { object[property_name] = initial_value; } }; expose(assert_readonly, "assert_readonly"); function assert_throws(code_or_object, func, description) { try { func.call(this); assert(false, make_message("assert_throws", description, "${func} did not throw", {func:String(func)})); } catch(e) { if (e instanceof AssertionError) { throw(e); } if (typeof code_or_object === "string") { assert(e[code_or_object] !== undefined && e.code === e[code_or_object] && e.name === code_or_object, make_message("assert_throws", description, [["{text}", "${func} threw with"] , function() { var actual_name; for (var p in DOMException) { if (e.code === DOMException[p]) { actual_name = p; break; } } if (actual_name) { return ["{text}", " code " + actual_name + " (${actual_number})"]; } else { return ["{text}", " error number ${actual_number}"]; } }, ["{text}"," expected ${expected}"], function() { return e[code_or_object] ? ["{text}", " (${expected_number})"] : null; } ], {func:String(func), actual_number:e.code, expected:String(code_or_object), expected_number:e[code_or_object]})); assert(e instanceof DOMException, make_message("assert_throws", description, "thrown exception ${exception} was not a DOMException", {exception:String(e)})); } else { assert(e instanceof Object && "name" in e && e.name == code_or_object.name, make_message("assert_throws", description, "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", {func:String(func), actual:String(e), actual_name:e.name, expected:String(code_or_object), expected_name:code_or_object.name})); } } } expose(assert_throws, "assert_throws"); function assert_unreached(description) { var message = make_message("assert_unreached", description, "Reached unreachable code"); assert(false, message); } expose(assert_unreached, "assert_unreached"); function Test(name, properties) { this.name = name; this.status = null; var timeout = default_timeout; this.is_done = false; if (properties.timeout) { timeout = properties.timeout; } this.message = null; var this_obj = this; this.steps = []; this.timeout_id = setTimeout(function() { this_obj.timeout(); }, timeout); tests.push(this); } Test.prototype.step = function(func, this_obj) { //In case the test has already failed if (this.status !== null) { return; } this.steps.push(func); try { func.apply(this_obj); } catch(e) { //This can happen if something called synchronously invoked another //step if (this.status !== null) { return; } this.status = status.FAIL; this.message = e.message; this.done(); if (debug) { throw e; } } }; Test.prototype.timeout = function() { this.status = status.TIMEOUT; this.timeout_id = null; this.message = "Test timed out"; this.done(); }; Test.prototype.done = function() { if (this.is_done) { //Using alert here is bad return; } clearTimeout(this.timeout_id); if (this.status == null) { this.status = status.PASS; } this.is_done = true; tests.done(this); }; /* * Harness */ var tests = new Tests(); function Tests() { this.tests = []; this.num_pending = 0; this.started = false; this.start_callbacks = []; this.test_done_callbacks = []; this.all_done_callbacks = []; var this_obj = this; //All tests can't be done until the load event fires this.all_loaded = false; on_event(window, "load", function() { this_obj.all_loaded = true; if (document.getElementById("log")) { add_completion_callback(output_results); } if (this_obj.all_done()) { this_obj.notify_results(); } }); } Tests.prototype.push = function(test) { if (!this.started) { this.start(); } this.num_pending++; this.tests.push(test); }; Tests.prototype.all_done = function() { return this.all_loaded && this.num_pending == 0; }; Tests.prototype.done = function(test) { this.num_pending--; var this_obj = this; forEach(this.test_done_callbacks, function(callback) { callback(test, this_obj); }); if(top !== window && top.result_callback) { top.result_callback.call(test, this_obj); } if (this.all_done()) { this.notify_results(); } }; Tests.prototype.start = function() { this.started = true; var this_obj = this; forEach (this.start_callbacks, function(callback) { callback(this_obj); }); if(top !== window && top.start_callback) { top.start_callback.call(this_obj); } }; Tests.prototype.notify_results = function() { var this_obj = this; forEach (this.all_done_callbacks, function(callback) { callback(this_obj.tests); }); if(top !== window && top.completion_callback) { top.completion_callback.call(this_obj, this_obj.tests); } }; function add_start_callback(callback) { tests.start_callbacks.push(callback); } function add_result_callback(callback) { tests.test_done_callbacks.push(callback); } function add_completion_callback(callback) { tests.all_done_callbacks.push(callback); } expose(add_start_callback, 'add_start_callback'); expose(add_result_callback, 'add_result_callback'); expose(add_completion_callback, 'add_completion_callback'); /* * Output listener */ (function show_status() { var done_count = 0; function on_done(test, tests) { var log = document.getElementById("log"); done_count++; if (log) { if (log.lastChild) { log.removeChild(log.lastChild); } var nodes = render([["{text}", "Running, ${done} complete"], function() { if (tests.all_done) { return ["{text}", " ${pending} remain"]; } else { return null; } } ], {done:done_count, pending:tests.num_pending}); forEach(nodes, function(node) { log.appendChild(node); }); log.normalize(); } } if (document.getElementById("log")) { add_result_callback(on_done); } })(); function output_results(tests) { var log = document.getElementById("log"); while (log.lastChild) { log.removeChild(log.lastChild); } var prefix = null; var scripts = document.getElementsByTagName("script"); for (var i=0; i