1490 lines
49 KiB
JavaScript
1490 lines
49 KiB
JavaScript
|
/* vim: set expandtab shiftwidth=4 tabstop=4: */
|
||
|
/*global require, phantom, setTimeout, clearTimeout */
|
||
|
|
||
|
/*
|
||
|
This file is part of the PhantomJS project from Ofi Labs.
|
||
|
|
||
|
Copyright 2015 Zachary Weinberg <zackw@panix.com>
|
||
|
|
||
|
Based on testharness.js <https://github.com/w3c/testharness.js>
|
||
|
produced by the W3C and distributed under the W3C 3-Clause BSD
|
||
|
License <http://www.w3.org/Consortium/Legal/2008/03-bsd-license>.
|
||
|
|
||
|
Redistribution and use in source and binary forms, with or without
|
||
|
modification, are permitted provided that the following conditions are met:
|
||
|
|
||
|
* Redistributions of source code must retain the above copyright
|
||
|
notice, this list of conditions and the following disclaimer.
|
||
|
* Redistributions in binary form must reproduce the above copyright
|
||
|
notice, this list of conditions and the following disclaimer in the
|
||
|
documentation and/or other materials provided with the distribution.
|
||
|
* Neither the name of the <organization> nor the
|
||
|
names of its contributors may be used to endorse or promote products
|
||
|
derived from this software without specific prior written permission.
|
||
|
|
||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||
|
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||
|
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
|
||
|
*/
|
||
|
|
||
|
(function () {
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
/** Public API: Defining and running tests.
|
||
|
*
|
||
|
* Compared to the W3C testharness.js, a number of minor changes have
|
||
|
* been made to fit the rather different PhantomJS controller
|
||
|
* environment, but the basic API is the same.
|
||
|
*/
|
||
|
|
||
|
/** Define an asynchronous test. Returns a Test object. In response
|
||
|
to appropriate events, call the Test object's step() method,
|
||
|
passing a function containing assertions (as for synch tests).
|
||
|
Eventually, call the Test object's done() method (inside a step).
|
||
|
|
||
|
All arguments are optional. If |func| is provided, it is called
|
||
|
immediately (after the previous test completes) as the first step
|
||
|
function. |properties| is the same as for test().
|
||
|
|
||
|
Test has several convenience methods for creating step functions,
|
||
|
see below. */
|
||
|
function async_test(func, name, properties) {
|
||
|
var test_obj;
|
||
|
|
||
|
if (func && typeof func !== "function") {
|
||
|
properties = name;
|
||
|
name = func;
|
||
|
func = null;
|
||
|
}
|
||
|
test_obj = new Test(test_name(func, name), properties);
|
||
|
if (func) {
|
||
|
test_obj.step(func);
|
||
|
}
|
||
|
return test_obj;
|
||
|
}
|
||
|
expose(async_test, 'async_test');
|
||
|
|
||
|
/** Define a synchronous test, which is just an asynchronous test that
|
||
|
calls done() immediately after its first step function (func) returns.
|
||
|
|
||
|
|func| is a function containing the test code, and |name|
|
||
|
(optionally) is a descriptive name for the test. |func| will be
|
||
|
called (after the previous test completes), with |this| a Test
|
||
|
object (see below), and should make use of the global assert_*
|
||
|
functions. |properties| is an optional dictionary of test
|
||
|
properties; currently only one is defined:
|
||
|
|
||
|
timeout - Timeout for this test, in milliseconds. */
|
||
|
function test(func, name, properties) {
|
||
|
return async_test(function sync_step () {
|
||
|
func.call(this);
|
||
|
this.done();
|
||
|
}, name, properties);
|
||
|
}
|
||
|
expose(test, 'test');
|
||
|
|
||
|
/** Define a series of synchronous tests all at once.
|
||
|
Easiest to explain by example:
|
||
|
|
||
|
generate_tests(assert_equals, [
|
||
|
["Sum one and one", 1+1, 2],
|
||
|
["Sum one and zero", 1+0, 1]
|
||
|
]);
|
||
|
|
||
|
is equivalent to
|
||
|
|
||
|
test(function() {assert_equals(1+1, 2)}, "Sum one and one");
|
||
|
test(function() {assert_equals(1+0, 1)}, "Sum one and zero");
|
||
|
|
||
|
The first argument can be an arbitrary function, and you can provide
|
||
|
as many arguments in each test vector entry as you like.
|
||
|
|
||
|
The properties argument can be a single dictionary, which is applied to
|
||
|
all the tests, or an array, applied entry-by-entry.
|
||
|
*/
|
||
|
function generate_tests(func, args, properties) {
|
||
|
function generate_one_test(argv) {
|
||
|
return function generated_step () {
|
||
|
// 'this' will be set by bind() inside test().
|
||
|
func.apply(this, argv);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
var i;
|
||
|
for (i = 0; i < args.length; i++) {
|
||
|
test(generate_one_test(args[i].slice(1)),
|
||
|
args[i][0],
|
||
|
Array.isArray(properties) ? properties[i] : properties);
|
||
|
}
|
||
|
}
|
||
|
expose(generate_tests, 'generate_tests');
|
||
|
|
||
|
/** Set up the test harness. Does nothing if called after any test has
|
||
|
begun execution. May be called as setup(func), setup(properties), or
|
||
|
setup(func, properties). |func| is a function to call synchronously;
|
||
|
if it throws an exception the entire test group is considered failed.
|
||
|
|properties| is a dictionary containing one or more of these keys:
|
||
|
|
||
|
explicit_done - Wait for an explicit call to done() before
|
||
|
declaring all tests complete (see below; implicitly true for
|
||
|
single-test files)
|
||
|
|
||
|
allow_uncaught_exception - Don't treat an uncaught exception from
|
||
|
non-test code as an error. (Exceptions thrown out of test
|
||
|
functions are still errors.)
|
||
|
|
||
|
timeout - Global timeout in milliseconds (default: 5 seconds)
|
||
|
test_timeout - Per-test timeout in milliseconds, unless overridden
|
||
|
by the timeout property on a specific test (default: none)
|
||
|
*/
|
||
|
function setup(func_or_properties, maybe_properties) {
|
||
|
var func = null,
|
||
|
properties = {};
|
||
|
|
||
|
if (arguments.length === 2) {
|
||
|
func = func_or_properties;
|
||
|
properties = maybe_properties;
|
||
|
} else if (typeof func_or_properties === "function") {
|
||
|
func = func_or_properties;
|
||
|
} else {
|
||
|
properties = func_or_properties;
|
||
|
}
|
||
|
tests.setup(func, properties);
|
||
|
}
|
||
|
expose(setup, 'setup');
|
||
|
|
||
|
/** Signal that all tests are complete. Must be called explicitly if
|
||
|
setup({explicit_done: true}) was used; otherwise implicitly
|
||
|
happens when all individual tests are done. */
|
||
|
function done() {
|
||
|
tests.end_wait();
|
||
|
}
|
||
|
expose(done, 'done');
|
||
|
|
||
|
|
||
|
/** Public API: Assertions.
|
||
|
* All assertion functions take a |description| argument which is used to
|
||
|
* annotate any failing tests.
|
||
|
*/
|
||
|
|
||
|
/** Assert that |actual| is strictly true. */
|
||
|
function assert_is_true(actual, description) {
|
||
|
assert(actual === true, "assert_is_true", description,
|
||
|
"expected true got ${actual}", {actual: actual});
|
||
|
}
|
||
|
expose(assert_is_true, 'assert_is_true');
|
||
|
|
||
|
/** Assert that |actual| is strictly false. */
|
||
|
function assert_is_false(actual, description) {
|
||
|
assert(actual === false, "assert_is_false", description,
|
||
|
"expected false got ${actual}", {actual: actual});
|
||
|
}
|
||
|
expose(assert_is_false, 'assert_is_false');
|
||
|
|
||
|
/** Assert that |actual| is strictly equal to |expected|.
|
||
|
The test is even more stringent than === (see same_value, below). */
|
||
|
function assert_equals(actual, expected, description) {
|
||
|
if (typeof actual !== typeof expected) {
|
||
|
assert(false, "assert_equals", description,
|
||
|
"expected (${expectedT}) ${expected} " +
|
||
|
"but got (${actualT}) ${actual}",
|
||
|
{expectedT: typeof expected,
|
||
|
expected: expected,
|
||
|
actualT: typeof actual,
|
||
|
actual: actual});
|
||
|
}
|
||
|
assert(same_value(actual, expected), "assert_equals", description,
|
||
|
"expected ${expected} but got ${actual}",
|
||
|
{expected: expected, actual: actual});
|
||
|
}
|
||
|
expose(assert_equals, 'assert_equals');
|
||
|
|
||
|
/** Assert that |actual| is not strictly equal to |expected|, using the
|
||
|
same extra-stringent criterion as for assert_equals. */
|
||
|
function assert_not_equals(actual, expected, description) {
|
||
|
if (typeof actual !== typeof expected) {
|
||
|
return;
|
||
|
}
|
||
|
assert(!same_value(actual, expected), "assert_not_equals", description,
|
||
|
"got disallowed value ${actual}",
|
||
|
{actual: actual});
|
||
|
}
|
||
|
expose(assert_not_equals, 'assert_not_equals');
|
||
|
|
||
|
/** Assert that |expected|, a duck-typed array, contains |actual|,
|
||
|
according to indexOf. */
|
||
|
function assert_in_array(actual, expected, description) {
|
||
|
assert(expected.indexOf(actual) !== -1, "assert_in_array", description,
|
||
|
"value ${actual} not in array ${expected}",
|
||
|
{actual: actual, expected: expected});
|
||
|
}
|
||
|
expose(assert_in_array, 'assert_in_array');
|
||
|
|
||
|
/** Assert that |expected| and |actual| have all the same properties,
|
||
|
which are, recursively, strictly equal. For primitive types this
|
||
|
is the same as |assert_equals|.
|
||
|
*/
|
||
|
function assert_deep_equals(actual, expected, description) {
|
||
|
var stack = [];
|
||
|
function check_equal_r(act, exp) {
|
||
|
if (is_primitive_value(exp) || is_primitive_value(act)) {
|
||
|
assert(same_value(act, exp),
|
||
|
"assert_deep_equals", description,
|
||
|
"expected ${exp} but got ${act}" +
|
||
|
" (top level: expected ${expected} but got ${actual})",
|
||
|
{exp: exp, act: act, expected: expected, actual: actual});
|
||
|
|
||
|
} else if (stack.indexOf(act) === -1) {
|
||
|
var ka = {}, ke = {}, k;
|
||
|
stack.push(act);
|
||
|
|
||
|
Object.getOwnPropertyNames(actual).forEach(function (x) {
|
||
|
ka[x] = true;
|
||
|
});
|
||
|
Object.getOwnPropertyNames(expected).forEach(function (x) {
|
||
|
ke[x] = true;
|
||
|
});
|
||
|
|
||
|
for (k in ke) {
|
||
|
assert(k in ka,
|
||
|
"assert_deep_equals", description,
|
||
|
"expected property ${k} missing" +
|
||
|
" (top level: expected ${expected} but got ${actual})",
|
||
|
{k: k, expected: expected, actual: actual});
|
||
|
|
||
|
check_equal_r(act[k], exp[k]);
|
||
|
delete ka[k];
|
||
|
}
|
||
|
for (k in ka) {
|
||
|
assert(false, "assert_deep_equals", description,
|
||
|
"unexpected property ${k}" +
|
||
|
" (top level: expected ${expected} but got ${actual})",
|
||
|
{k: k, expected: expected, actual: actual});
|
||
|
}
|
||
|
|
||
|
stack.pop();
|
||
|
}
|
||
|
}
|
||
|
check_equal_r(actual, expected);
|
||
|
}
|
||
|
expose(assert_deep_equals, 'assert_deep_equals');
|
||
|
|
||
|
/** Assert that |expected| and |actual|, both primitive numbers, are
|
||
|
within |epsilon| of each other. */
|
||
|
function assert_approx_equals(actual, expected, epsilon, description) {
|
||
|
assert(typeof actual === "number",
|
||
|
"assert_approx_equals", description,
|
||
|
"expected a number but got a ${type_actual}",
|
||
|
{type_actual: typeof actual});
|
||
|
assert(typeof expected === "number",
|
||
|
"assert_approx_equals", description,
|
||
|
"expectation should be a number, got a ${type_expected}",
|
||
|
{type_expected: typeof expected});
|
||
|
assert(typeof epsilon === "number",
|
||
|
"assert_approx_equals", description,
|
||
|
"epsilon should be a number but got a ${type_epsilon}",
|
||
|
{type_epsilon: typeof epsilon});
|
||
|
|
||
|
assert(Math.abs(actual - expected) <= epsilon,
|
||
|
"assert_approx_equals", description,
|
||
|
"expected ${expected} +/- ${epsilon} but got ${actual}",
|
||
|
{expected: expected, actual: actual, epsilon: epsilon});
|
||
|
}
|
||
|
expose(assert_approx_equals, 'assert_approx_equals');
|
||
|
|
||
|
/** Assert that |actual| is less than |expected|, where both are
|
||
|
primitive numbers. */
|
||
|
function assert_less_than(actual, expected, description) {
|
||
|
assert(typeof actual === "number",
|
||
|
"assert_less_than", description,
|
||
|
"expected a number but got a ${type_actual}",
|
||
|
{type_actual: typeof actual});
|
||
|
assert(typeof expected === "number",
|
||
|
"assert_approx_equals", description,
|
||
|
"expectation should be a number, got a ${type_expected}",
|
||
|
{type_expected: typeof expected});
|
||
|
|
||
|
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');
|
||
|
|
||
|
/** Assert that |actual| is greater than |expected|, where both are
|
||
|
primitive numbers. */
|
||
|
function assert_greater_than(actual, expected, description) {
|
||
|
assert(typeof actual === "number",
|
||
|
"assert_greater_than", description,
|
||
|
"expected a number but got a ${type_actual}",
|
||
|
{type_actual: typeof actual});
|
||
|
assert(typeof expected === "number",
|
||
|
"assert_approx_equals", description,
|
||
|
"expectation should be a number, got a ${type_expected}",
|
||
|
{type_expected: typeof expected});
|
||
|
|
||
|
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');
|
||
|
|
||
|
/** Assert that |actual| is less than or equal to |expected|, where
|
||
|
both are primitive numbers. */
|
||
|
function assert_less_than_equal(actual, expected, description) {
|
||
|
assert(typeof actual === "number",
|
||
|
"assert_less_than_equal", description,
|
||
|
"expected a number but got a ${type_actual}",
|
||
|
{type_actual: typeof actual});
|
||
|
assert(typeof expected === "number",
|
||
|
"assert_approx_equals", description,
|
||
|
"expectation should be a number, got a ${type_expected}",
|
||
|
{type_expected: typeof expected});
|
||
|
|
||
|
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');
|
||
|
|
||
|
/** Assert that |actual| is greater than or equal to |expected|, where
|
||
|
both are primitive numbers. */
|
||
|
function assert_greater_than_equal(actual, expected, description) {
|
||
|
assert(typeof actual === "number",
|
||
|
"assert_greater_than_equal", description,
|
||
|
"expected a number but got a ${type_actual}",
|
||
|
{type_actual: typeof actual});
|
||
|
assert(typeof expected === "number",
|
||
|
"assert_approx_equals", description,
|
||
|
"expectation should be a number, got a ${type_expected}",
|
||
|
{type_expected: typeof expected});
|
||
|
|
||
|
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');
|
||
|
|
||
|
/** Assert that |actual|, a string, matches a regexp, |expected|. */
|
||
|
function assert_regexp_match(actual, expected, description) {
|
||
|
assert(expected.test(actual),
|
||
|
"assert_regexp_match", description,
|
||
|
"expected ${actual} to match ${expected}",
|
||
|
{expected: expected, actual: actual});
|
||
|
}
|
||
|
expose(assert_regexp_match, 'assert_regexp_match');
|
||
|
|
||
|
/** Assert that |actual|, a string, does _not_ match a regexp, |expected|. */
|
||
|
function assert_regexp_not_match(actual, expected, description) {
|
||
|
assert(!expected.test(actual),
|
||
|
"assert_regexp_not_match", description,
|
||
|
"expected ${actual} not to match ${expected}",
|
||
|
{expected: expected, actual: actual});
|
||
|
}
|
||
|
expose(assert_regexp_not_match, 'assert_regexp_not_match');
|
||
|
|
||
|
/** Assert that |typeof object| is strictly equal to |type|. */
|
||
|
function assert_type_of(object, type, description) {
|
||
|
assert(typeof object === type,
|
||
|
"assert_type_of", description,
|
||
|
"expected typeof ${object} to be ${expected}, got ${actual}",
|
||
|
{object: object, expected: type, actual: typeof object});
|
||
|
}
|
||
|
expose(assert_type_of, 'assert_type_of');
|
||
|
|
||
|
/** Assert that |object instanceof type|. */
|
||
|
function assert_instance_of(object, type, description) {
|
||
|
assert(object instanceof type,
|
||
|
"assert_instance_of", description,
|
||
|
"expected ${object} to be instanceof ${expected}",
|
||
|
{object: object, expected: type});
|
||
|
}
|
||
|
expose(assert_instance_of, 'assert_instance_of');
|
||
|
|
||
|
/** Assert that |object| has the class string |expected|. */
|
||
|
function assert_class_string(object, expected, description) {
|
||
|
var actual = ({}).toString.call(object).slice(8, -1);
|
||
|
assert(actual === expected,
|
||
|
"assert_class_string", description,
|
||
|
"expected ${object} to have class string ${expected}, "+
|
||
|
"but got ${actual}",
|
||
|
{object: object, expected: expected, actual: actual});
|
||
|
}
|
||
|
expose(assert_class_string, 'assert_class_string');
|
||
|
|
||
|
/** Assert that |object| has a property named |name|. */
|
||
|
function assert_own_property(object, name, description) {
|
||
|
assert(typeof object === "object",
|
||
|
"assert_own_property", description,
|
||
|
"provided value is not an object");
|
||
|
|
||
|
assert("hasOwnProperty" in object,
|
||
|
"assert_own_property", description,
|
||
|
"provided value is an object but has no hasOwnProperty method");
|
||
|
|
||
|
assert(object.hasOwnProperty(name),
|
||
|
"assert_own_property", description,
|
||
|
"expected property ${name} missing", {name: name});
|
||
|
}
|
||
|
expose(assert_own_property, 'assert_own_property');
|
||
|
|
||
|
/** Assert that |object| inherits a property named |name|.
|
||
|
Note: this assertion will fail for objects that have an
|
||
|
own-property named |name|. */
|
||
|
function assert_inherits(object, name, description) {
|
||
|
assert(typeof object === "object",
|
||
|
"assert_inherits", description,
|
||
|
"provided value is not an object");
|
||
|
|
||
|
assert("hasOwnProperty" in object,
|
||
|
"assert_inherits", description,
|
||
|
"provided value is an object but has no hasOwnProperty method");
|
||
|
|
||
|
assert(!object.hasOwnProperty(name),
|
||
|
"assert_inherits", description,
|
||
|
"property ${p} found on object, expected only in prototype chain",
|
||
|
{p: name});
|
||
|
|
||
|
assert(name in object,
|
||
|
"assert_inherits", description,
|
||
|
"property ${p} not found in prototype chain",
|
||
|
{p: name});
|
||
|
|
||
|
}
|
||
|
expose(assert_inherits, 'assert_inherits');
|
||
|
|
||
|
/** Assert that |object| neither has nor inherits a property named |name|. */
|
||
|
function assert_no_property(object, name, description) {
|
||
|
assert(typeof object === "object",
|
||
|
"assert_no_property", description,
|
||
|
"provided value is not an object");
|
||
|
|
||
|
assert("hasOwnProperty" in object,
|
||
|
"assert_no_property", description,
|
||
|
"provided value is an object but has no hasOwnProperty method");
|
||
|
|
||
|
assert(!object.hasOwnProperty(name),
|
||
|
"assert_no_property", description,
|
||
|
"property ${p} found on object, expected to be absent",
|
||
|
{p: name});
|
||
|
|
||
|
assert(!(name in object),
|
||
|
"assert_no_property", description,
|
||
|
"property ${p} found in prototype chain, expected to be absent",
|
||
|
{p: name});
|
||
|
}
|
||
|
expose(assert_no_property, 'assert_no_property');
|
||
|
|
||
|
/** Assert that property |name| of |object| is read-only according
|
||
|
to its property descriptor. */
|
||
|
function assert_readonly(object, name, description) {
|
||
|
var o = {}, desc;
|
||
|
|
||
|
assert('getOwnPropertyDescriptor' in o,
|
||
|
"assert_readonly", description,
|
||
|
"Object.getOwnPropertyDescriptor is missing");
|
||
|
|
||
|
assert(object.hasOwnProperty(name),
|
||
|
"assert_readonly", description,
|
||
|
"expected property ${name} missing", {name: name});
|
||
|
|
||
|
desc = o.getOwnPropertyDescriptor.call(object, name);
|
||
|
if ('writable' in desc) {
|
||
|
assert(!desc.writable, "assert_readonly", description,
|
||
|
"data property ${name} is writable (expected read-only)",
|
||
|
{name: name});
|
||
|
} else {
|
||
|
assert('get' in desc && 'set' in desc,
|
||
|
"assert_readonly", description,
|
||
|
"unrecognized type of property descriptor "+
|
||
|
"for ${name}: ${desc}",
|
||
|
{name: name, desc: desc});
|
||
|
assert(desc.set === undefined,
|
||
|
"assert_readonly", description,
|
||
|
"property ${name} has a setter (expected read-only)",
|
||
|
{name: name, desc: desc});
|
||
|
}
|
||
|
}
|
||
|
expose(assert_readonly, 'assert_readonly');
|
||
|
|
||
|
/** Assert that |func| throws an exception described by |code|.
|
||
|
|func| is called with no arguments and no |this| -- use bind() if
|
||
|
that's a problem. |code| can take one of two forms:
|
||
|
|
||
|
string - the thrown exception must be a DOMException with the
|
||
|
given name, e.g., "TimeoutError", or else it must
|
||
|
stringify to this string.
|
||
|
|
||
|
object - must have one or more of the properties "code", "name",
|
||
|
and "message". Whichever properties are present must
|
||
|
match the corresponding properties of the thrown
|
||
|
exception. As a special case, "message" will also match
|
||
|
the stringification of the exception.
|
||
|
*/
|
||
|
|
||
|
function assert_throws(code, func, description) {
|
||
|
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 (typeof code === "object") {
|
||
|
assert("name" in code || "code" in code || "message" in code,
|
||
|
"assert_throws", description,
|
||
|
"exception spec ${code} has no 'name', 'code', or 'message'" +
|
||
|
"properties",
|
||
|
{code: code});
|
||
|
} else if (name_code_map.hasOwnProperty(code)) {
|
||
|
code = { name: code,
|
||
|
code: name_code_map[code] };
|
||
|
} else {
|
||
|
code = { message: code.toString() };
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
func();
|
||
|
assert(false, "assert_throws", description,
|
||
|
"${func} did not throw", {func: func});
|
||
|
} catch (e) {
|
||
|
if (e instanceof AssertionError) {
|
||
|
throw e;
|
||
|
}
|
||
|
|
||
|
// Backward compatibility wart for DOMExceptions identified
|
||
|
// only by numeric code.
|
||
|
if ("code" in code && code.code !== 0 &&
|
||
|
(!("name" in e) || e.name === e.name.toUpperCase() ||
|
||
|
e.name === "DOMException"))
|
||
|
delete code.name;
|
||
|
|
||
|
if ("name" in code) {
|
||
|
assert("name" in e && e.name === code.name,
|
||
|
"assert_throws", description,
|
||
|
"${func} threw ${actual} (${actual_name}), "+
|
||
|
"expected ${expected} (${expected_name})",
|
||
|
{func: func, actual: e, actual_name: e.name,
|
||
|
expected: code,
|
||
|
expected_name: code.name});
|
||
|
}
|
||
|
if ("code" in code) {
|
||
|
assert("code" in e && e.code === code.code,
|
||
|
"assert_throws", description,
|
||
|
"${func} threw ${actual} (${actual_code}), "+
|
||
|
"expected ${expected} (${expected_code})",
|
||
|
{func: func, actual: e, actual_code: e.code,
|
||
|
expected: code,
|
||
|
expected_code: code.code});
|
||
|
}
|
||
|
if ("message" in code) {
|
||
|
if (Object.hasOwnProperty.call(e, "message")) {
|
||
|
assert(e.message === code.message,
|
||
|
"assert_throws", description,
|
||
|
"${func} threw ${actual} (${actual_message}), "+
|
||
|
"expected ${expected} (${expected_message})",
|
||
|
{func: func, actual: e, actual_message: e.message,
|
||
|
expected: code, expected_message: code.message});
|
||
|
} else {
|
||
|
// Intentional use of loose equality
|
||
|
assert(e == code.message,
|
||
|
"assert_throws", description,
|
||
|
"${func} threw ${actual}, expected ${expected})",
|
||
|
{func: func, actual: e, expected: code.message});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
expose(assert_throws, 'assert_throws');
|
||
|
|
||
|
/** Assert that control flow cannot reach the point where this
|
||
|
assertion appears. */
|
||
|
function assert_unreached(description) {
|
||
|
assert(false, "assert_unreached", description,
|
||
|
"reached unreachable code");
|
||
|
}
|
||
|
expose(assert_unreached, 'assert_unreached');
|
||
|
|
||
|
/** Test object.
|
||
|
* These must be created by calling test() or async_test(), but
|
||
|
* many of their methods are part of the public API.
|
||
|
*/
|
||
|
|
||
|
function Test(name, properties) {
|
||
|
this.name = name;
|
||
|
this.phase = Test.phases.INITIAL;
|
||
|
this.in_done = false;
|
||
|
this.status = Test.NOTRUN;
|
||
|
this.timeout_id = null;
|
||
|
this.message = null;
|
||
|
this.steps = [];
|
||
|
this.cleanup_callbacks = [];
|
||
|
|
||
|
if (!properties) {
|
||
|
properties = {};
|
||
|
}
|
||
|
this.properties = properties;
|
||
|
this.timeout_length = properties.timeout ? properties.timeout
|
||
|
: tests.test_timeout_length;
|
||
|
this.should_run = !properties.skip;
|
||
|
tests.push(this);
|
||
|
this.number = tests.tests.length;
|
||
|
|
||
|
if (!this.should_run) {
|
||
|
// Fake initial step that _does_ run, but short-circuits the test.
|
||
|
// All other steps will be marked not to be run via the defaults
|
||
|
// in step(). The step function does not call done() because that
|
||
|
// would record a success. We can't do this ourselves because the
|
||
|
// plan line has not been emitted yet.
|
||
|
var stepdata = this.step(function () {
|
||
|
this.phase = Test.phases.COMPLETE;
|
||
|
tests.result(this);
|
||
|
});
|
||
|
stepdata.should_run = true;
|
||
|
stepdata.auto_run = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Test.phases = {
|
||
|
INITIAL: 0,
|
||
|
STARTED: 1,
|
||
|
HAS_RESULT: 2,
|
||
|
COMPLETE: 3
|
||
|
};
|
||
|
Test.statuses = {
|
||
|
NOTRUN: -1,
|
||
|
PASS: 0,
|
||
|
FAIL: 1,
|
||
|
XFAIL: 4,
|
||
|
XPASS: 5
|
||
|
};
|
||
|
Test.prototype.phases = Test.phases;
|
||
|
Test.prototype.statuses = Test.phases;
|
||
|
|
||
|
(function() {
|
||
|
var x;
|
||
|
var o = Test.statuses;
|
||
|
for (x in o) {
|
||
|
if (o.hasOwnProperty(x)) {
|
||
|
Test[x] = Test.statuses[x];
|
||
|
Test.prototype[x] = Test.statuses[x];
|
||
|
}
|
||
|
}
|
||
|
})();
|
||
|
|
||
|
/** Queue one step of a test. |func| will eventually be called, with
|
||
|
|this| set to |this_obj|, or to the Test object if |this_obj| is
|
||
|
absent. Any further arguments will be passed down to |func|. It
|
||
|
should carry out some tests using assert_* and eventually return.
|
||
|
|func| will _not_ be called if a previous step of the test has
|
||
|
already failed.
|
||
|
|
||
|
Returns an object which can be passed to this.perform_step() to
|
||
|
cause |func| actually to be called -- but you should not do this
|
||
|
yourself unless absolutely unavoidable.
|
||
|
*/
|
||
|
Test.prototype.step = function step(func, this_obj) {
|
||
|
if (this_obj == null) {
|
||
|
this_obj = this;
|
||
|
}
|
||
|
func = func.bind(this_obj, ...Array.prototype.slice.call(arguments, 2));
|
||
|
|
||
|
var stepdata = {
|
||
|
func: func,
|
||
|
should_run: this.should_run,
|
||
|
auto_run: this.should_run,
|
||
|
has_run: false
|
||
|
};
|
||
|
|
||
|
this.steps.push(stepdata);
|
||
|
return stepdata;
|
||
|
};
|
||
|
|
||
|
/** Internal: perform one step of a test.
|
||
|
*/
|
||
|
Test.prototype.perform_step = function perform_step(stepdata) {
|
||
|
var message;
|
||
|
|
||
|
if (this.phase > this.phases.STARTED ||
|
||
|
tests.phase > tests.phases.HAVE_RESULTS) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
this.phase = this.phases.STARTED;
|
||
|
tests.started = true;
|
||
|
stepdata.has_run = true;
|
||
|
|
||
|
// Arm the local timeout if it hasn't happened already.
|
||
|
if (this.timeout_id === null && this.timeout_length !== null) {
|
||
|
this.timeout_id = setTimeout(this.force_timeout.bind(this),
|
||
|
this.timeout_length);
|
||
|
}
|
||
|
|
||
|
var rv = undefined;
|
||
|
try {
|
||
|
rv = stepdata.func();
|
||
|
} catch (e) {
|
||
|
this.fail(format_exception(e));
|
||
|
}
|
||
|
return rv;
|
||
|
};
|
||
|
|
||
|
/** Mark this test as failed. */
|
||
|
Test.prototype.fail = function fail(message) {
|
||
|
if (this.phase < this.phases.HAS_RESULT) {
|
||
|
this.message = message;
|
||
|
this.status = this.FAIL;
|
||
|
this.phase = this.phases.HAS_RESULT;
|
||
|
if (!this.in_done) {
|
||
|
this.done();
|
||
|
}
|
||
|
} else {
|
||
|
tests.output.error(message);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/** Mark this test as completed. */
|
||
|
Test.prototype.done = function done() {
|
||
|
var i;
|
||
|
|
||
|
this.in_done = true;
|
||
|
|
||
|
if (this.timeout_id !== null) {
|
||
|
clearTimeout(this.timeout_id);
|
||
|
this.timeout_id = null;
|
||
|
}
|
||
|
|
||
|
if (this.phase == this.phases.COMPLETE) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Cleanups run in reverse order (most recently added first).
|
||
|
for (i = this.cleanup_callbacks.length - 1; i >= 0; i--) {
|
||
|
try {
|
||
|
this.cleanup_callbacks[i].call(this);
|
||
|
} catch (e) {
|
||
|
this.fail("In cleanup: " + format_exception(e));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If any step of the test was not run (except those that are not
|
||
|
// _supposed_ to run), and no previous error was detected, that is
|
||
|
// an error.
|
||
|
if (this.phase < this.phases.HAS_RESULT) {
|
||
|
for (i = 0; i < this.steps.length; i++) {
|
||
|
if (this.steps[i].should_run && !this.steps[i].has_run) {
|
||
|
this.fail("Step "+i+" was not run");
|
||
|
} else if (!this.steps[i].should_run && this.steps[i].has_run) {
|
||
|
this.fail("Step "+i+" should not have run");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.phase == this.phases.STARTED) {
|
||
|
this.message = null;
|
||
|
this.status = this.PASS;
|
||
|
}
|
||
|
|
||
|
if (this.properties.expected_fail) {
|
||
|
if (this.status === this.PASS) {
|
||
|
this.status = this.XPASS;
|
||
|
} else if (this.status === this.FAIL) {
|
||
|
this.status = this.XFAIL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.phase = this.phases.COMPLETE;
|
||
|
|
||
|
tests.result(this);
|
||
|
|
||
|
};
|
||
|
|
||
|
/** Register |func| as a step function, and return a function that
|
||
|
will run |func|'s step when called. The arguments to |func| are
|
||
|
whatever the arguments to the callback were, and |this| is
|
||
|
|this_obj|, which defaults to the Test object. Useful as an event
|
||
|
handler, for instance. */
|
||
|
Test.prototype.step_func = function(func, this_obj) {
|
||
|
var test_this = this;
|
||
|
var cb_args = [];
|
||
|
if (arguments.length === 1) {
|
||
|
this_obj = test_this;
|
||
|
}
|
||
|
|
||
|
// The function returned stashes its arguments in |cb_args|, then
|
||
|
// the registered step function uses them to call |func| with the
|
||
|
// appropriate arguments. We have to do it this way because
|
||
|
// perform_step() doesn't forward its arguments.
|
||
|
var stepdata = this.step(function cb_step () {
|
||
|
return func.apply(this_obj, cb_args);
|
||
|
});
|
||
|
|
||
|
stepdata.auto_run = false;
|
||
|
return function() {
|
||
|
cb_args = Array.prototype.slice.call(arguments);
|
||
|
return test_this.perform_step(stepdata);
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/** As |step_func|, but the step calls this.done() after |func|
|
||
|
returns (regardless of what it returns). |func| may be omitted,
|
||
|
in which case the step just calls this.done(). */
|
||
|
Test.prototype.step_func_done = function(func, this_obj) {
|
||
|
var test_this = this;
|
||
|
var cb_args = [];
|
||
|
|
||
|
if (arguments.length <= 1) {
|
||
|
this_obj = test_this;
|
||
|
}
|
||
|
if (!func) {
|
||
|
func = function () {};
|
||
|
}
|
||
|
|
||
|
// The function returned stashes its arguments in |cb_args|, then
|
||
|
// the registered step function uses them to call |func| with the
|
||
|
// appropriate arguments. We have to do it this way because
|
||
|
// perform_step() doesn't forward its arguments.
|
||
|
var stepdata = this.step(function cb_done_step () {
|
||
|
var rv = func.apply(this_obj, cb_args);
|
||
|
test_this.done();
|
||
|
return rv;
|
||
|
});
|
||
|
|
||
|
stepdata.auto_run = false;
|
||
|
return function() {
|
||
|
cb_args = Array.prototype.slice.call(arguments);
|
||
|
return test_this.perform_step(stepdata);
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/** Returns a function that, if called, will call assert_unreached()
|
||
|
inside a perform_step() invocation. Use to set event handlers for
|
||
|
events that should _not_ happen. */
|
||
|
Test.prototype.unreached_func = function unreached_func(description) {
|
||
|
var test_this = this;
|
||
|
var stepdata = this.step(function unreached_step () {
|
||
|
assert_unreached(description);
|
||
|
});
|
||
|
stepdata.should_run = false;
|
||
|
stepdata.auto_run = false;
|
||
|
|
||
|
return function() { test_this.perform_step(stepdata); };
|
||
|
};
|
||
|
|
||
|
/** Register |callback| to be called once this test is done. */
|
||
|
Test.prototype.add_cleanup = function add_cleanup(callback) {
|
||
|
this.cleanup_callbacks.push(callback);
|
||
|
};
|
||
|
|
||
|
/** Treat this test as having timed out. */
|
||
|
Test.prototype.force_timeout = function force_timeout() {
|
||
|
this.message = "Test timed out";
|
||
|
this.status = this.FAIL;
|
||
|
this.phase = this.phases.HAS_RESULT;
|
||
|
this.done();
|
||
|
};
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Private implementation logic begins at this point.
|
||
|
*/
|
||
|
|
||
|
/*
|
||
|
* The Tests object is responsible for tracking the complete set of
|
||
|
* tests in this file.
|
||
|
*/
|
||
|
|
||
|
function Tests(output) {
|
||
|
this.tests = [];
|
||
|
this.num_pending = 0;
|
||
|
|
||
|
this.all_loaded = false;
|
||
|
this.wait_for_finish = false;
|
||
|
this.allow_uncaught_exception = false;
|
||
|
|
||
|
this.test_timeout_length = settings.test_timeout;
|
||
|
this.harness_timeout_length = settings.harness_timeout;
|
||
|
this.timeout_id = null;
|
||
|
|
||
|
this.properties = {};
|
||
|
this.phase = Test.phases.INITIAL;
|
||
|
this.output = output;
|
||
|
|
||
|
var this_obj = this;
|
||
|
phantom.onError = function (message, stack) {
|
||
|
if (!tests.allow_uncaught_exception) {
|
||
|
this_obj.output.error(message);
|
||
|
}
|
||
|
if (this_obj.all_done()) {
|
||
|
this_obj.complete();
|
||
|
}
|
||
|
};
|
||
|
phantom.page.onConsoleMessage = function (message) {
|
||
|
if (!tests.allow_uncaught_exception) {
|
||
|
this_obj.output.error("stray console message: " + message);
|
||
|
}
|
||
|
};
|
||
|
this.set_timeout();
|
||
|
}
|
||
|
|
||
|
Tests.phases = {
|
||
|
INITIAL: 0,
|
||
|
SETUP: 1,
|
||
|
HAVE_TESTS: 2,
|
||
|
HAVE_RESULTS: 3,
|
||
|
ABANDONED: 4,
|
||
|
COMPLETE: 5
|
||
|
};
|
||
|
Tests.prototype.phases = Tests.phases;
|
||
|
|
||
|
Tests.prototype.setup = function setup(func, properties) {
|
||
|
if (this.phase >= this.phases.HAVE_RESULTS) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (this.phase < this.phases.SETUP) {
|
||
|
this.phase = this.phases.SETUP;
|
||
|
}
|
||
|
|
||
|
this.properties = properties;
|
||
|
|
||
|
for (var p in properties) {
|
||
|
if (properties.hasOwnProperty(p)) {
|
||
|
var value = properties[p];
|
||
|
if (p == "allow_uncaught_exception") {
|
||
|
this.allow_uncaught_exception = value;
|
||
|
} else if (p == "explicit_done" && value) {
|
||
|
this.wait_for_finish = true;
|
||
|
} else if (p == "timeout" && value) {
|
||
|
this.harness_timeout_length = value;
|
||
|
} else if (p == "test_timeout") {
|
||
|
this.test_timeout_length = value;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
this.set_timeout();
|
||
|
|
||
|
if (func) {
|
||
|
try {
|
||
|
func();
|
||
|
} catch (e) {
|
||
|
this.output.error(e);
|
||
|
this.phase = this.phases.ABANDONED;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Tests.prototype.set_timeout = function set_timeout() {
|
||
|
var this_obj = this;
|
||
|
clearTimeout(this.timeout_id);
|
||
|
if (this.harness_timeout_length !== null) {
|
||
|
this.timeout_id = setTimeout(function () { this_obj.timeout(); },
|
||
|
this.harness_timeout_length);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Tests.prototype.timeout = function timeout() {
|
||
|
this.output.error("Global timeout expired");
|
||
|
for (var i = 0; i < tests.tests.length; i++) {
|
||
|
var t = tests.tests[i];
|
||
|
if (t.phase < Test.phases.HAS_RESULT) {
|
||
|
t.force_timeout();
|
||
|
}
|
||
|
}
|
||
|
this.complete();
|
||
|
};
|
||
|
|
||
|
Tests.prototype.end_wait = function end_wait() {
|
||
|
this.wait_for_finish = false;
|
||
|
if (this.all_done()) {
|
||
|
this.complete();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Tests.prototype.push = function push(test)
|
||
|
{
|
||
|
if (this.phase < this.phases.HAVE_TESTS) {
|
||
|
this.phase = this.phases.HAVE_TESTS;
|
||
|
}
|
||
|
this.num_pending++;
|
||
|
this.tests.push(test);
|
||
|
};
|
||
|
|
||
|
Tests.prototype.all_done = function all_done() {
|
||
|
return (this.tests.length > 0 &&
|
||
|
this.all_loaded &&
|
||
|
this.num_pending === 0 &&
|
||
|
!this.wait_for_finish);
|
||
|
};
|
||
|
|
||
|
Tests.prototype.result = function result(test) {
|
||
|
if (this.phase < this.phases.HAVE_RESULTS) {
|
||
|
this.phase = this.phases.HAVE_RESULTS;
|
||
|
}
|
||
|
this.num_pending--;
|
||
|
this.output.result(test);
|
||
|
if (this.all_done()) {
|
||
|
this.complete();
|
||
|
} else {
|
||
|
setTimeout(this.run_tests.bind(this), 0);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Tests.prototype.run_tests = function run_tests() {
|
||
|
if (this.phase >= this.phases.COMPLETE) {
|
||
|
return;
|
||
|
}
|
||
|
if (this.all_done() || this.phase >= this.phases.ABANDONED) {
|
||
|
this.complete();
|
||
|
return;
|
||
|
}
|
||
|
for (var i = 0; i < this.tests.length; i++) {
|
||
|
var t = this.tests[i];
|
||
|
if (t.phase < t.phases.STARTED && t.steps.length > 0) {
|
||
|
for (var j = 0; j < t.steps.length; j++) {
|
||
|
if (t.steps[j].auto_run) {
|
||
|
t.perform_step(t.steps[j]);
|
||
|
}
|
||
|
}
|
||
|
// We will come back here via the setTimeout in
|
||
|
// Tests.prototype.result.
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Tests.prototype.begin = function begin() {
|
||
|
this.all_loaded = true;
|
||
|
this.output.begin(this.tests.length, this.phase);
|
||
|
this.run_tests();
|
||
|
if (this.all_done()) {
|
||
|
this.complete();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Tests.prototype.complete = function complete() {
|
||
|
var i, x;
|
||
|
|
||
|
if (this.phase === this.phases.COMPLETE) {
|
||
|
return;
|
||
|
}
|
||
|
for (i = 0; i < this.tests.length; i++) {
|
||
|
this.tests[i].done();
|
||
|
}
|
||
|
|
||
|
this.phase = this.phases.COMPLETE;
|
||
|
this.output.complete(this);
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* The Output object is responsible for reporting the status of each
|
||
|
* test. For PhantomJS, output is much simpler than for the W3C
|
||
|
* harness; we basically just log things to the console as
|
||
|
* appropriate. The output format is meant to be compatible with
|
||
|
* TAP (http://testanything.org/tap-specification.html).
|
||
|
*/
|
||
|
|
||
|
function Output(fp, verbose) {
|
||
|
this.fp = fp;
|
||
|
this.verbose = verbose;
|
||
|
this.failed = false;
|
||
|
}
|
||
|
|
||
|
Output.prototype.begin = function begin(n, phase) {
|
||
|
if (phase === Tests.phases.ABANDONED) {
|
||
|
this.fp.write("1..0 # SKIP: setup failed\n");
|
||
|
} else {
|
||
|
this.fp.write("1.." + n + "\n");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Output.prototype.diagnostic = function diagnostic(message, is_info) {
|
||
|
var fp = this.fp;
|
||
|
var prefix = "# ";
|
||
|
if (is_info) {
|
||
|
prefix = "## ";
|
||
|
}
|
||
|
message.split("\n").forEach(function (line) {
|
||
|
fp.write(prefix + line + "\n");
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Output.prototype.error = function error(message) {
|
||
|
this.diagnostic("ERROR: " + message);
|
||
|
this.failed = true;
|
||
|
};
|
||
|
|
||
|
Output.prototype.info = function info(message) {
|
||
|
this.diagnostic(message, true);
|
||
|
};
|
||
|
|
||
|
Output.prototype.result = function result(test) {
|
||
|
if (test.message) {
|
||
|
this.diagnostic(test.message);
|
||
|
}
|
||
|
var prefix, directive = "";
|
||
|
switch (test.status) {
|
||
|
case Test.PASS: prefix = "ok "; break;
|
||
|
case Test.FAIL: prefix = "not ok "; break;
|
||
|
case Test.XPASS: prefix = "ok "; directive = " # TODO"; break;
|
||
|
case Test.XFAIL: prefix = "not ok "; directive = " # TODO"; break;
|
||
|
case Test.NOTRUN: prefix = "ok "; directive = " # SKIP"; break;
|
||
|
default:
|
||
|
this.error("Unrecognized test status " + test.status);
|
||
|
prefix = "not ok ";
|
||
|
}
|
||
|
if ((prefix === "not ok " && directive !== " # TODO") ||
|
||
|
(prefix === "ok " && directive === " # TODO")) {
|
||
|
this.failed = true;
|
||
|
}
|
||
|
this.fp.write(prefix + test.number + " " + test.name + directive + "\n");
|
||
|
};
|
||
|
|
||
|
Output.prototype.complete = function complete(tests) {
|
||
|
phantom.exit(this.failed ? 1 : 0);
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* Utilities.
|
||
|
*/
|
||
|
|
||
|
function expose(fn, name) {
|
||
|
window[name] = fn;
|
||
|
}
|
||
|
|
||
|
/** This function is not part of the public API, but its
|
||
|
*behavior* is part of the contract of several assert_* functions. */
|
||
|
function same_value(x, y) {
|
||
|
if (x === y) {
|
||
|
// Distinguish +0 and -0
|
||
|
if (x === 0 && y === 0) {
|
||
|
return 1/x === 1/y;
|
||
|
}
|
||
|
return true;
|
||
|
} else {
|
||
|
// NaN !== _everything_, including another NaN.
|
||
|
// Make it same_value as itself.
|
||
|
if (x !== x) {
|
||
|
return y !== y;
|
||
|
}
|
||
|
// Compare Date and RegExp by value.
|
||
|
if (x instanceof Date) {
|
||
|
return y instanceof Date && x.getTime() === y.getTime();
|
||
|
}
|
||
|
if (x instanceof RegExp) {
|
||
|
return y instanceof RegExp && x.toString() === y.toString();
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Similarly, this function's behavior is part of the contract of
|
||
|
assert_deep_equals. (These are the things for which it will just
|
||
|
call same_value rather than doing a recursive property comparison.) */
|
||
|
function is_primitive_value(val) {
|
||
|
return (val === undefined || val === null || typeof val !== 'object' ||
|
||
|
val instanceof Date || val instanceof RegExp);
|
||
|
}
|
||
|
|
||
|
var names_used = {};
|
||
|
function test_name(func, name) {
|
||
|
var n, c;
|
||
|
|
||
|
if (name)
|
||
|
;
|
||
|
else if (func && func.displayName)
|
||
|
name = func.displayName;
|
||
|
else if (func && func.name)
|
||
|
name = func.name;
|
||
|
else
|
||
|
name = "test";
|
||
|
|
||
|
n = name;
|
||
|
c = 0;
|
||
|
while (n in names_used) {
|
||
|
n = name + "." + c.toString();
|
||
|
c += 1;
|
||
|
}
|
||
|
return n;
|
||
|
}
|
||
|
|
||
|
function AssertionError(message) {
|
||
|
this.message = message;
|
||
|
}
|
||
|
|
||
|
AssertionError.prototype.toString = function toString() {
|
||
|
return this.message;
|
||
|
};
|
||
|
|
||
|
function assert(expected_true, name, description, error, substitutions) {
|
||
|
if (expected_true !== true) {
|
||
|
throw new AssertionError(make_message(
|
||
|
name, description, error, substitutions));
|
||
|
} else if (output.verbose >= 4) {
|
||
|
output.info(make_message(name, description, error, substitutions));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function make_message(function_name, description, error, substitutions) {
|
||
|
var p, message;
|
||
|
|
||
|
for (p in substitutions) {
|
||
|
if (substitutions.hasOwnProperty(p)) {
|
||
|
substitutions[p] = format_value(substitutions[p]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (description) {
|
||
|
description += ": ";
|
||
|
} else {
|
||
|
description = "";
|
||
|
}
|
||
|
|
||
|
return (function_name + ": " + description +
|
||
|
error.replace(/\$\{[^}]+\}/g, function (match) {
|
||
|
return substitutions[match.slice(2,-1)];
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
function format_value(val, seen) {
|
||
|
if (seen === undefined)
|
||
|
seen = [];
|
||
|
|
||
|
var s;
|
||
|
function truncate(s, len) {
|
||
|
if (s.length > len) {
|
||
|
return s.slice(-3) + "...";
|
||
|
}
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
switch (typeof val) {
|
||
|
case "number":
|
||
|
// In JavaScript, -0 === 0 and String(-0) == "0", so we have to
|
||
|
// special-case.
|
||
|
if (val === -0 && 1/val === -Infinity) {
|
||
|
return "-0";
|
||
|
}
|
||
|
// falls through
|
||
|
case "boolean":
|
||
|
case "undefined":
|
||
|
return String(val);
|
||
|
|
||
|
case "string":
|
||
|
// Escape ", \, all C0 and C1 control characters, and
|
||
|
// Unicode's LINE SEPARATOR and PARAGRAPH SEPARATOR.
|
||
|
// The latter two are the only characters above U+009F
|
||
|
// that may not appear verbatim in a JS string constant.
|
||
|
val = val.replace(/["\\\u0000-\u001f\u007f-\u009f\u2028\u2029]/g,
|
||
|
function (c) {
|
||
|
switch (c) {
|
||
|
case "\b": return "\\b";
|
||
|
case "\f": return "\\f";
|
||
|
case "\n": return "\\n";
|
||
|
case "\r": return "\\r";
|
||
|
case "\t": return "\\t";
|
||
|
case "\v": return "\\v";
|
||
|
case "\\": return "\\\\";
|
||
|
case "\"": return "\\\"";
|
||
|
default:
|
||
|
// We know by construction that c is
|
||
|
// a single BMP character.
|
||
|
c = c.charCodeAt(0);
|
||
|
if (c < 0x0080) {
|
||
|
return "\\x" +
|
||
|
("00" + c.toString(16)).slice(-2);
|
||
|
} else {
|
||
|
return "\\u" +
|
||
|
("0000" + c.toString(16)).slice(-4);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
return '"' + val + '"';
|
||
|
|
||
|
case "object":
|
||
|
if (val === null) {
|
||
|
return "null";
|
||
|
}
|
||
|
if (seen.indexOf(val) >= 0) {
|
||
|
return "<cycle>";
|
||
|
}
|
||
|
seen.push(val);
|
||
|
|
||
|
if (Array.isArray(val)) {
|
||
|
return "[" + val.map(function (x) {
|
||
|
return format_value(x, seen);
|
||
|
}).join(", ") + "]";
|
||
|
}
|
||
|
|
||
|
s = String(val);
|
||
|
if (s != "[object Object]") {
|
||
|
return truncate(s, 60);
|
||
|
}
|
||
|
return "{ " + Object.keys(val).map(function (k) {
|
||
|
return format_value(k, seen) + ": " + format_value(val[k], seen);
|
||
|
}).join(", ") + "}";
|
||
|
|
||
|
default:
|
||
|
return typeof val + ' "' + truncate(String(val), 60) + '"';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function format_exception (e) {
|
||
|
var message = (typeof e === "object" && e !== null) ? e.message : e;
|
||
|
if (typeof e.stack != "undefined" && typeof e.message == "string") {
|
||
|
// Prune the stack. This knows the format of WebKit's stack traces.
|
||
|
var stack = e.stack.split("\n");
|
||
|
var lo, hi;
|
||
|
// We do not need to hear about initial lines naming the
|
||
|
// assertion function.
|
||
|
for (lo = 0; lo < stack.length; lo++) {
|
||
|
if (!/^assert(?:_[a-z0-9_]+)?@.*?testharness\.js:/
|
||
|
.test(stack[lo])) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
// We do not need to hear about how we got _to_ the test function.
|
||
|
// The caller of the test function is guaranteed to have "_step" in
|
||
|
// its name.
|
||
|
for (hi = lo; hi < stack.length; hi++) {
|
||
|
if (/^[a-z_]+_step.*?testharness\.js:/.test(stack[hi])) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (lo < stack.length && lo < hi) {
|
||
|
stack = stack.slice(lo, hi);
|
||
|
}
|
||
|
message += "\n";
|
||
|
message += stack.join("\n");
|
||
|
}
|
||
|
return message;
|
||
|
}
|
||
|
|
||
|
function process_command_line(sys) {
|
||
|
function usage(error) {
|
||
|
sys.stderr.write("error: " + error + "\n");
|
||
|
sys.stderr.write("usage: " + sys.args[0] +
|
||
|
" [--verbose=N] test_script.js ...\n");
|
||
|
}
|
||
|
var args = { verbose: -1,
|
||
|
test_script: "" };
|
||
|
|
||
|
for (var i = 1; i < sys.args.length; i++) {
|
||
|
if (sys.args[i].length === 0) {
|
||
|
usage("empty argument is not meaningful");
|
||
|
return args;
|
||
|
}
|
||
|
if (sys.args[i][0] !== '-') {
|
||
|
args.test_script = sys.args[i];
|
||
|
break;
|
||
|
}
|
||
|
var n = "--verbose=".length;
|
||
|
var v = sys.args[i].slice(0, n);
|
||
|
var a = sys.args[i].slice(n);
|
||
|
if (v === "--verbose=" && /^[0-9]+$/.test(a)) {
|
||
|
if (args.verbose === -1) {
|
||
|
args.verbose = parseInt(a, 10);
|
||
|
continue;
|
||
|
} else {
|
||
|
usage("--verbose specified twice");
|
||
|
return args;
|
||
|
}
|
||
|
}
|
||
|
usage("unrecognized option " + format_value(sys.args[i]));
|
||
|
return args;
|
||
|
}
|
||
|
|
||
|
if (args.test_script === "") {
|
||
|
usage("no test script specified");
|
||
|
return args;
|
||
|
}
|
||
|
|
||
|
if (args.verbose === -1) {
|
||
|
args.verbose = 0;
|
||
|
}
|
||
|
|
||
|
return args;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Global state
|
||
|
*/
|
||
|
|
||
|
var settings = {
|
||
|
harness_timeout: 5000,
|
||
|
test_timeout: null
|
||
|
};
|
||
|
|
||
|
var sys = require('system');
|
||
|
var fs = require('fs');
|
||
|
var args = process_command_line(sys);
|
||
|
|
||
|
if (args.test_script === "") {
|
||
|
// process_command_line has already issued an error message.
|
||
|
phantom.exit(2);
|
||
|
} else {
|
||
|
// Reset the library paths for injectJs and require to the
|
||
|
// directory containing the test script, so relative imports work
|
||
|
// as expected. Unfortunately, phantom.libraryPath is not a
|
||
|
// proper search path -- it can only hold one directory at a time.
|
||
|
// require.paths has no such limitation.
|
||
|
var test_script = fs.absolute(args.test_script);
|
||
|
phantom.libraryPath = test_script.slice(0,
|
||
|
test_script.lastIndexOf(fs.separator));
|
||
|
require.paths.push(phantom.libraryPath);
|
||
|
|
||
|
// run-tests.py sets these environment variables to the base URLs
|
||
|
// of its HTTP and HTTPS servers.
|
||
|
expose(sys.env['TEST_HTTP_BASE'], 'TEST_HTTP_BASE');
|
||
|
expose(sys.env['TEST_HTTPS_BASE'], 'TEST_HTTPS_BASE');
|
||
|
|
||
|
var output = new Output(sys.stdout, args.verbose);
|
||
|
var tests = new Tests(output);
|
||
|
|
||
|
// This evaluates the test script synchronously.
|
||
|
// Any errors should be caught by our onError hook.
|
||
|
phantom.injectJs(test_script);
|
||
|
|
||
|
tests.begin();
|
||
|
}
|
||
|
|
||
|
})();
|