jstd-shim
Version:
A jstd shim for executing jstd tests outside the JSTD environment
986 lines (807 loc) • 26 kB
JavaScript
/*
* A very barebones implementation of the JSTD window environment.
*
* Defines some Globals required for running JSTD tests:
* - TestCase, AsyncTestCase, jstestdriver
* - Assert.js from the jstd projectcopied and pasted at end for assertion
* globals (AssertTrue, AssertBoolean, etc.)
*
* Reporter functions:
* - result(resultObj)
* - complete()
*/
var JSTD_SHIM = (function (global) {
var suites = [],
currentSuite,
testQueue = [],
Async = function () {},
default_timeout = 30000, //timeout after 30 secs by default (like jstd)
timeout_ms,
stats,
reporter = Object.create({
result: function (resultObj) {
console.log((resultObj.success ? 'SUCCESS' : 'FAIL') + ' ' + resultObj.description);
},
success: function (resultObj) {},
error: function (error, resultObj, name) {
var message;
name = name || error.name;
console.log(name, error);
if (error.stack) {
console.error(error.stack);
}
},
complete: function (stats) {
console.log(" **** JSTD SHIM RESULTS: " + (stats.fail > 0 || stats.error > 0 ? 'FAIL' : 'SUCCESS') + " **** ");
console.log(" Ran: " + stats.total + " in " + (stats.totalTime / 1000).toFixed(3) + " secs");
console.log(" Passed: " + stats.pass);
console.log(" Failed: " + stats.fail);
console.log(" Error: " + stats.error);
console.log(" Ignored: " + stats.ignore);
}
});
global.TestCase = function (suite_name, tests) {
var fn = function () {},
Sync = function () {};
Sync.prototype = tests;
fn.prototype = new Sync();
suites.push({
tests: fn,
name: suite_name
});
return fn;
};
global.AsyncTestCase = function (name) {
var tempFn = function () {};
tempFn.prototype = new Async();
suites.push({
tests: tempFn,
name: name
});
return tempFn;
};
//mock the jsttestdriver object and jQuery where necessary - because of Asserts.js
global.jstestdriver = {
assertCount: 0,
jQuery: {
isArray: function (obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
}
};
/**
* TODO: Need to think about best solution for DOM manipulations
*/
function cleanUpTestZone() {
var node = document.body,
child = node.firstChild;
while (child) {
node.removeChild(child);
child = node.firstChild;
}
node.innerHTML = "";
}
/*
* JSTD allowed us to insert things into the DOM with comments:
* DOC += <div>something</div>
*
* Since that functionality is gone, we have to toString the fn and replicate that logic here
*/
function doDOMstuff(fn) {
var fnString = fn.toString(), //added inside TestCaseOverrides
pattern = /\/\*:DOC\s\+=([\S\s]+)\*\//g,
cnt = 0;
fnString.replace(pattern, function (all, htmlString) {
//only support one
if (!cnt) {
htmlString = htmlString.replace(/(\r\n?|\n)/g, "");//ignore newlines
htmlString = htmlString.replace(/^\s+|\s+$/g, '');//trim
//TODO: For some tags in IE this is buggy
document.body.innerHTML = htmlString;
}
cnt += 1;
});
}
/**
* A clumsy implementation of JSTD's async test API. This can probably be improved
*/
function runAsyncTest(test, result) {
var fns = [],
suite = currentSuite.tests,
timeout,
counter = 0,
finished = false,
failed = false;
callbacks = {
add: function (fn, count) {
count = count || 1;
if (!counter) {
timeout = setTimeout(function () {
failed = true;
error(new Error("Async Test Timeout: " + currentSuite.name), result);
}, timeout_ms || default_timeout);
}
counter += count;
var callback = function callback () {
if (!failed) {
fn.call();
counter -= 1;
if (counter <= 0) {
clearTimeout(timeout);
setTimeout(run, 1);
}
}
};
return callback;
}
},
queue = {
call: function (msg, fn) {
fns.push(function () {
fn.call({}, callbacks);
// necessary to move to next queue.call
// if no callbacks are added inside
// current queue.call
if (counter <= 0) {
setTimeout(run, 1);
}
});
}
},
run = function () {
if (fns.length) {
try {
fns.shift().call({});
} catch (e) {
error(e, result);
}
} else {
if (!finished) {
finished = true;
runJstdFn("tearDown");
successTest(result);
}
}
};
suite[test](queue); //load fns
run(); //begin executing callbacks in queue
}
function runJstdFn(fnName) {
var suite = currentSuite.tests;
if (suite[fnName]) {
doDOMstuff(suite[fnName]);
suite[fnName]();
}
}
function testIgnore (fn) {
return (fn.toString().indexOf("ignore();") > -1);
}
function ignoreTest(result) {
stats.ignore += 1;
result.skipped = true;
result.time = 0;
completeTest(result);
}
function successTest(result) {
stats.pass += 1;
result.success = true;
result.time = Date.now() - result.t0;
reporter.success(result);
completeTest(result);
}
function completeTest(result) {
reporter.result(result);
stats.total += 1;
setTimeout(runTest, 1);
}
function error(e, result) {
result.success = false;
result.time = Date.now() - result.t0;
if (e.name === "AssertError") {
stats.fail += 1;
reporter.error(e, result);
} else {
stats.error += 1;
reporter.error(e, result, "Unexpected Error");
}
completeTest(result);
}
function runTest() {
var testName = testQueue.shift(),
testObject = currentSuite.tests,
result;
if (testName) {
cleanUpTestZone();
result = {
id: stats.total,
description: testName,
suite: [],
log: [],
t0: Date.now()
};
result.suite.unshift(currentSuite.name);
if (testIgnore(testObject[testName])) {
ignoreTest(result);
} else {
global.globalSetup && globalSetup();
runJstdFn("setUp");
if (testObject instanceof Async) {
runAsyncTest(testName, result);
} else {
try {
runJstdFn(testName);
runJstdFn("tearDown");
successTest(result);
} catch (e) {
error(e, result);
}
}
}
} else {
//DONE WITH ALL TESTS FOR THIS SUITE
runSuite();
}
}
function runSuite() {
currentSuite = suites.shift();
if (currentSuite) {
//push the tests to an array
testQueue = [];
//first, instantiate
if (typeof currentSuite.tests === "function") {
currentSuite.tests = new currentSuite.tests();
}
for (test in currentSuite.tests) if (/^test/.test(test)) {
testQueue.push(test);
}
//START TESTS FOR THIS SUITE
runTest();
} else {
// COMPLETED ALL
stats.totalTime = Date.now() - stats.t0;
reporter.complete(stats);
}
}
function getTotal() {
var cnt = 0,
temp;
suites.forEach(function (suite) {
temp = new suite.tests();
for (prop in temp) {
if (/^test/.test(prop)) {
cnt += 1;
}
}
});
return cnt;
}
return {
execute: function() {
stats = {
total: 0,
pass: 0,
fail: 0,
error: 0,
ignore: 0,
t0: Date.now()
};
runSuite();
},
reset: function () {
suites = [];
timeout_ms = default_timeout;
},
modifyReporter: function (modifyFn) {
reporter = modifyFn(reporter);
},
setAsyncTimeout: function (ms) {
timeout_ms = ms;
},
getTotal: function () {
return getTotal();
},
setNoopReporter: function() {
var noop = function () {};
reporter = {
result: noop,
success: noop,
error: noop,
complete: noop
};
}
}
}(this));
//Begin the jstd project's Asserts.js
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
function expectAsserts(count) {
jstestdriver.expectedAssertCount = count;
}
var fail = function fail(msg) {
var err = new Error(msg);
err.name = 'AssertError';
if (!err.message) {
err.message = msg;
}
throw err;
};
function isBoolean_(bool) {
if (typeof(bool) != 'boolean') {
fail('Not a boolean: ' + prettyPrintEntity_(bool));
}
}
var isElement_ = (function () {
var div = document.createElement('div');
function isNode(obj) {
try {
div.appendChild(obj);
div.removeChild(obj);
} catch (e) {
return false;
}
return true;
}
return function isElement(obj) {
return obj && obj.nodeType === 1 && isNode(obj);
};
}());
function formatElement_(el) {
var tagName;
try {
tagName = el.tagName.toLowerCase();
var str = '<' + tagName;
var attrs = el.attributes, attribute;
for (var i = 0, l = attrs.length; i < l; i++) {
attribute = attrs.item(i);
if (!!attribute.nodeValue) {
str += ' ' + attribute.nodeName + '=\"' + attribute.nodeValue + '\"';
}
}
return str + '>...</' + tagName + '>';
} catch (e) {
return '[Element]' + (!!tagName ? ' ' + tagName : '');
}
}
function prettyPrintEntity_(entity) {
if (isElement_(entity)) {
return formatElement_(entity);
}
var str;
if (typeof entity == 'function') {
try {
str = entity.toString().match(/(function [^\(]+\(\))/)[1];
} catch (e) {}
return str || '[function]';
}
try {
str = JSON.stringify(entity);
} catch (e) {}
return str || '[' + typeof entity + ']';
}
function argsWithOptionalMsg_(args, length) {
var copyOfArgs = [];
// make copy because it's bad practice to change a passed in mutable
// And to ensure we aren't working with an arguments array. IE gets bitchy.
for(var i = 0; i < args.length; i++) {
copyOfArgs.push(args[i]);
}
var min = length - 1;
if (args.length < min) {
fail('expected at least ' + min + ' arguments, got ' + args.length);
} else if (args.length == length) {
copyOfArgs[0] += ' ';
} else {
copyOfArgs.unshift('');
}
return copyOfArgs;
}
function assertTrue(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
isBoolean_(args[1]);
if (args[1] != true) {
fail(args[0] + 'expected true but was ' + prettyPrintEntity_(args[1]));
}
return true;
}
function assertFalse(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
isBoolean_(args[1]);
if (args[1] != false) {
fail(args[0] + 'expected false but was ' + prettyPrintEntity_(args[1]));
}
return true;
}
function assertEquals(msg, expected, actual) {
var args = argsWithOptionalMsg_(arguments, 3);
jstestdriver.assertCount++;
msg = args[0];
expected = args[1];
actual = args[2];
if (!compare_(expected, actual)) {
fail(msg + 'expected ' + prettyPrintEntity_(expected) + ' but was ' +
prettyPrintEntity_(actual) + '');
}
return true;
}
function compare_(expected, actual) {
if (expected === actual) {
return true;
}
if (typeof expected != 'object' ||
typeof actual != 'object' ||
!expected || !actual) {
return expected == actual;
}
if (isElement_(expected) || isElement_(actual)) {
return false;
}
var key = null;
var actualLength = 0;
var expectedLength = 0;
try {
// If an array is expected the length of actual should be simple to
// determine. If it is not it is undefined.
if (jstestdriver.jQuery.isArray(actual)) {
actualLength = actual.length;
} else {
// In case it is an object it is a little bit more complicated to
// get the length.
for (key in actual) {
if (actual.hasOwnProperty(key)) {
++actualLength;
}
}
}
// Arguments object
if (actualLength == 0 && typeof actual.length == 'number') {
actualLength = actual.length;
for (var i = 0, l = actualLength; i < l; i++) {
if (!(i in actual)) {
actualLength = 0;
break;
}
}
}
for (key in expected) {
if (expected.hasOwnProperty(key)) {
if (!compare_(expected[key], actual[key])) {
return false;
}
++expectedLength;
}
}
if (expectedLength != actualLength) {
return false;
}
return expectedLength == 0 ? expected.toString() == actual.toString() : true;
} catch (e) {
return false;
}
}
function assertNotEquals(msg, expected, actual) {
try {
assertEquals.apply(this, arguments);
} catch (e) {
if (e.name == 'AssertError') {
return true;
}
throw e;
}
var args = argsWithOptionalMsg_(arguments, 3);
fail(args[0] + 'expected ' + prettyPrintEntity_(args[1]) +
' not to be equal to ' + prettyPrintEntity_(args[2]));
}
function assertSame(msg, expected, actual) {
var args = argsWithOptionalMsg_(arguments, 3);
jstestdriver.assertCount++;
if (!isSame_(args[2], args[1])) {
fail(args[0] + 'expected ' + prettyPrintEntity_(args[1]) + ' but was ' +
prettyPrintEntity_(args[2]));
}
return true;
}
function assertNotSame(msg, expected, actual) {
var args = argsWithOptionalMsg_(arguments, 3);
jstestdriver.assertCount++;
if (isSame_(args[2], args[1])) {
fail(args[0] + 'expected not same as ' + prettyPrintEntity_(args[1]) +
' but was ' + prettyPrintEntity_(args[2]));
}
return true;
}
function isSame_(expected, actual) {
return actual === expected;
}
function assertNull(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
if (args[1] !== null) {
fail(args[0] + 'expected null but was ' + prettyPrintEntity_(args[1]));
}
return true;
}
function assertNotNull(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
if (args[1] === null) {
fail(args[0] + 'expected not null but was null');
}
return true;
}
function assertUndefined(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
if (typeof args[1] != 'undefined') {
fail(args[2] + 'expected undefined but was ' + prettyPrintEntity_(args[1]));
}
return true;
}
function assertNotUndefined(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
if (typeof args[1] == 'undefined') {
fail(args[0] + 'expected not undefined but was undefined');
}
return true;
}
function assertNaN(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
if (!isNaN(args[1])) {
fail(args[0] + 'expected to be NaN but was ' + args[1]);
}
return true;
}
function assertNotNaN(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
if (isNaN(args[1])) {
fail(args[0] + 'expected not to be NaN');
}
return true;
}
function assertException(msg, callback, error) {
if (arguments.length == 1) {
// assertThrows(callback)
callback = msg;
msg = '';
} else if (arguments.length == 2) {
if (typeof callback != 'function') {
// assertThrows(callback, type)
error = callback;
callback = msg;
msg = '';
} else {
// assertThrows(msg, callback)
msg += ' ';
}
} else {
// assertThrows(msg, callback, type)
msg += ' ';
}
jstestdriver.assertCount++;
try {
callback();
} catch(e) {
if (e.name == 'AssertError') {
throw e;
}
if (error && e.name != error) {
fail(msg + 'expected to throw ' + error + ' but threw ' + e.name);
}
return true;
}
fail(msg + 'expected to throw exception');
}
function assertNoException(msg, callback) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
try {
args[1]();
} catch(e) {
fail(args[0] + 'expected not to throw exception, but threw ' + e.name +
' (' + e.message + ')');
}
}
function assertArray(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
jstestdriver.assertCount++;
if (!jstestdriver.jQuery.isArray(args[1])) {
fail(args[0] + 'expected to be array, but was ' +
prettyPrintEntity_(args[1]));
}
}
function assertTypeOf(msg, expected, value) {
var args = argsWithOptionalMsg_(arguments, 3);
jstestdriver.assertCount++;
var actual = typeof args[2];
if (actual != args[1]) {
fail(args[0] + 'expected to be ' + args[1] + ' but was ' + actual);
}
return true;
}
function assertBoolean(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
return assertTypeOf(args[0], 'boolean', args[1]);
}
function assertFunction(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
return assertTypeOf(args[0], 'function', args[1]);
}
function assertObject(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
return assertTypeOf(args[0], 'object', args[1]);
}
function assertNumber(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
return assertTypeOf(args[0], 'number', args[1]);
}
function assertString(msg, actual) {
var args = argsWithOptionalMsg_(arguments, 2);
return assertTypeOf(args[0], 'string', args[1]);
}
function assertMatch(msg, regexp, actual) {
var args = argsWithOptionalMsg_(arguments, 3);
var isUndef = typeof args[2] == 'undefined';
jstestdriver.assertCount++;
var _undef;
if (isUndef || !args[1].test(args[2])) {
actual = (isUndef ? _undef : prettyPrintEntity_(args[2]));
fail(args[0] + 'expected ' + actual + ' to match ' + args[1]);
}
return true;
}
function assertNoMatch(msg, regexp, actual) {
var args = argsWithOptionalMsg_(arguments, 3);
jstestdriver.assertCount++;
if (args[1].test(args[2])) {
fail(args[0] + 'expected ' + prettyPrintEntity_(args[2]) +
' not to match ' + args[1]);
}
return true;
}
function assertTagName(msg, tagName, element) {
var args = argsWithOptionalMsg_(arguments, 3);
var actual = args[2] && args[2].tagName;
if (String(actual).toUpperCase() != args[1].toUpperCase()) {
fail(args[0] + 'expected tagName to be ' + args[1] + ' but was ' + actual);
}
return true;
}
function assertClassName(msg, className, element) {
var args = argsWithOptionalMsg_(arguments, 3);
var actual = args[2] && args[2].className;
var regexp = new RegExp('(^|\\s)' + args[1] + '(\\s|$)');
try {
assertMatch(args[0], regexp, actual);
} catch (e) {
actual = prettyPrintEntity_(actual);
fail(args[0] + 'expected class name to include ' +
prettyPrintEntity_(args[1]) + ' but was ' + actual);
}
return true;
}
function assertElementId(msg, id, element) {
var args = argsWithOptionalMsg_(arguments, 3);
var actual = args[2] && args[2].id;
jstestdriver.assertCount++;
if (actual !== args[1]) {
fail(args[0] + 'expected id to be ' + args[1] + ' but was ' + actual);
}
return true;
}
function assertInstanceOf(msg, constructor, actual) {
jstestdriver.assertCount++;
var args = argsWithOptionalMsg_(arguments, 3);
var pretty = prettyPrintEntity_(args[2]);
var expected = args[1] && args[1].name || args[1];
if (args[2] == null) {
fail(args[0] + 'expected ' + pretty + ' to be instance of ' + expected);
}
if (!(Object(args[2]) instanceof args[1])) {
fail(args[0] + 'expected ' + pretty + ' to be instance of ' + expected);
}
return true;
}
function assertNotInstanceOf(msg, constructor, actual) {
var args = argsWithOptionalMsg_(arguments, 3);
jstestdriver.assertCount++;
if (Object(args[2]) instanceof args[1]) {
var expected = args[1] && args[1].name || args[1];
var pretty = prettyPrintEntity_(args[2]);
fail(args[0] + 'expected ' + pretty + ' not to be instance of ' + expected);
}
return true;
}
/**
* Asserts that two doubles, or the elements of two arrays of doubles,
* are equal to within a positive delta.
*/
function assertEqualsDelta(msg, expected, actual, epsilon) {
var args = this.argsWithOptionalMsg_(arguments, 4);
jstestdriver.assertCount++;
msg = args[0];
expected = args[1];
actual = args[2];
epsilon = args[3];
if (!compareDelta_(expected, actual, epsilon)) {
this.fail(msg + 'expected ' + epsilon + ' within ' +
this.prettyPrintEntity_(expected) +
' but was ' + this.prettyPrintEntity_(actual) + '');
}
return true;
};
function compareDelta_(expected, actual, epsilon) {
var compareDouble = function(e,a,d) {
return Math.abs(e - a) <= d;
}
if (expected === actual) {
return true;
}
if (typeof expected == "number" ||
typeof actual == "number" ||
!expected || !actual) {
return compareDouble(expected, actual, epsilon);
}
if (isElement_(expected) || isElement_(actual)) {
return false;
}
var key = null;
var actualLength = 0;
var expectedLength = 0;
try {
// If an array is expected the length of actual should be simple to
// determine. If it is not it is undefined.
if (jstestdriver.jQuery.isArray(actual)) {
actualLength = actual.length;
} else {
// In case it is an object it is a little bit more complicated to
// get the length.
for (key in actual) {
if (actual.hasOwnProperty(key)) {
++actualLength;
}
}
}
// Arguments object
if (actualLength == 0 && typeof actual.length == "number") {
actualLength = actual.length;
for (var i = 0, l = actualLength; i < l; i++) {
if (!(i in actual)) {
actualLength = 0;
break;
}
}
}
for (key in expected) {
if (expected.hasOwnProperty(key)) {
if (!compareDelta_(expected[key], actual[key], epsilon)) {
return false;
}
++expectedLength;
}
}
if (expectedLength != actualLength) {
return false;
}
return expectedLength == 0 ? expected.toString() == actual.toString() : true;
} catch (e) {
return false;
}
};
var assert = assertTrue;