/**
* SJTest - version: see SJTest.version property
* @author Daniel Winterstein (http://winterstein.me.uk)
*
* Requires: nothing!
*
* Will use if present:
*
* - jQuery (or zepto)
* - Bootstrap
*
* Will create if absent:
*
* - assert() = SJTest.assert
* - match() = SJTest.match (a flexible matcher for easier testing)
* - assertMatch() = SJTest.assertMatch = assert(match())
* - isa() = SJTest.isa (like instanceof, but more robust)
* - waitFor() = SJTest.waitFor (polling based, handy for async tests, and elsewhere)
*
* Usage:
*
* - In the browser, must be switched on with SJTest.on = true; Or by adding SJTest=on to the url.
* - In PhantomJS: Run phantomjs SJTest.js MyTest1.html MyTest2.html
* - In Mocha: use assertMatch and match for joyful testing
* Documentation: http://winterstein.github.io/SJTest/
* Or see the code...
*/
// In mocha/node?
if (typeof window === 'undefined') window = global;
// *********************
// **** ATest ****
// *********************
/**
* @class ATest
* @param testName
* @param testFn
* This should throw something to fail. Or you can use assert();
*/
function ATest(testName, testFn) {
assertMatch(testName, String, testFn, Function);
this.name = testName;
this.fn = testFn;
this._status = 'queue';
this.stack = null;
/**
* If a passing test returns a value (e.g. some useful extra info) --
* then store it here.
*/
this.details = null;
this.error = false;
this._id = ++ATest._idCnt;
}
ATest._idCnt = 0;
// NB: Object.defineProperty doesn't work on IE7
/**
* @returns {string} queue|skip|running...|waiting|pass|fail
*/
ATest.prototype.getStatus = function() {return this._status;};
/**
* @param s {string} Must be one of: queue|skip|running...|waiting|pass|fail <br>
* You can use setStatus('waiting') within a test function to defer pass/fail. <br>
* You can then use setStatus('fail') within a test function to explicitly fail the test.
*/
ATest.prototype.setStatus = function(s) {
assert('|queue|skip|running...|waiting|pass|fail|'.indexOf(s) != -1, s);
this._status = s;
// Logging status provides a hook for the PhantomJS runner to watch
console.log(SJTest.LOGTAG+':'+this._status, this.name, this.details || this.stack || '');
};
/**
* @param waitForThis
* {?function} see SJTest.runTest()
* @param timeout
* {?number} milliseonds Defaults to 5,000
*/
ATest.prototype.run = function(waitForThis, timeout) {
var old_re = window.reportError;
try {
// Winterwell's assert() will normally swallow errors (outputting to
// log) -- But we want them thrown
window.reportError = function(err){throw err;};
this.setStatus('running...');
console.log(SJTest.LOGTAG, this.name, this.getStatus());
// Run the Test!
// NB: Pass in the ATest for reflection, though almost all tests will ignore it.
// The ATest is one way of doing async tests: E.g.
// function MyTest(test) {test.setStatus('waiting');
// $.get('myurl').done(function(){test.setStatus('pass');});
// }
this.details = this.fn(this);
// wait for async test?
var atest = this;
if ( ! waitForThis) {
if (this._status === 'waiting') {
waitForThis = function(){return atest._status==='pass' || atest._status==='fail';};
} else {
// All done -- no need to wait
this.setStatus('pass');
return;
}
}
// waitFor?
var testDoneFn = function(yes) {
// Passed (unless it failed itself)
if (atest._status !== 'fail') atest.setStatus('pass');
if (yes !== true) atest.details = yes;
assert(match(SJTest._displayTest, Function));
if (SJTest._displayTable) SJTest._displayTest(atest);
};
var timeoutFn = function() {
//console.log("TIMEOUT ATest.this", atest);
atest.error = new Error("Timeout");
atest.setStatus('fail');
assert(match(SJTest._displayTest, Function));
if (SJTest._displayTable) SJTest._displayTest(atest);
};
SJTest.waitFor(waitForThis, testDoneFn,
timeout || 10000, timeoutFn);
} catch(error) {
this.error = error;
if (error && error.stack) this.stack = error.stack;
this.setStatus('fail');
} finally {
window.reportError = old_re;
}
};
ATest.prototype.toString = function() {
return "ATest["+this.name+" "+this.getStatus()+"]";
};
// **********************
// **** SJTest ****
// **********************
/**
* Simple Javascript Testing (for browser-based code).
*
* Extend an existing object if present, to allow the user to put in settings.
* The default values below use ||s to let any user settings take precedence.
*
* @class SJTest
* @static
*/
var SJTest = SJTest || {};
/** What version of SJTest is this? */
SJTest.version = '0.3.4';
/**
* If true, isDone() will return false.
* Use-case: To avoid PhantomJS stopping early before after-page-load tests are setup,
* set true while loading & setting up tests, then you must set to false.
* @see SJTest.minTime
*/
SJTest.wait = SJTest.wait || false;
/**
* If true (the default), use inline styles to improve the standard
* display. Set to false if you want to take charge of styling yourself.
*/
if (SJTest.styling===undefined) SJTest.styling = true;
/**
* Used with all console.log output, for easy filtering.
*/
SJTest.LOGTAG = 'SJTest';
/**
* {Boolean} If off (the default), then SJTest will do nothing! Which lets you include tests in production code.
* Set by the url parameter SJTest=(on|a url), or it can be explicitly set in javascript.
* A javascript setting takes precedence over a url parameter.
* <p>
* NB: Even when off, SJTest will still define some functions, e.g. assertMatch() & isa().
*/
if (SJTest.on===undefined) {
var locn = ""+window.location;
var queryParser = /[?&]SJTest=([^&]+)?/;
var m = locn.match(queryParser);
if (m && m[1] && m[1]!=='false' && m[1]!=='off') {
SJTest.on = true;
} else {
SJTest.on = false;
}
}
/** true by default: Expose SJTest.assert() as a global function
* -- plus assertMatch(), isa(), waitFor(), match()
*/
if (SJTest.expose===undefined) SJTest.expose = true;
/**
* {Number} milliseconds (default: 100). isDone() will return false for at least this long.
* Use-case: To avoid PhantomJS stopping early before after-page-load tests are setup.
* @see SJTest.wait
*/
if (SJTest.minTime===undefined) SJTest.minTime = 100;
// Focus on certain tests?
if (SJTest.skip===undefined) SJTest.skip = [];
if (SJTest.only===undefined) SJTest.only = [];
SJTest.tests = [];
/**
* @param testSet
* {object} A set of test functions to run, e.g. { name:
* "MySimpleTests" MyTest1: function() { assert( 1+1 == 2); }
* MyTest2: function() { assert(match("aa", /[abc]+/)); } }
*
*/
SJTest.run = function(testSet) {
if ( ! SJTest.on) {
console.log(SJTest.LOGTAG, "NO run", testSet);
return;
}
console.log(SJTest.LOGTAG, "run", testSet);
assert(typeof testSet === 'object', testSet);
var setName = testSet.name || false;
for(var tName in testSet) {
if (tName=='name') continue;
var tFn = testSet[tName];
var fullName = setName? setName+"."+tName : tName;
SJTest.runTest(fullName, tFn);
}
}; // run
/** @ignore */
/* When did SJTest start? Used to determine minTime timeout. */
SJTest._started = new Date().getTime();
/**
* @returns {Boolean} true if all tests are run, and minTime has expired
*/
SJTest.isDone = function() {
//console.log("isDone?");
assert( ! SJTest.phantomjsTopLevel);
if (SJTest.wait) return false;
if (new Date().getTime() < SJTest._started + SJTest.minTime) {
//console.log("Wait!");
return false;
}
// Any running tests?
for(var i=0; i<SJTest.tests.length; i++) {
var test = SJTest.tests[i];
if (test.getStatus()!=='pass' && test.getStatus()!=='fail' && test.getStatus()!=='skip') {
return false;
}
}
//console.log("isDone! ", SJTest.tests.length);
return true;
};
/**
* @param testName
* {!String}
* @param testFn
* {?Function} If null, look for a test by this name.
* @param waitFor
* {?Function} A check for test-success which will be run
* periodically. Returns true when done (or a String, which
* will be reported as the test-details). Throw an error if
* the test fails.
* @param timeout
* {?Number} max milliseconds to allow. Default to 10000 (10 seconds)
*/
SJTest.runTest = function(testName, testFn, waitForThis, timeout) {
if ( ! SJTest.on) {
console.log(SJTest.LOGTAG, "NO runTest", testName);
return;
}
assertMatch(testName, String, testFn, "?Function", waitForThis, "?Function", timeout, "?Number");
console.log(SJTest.LOGTAG, "runTest", testName);
var dtest = false;
if (testFn) {
dtest = new ATest(testName, testFn);
dtest.waitForThis = waitForThis;
dtest.timeout = timeout;
} else {
// look for a test by name
for(var i=0; i<SJTest.tests.length; i++) {
var test = SJTest.tests[i];
if (test.name === testName) {
dtest = test;
waitForThis = dtest.waitForThis;
timeout = dtest.timeout;
break;
}
}
if ( ! dtest) {
// Fail -- bad name
dtest = new ATest(testName, function(){throw "Could not find test: "+testName;});
}
}
var skip = false;
if (testFn) { // don't skip "manual" run-by-name calls
// Skip if name begins //
if (testName.indexOf('//') != -1) {
skip = true;
}
if (SJTest.skip) {
if (SJTest.skip.indexOf(testName)!=-1) {
skip = true;
}
}
if (SJTest.only) {
for(var i=0; i<SJTest.only.length; i++) {
}
}
}
SJTest.tests.push(dtest);
// run!
if (!skip) {
dtest.run(waitForThis, timeout);
} else {
dtest.setStatus('skip');
}
// Result! display now?
if (SJTest._displayTable) SJTest._displayTest(dtest);
}; // runTestASync()
/**
* Called automatically. Adds a table of test results to the page. The table floats above the normal page, and can be closed.
*/
SJTest.display = function() {
if ( ! SJTest.on) {
//console.log(SJTest.LOGTAG, "Not on = no display");
return;
}
//console.log(SJTest.LOGTAG, "Display!");
var good=0,total=SJTest.tests.length;
for(var i=0; i<SJTest.tests.length; i++) {
var test = SJTest.tests[i];
if (test.getStatus()==='pass') good++;
}
/** @ignore */
/* The display DOM element */
SJTest._displayPanel = SJTestUtils.$getById("SJTestDisplay");
if ( ! SJTest._displayPanel || ! SJTest._displayPanel.length) {
//console.log(SJTest.LOGTAG, "Display Make it!");
SJTest._displayPanel = SJTestUtils.$create(
"<div id='SJTestDisplay' "
+(SJTest.styling? "style='z-index:100000;background:white;border:2px solid black;position:fixed;top:0px;right:0px;width:70%;overflow:auto;max-height:100%;'" : '')
+" class='SJTest panel panel-default'></div>");
SJTestUtils.$(document.body).append(SJTest._displayPanel);
} else {
// clear out old
SJTest._displayPanel.html("");
}
// Header
SJTest._displayPanel.append("<div class='panel-heading'><h2 class='panel-title'>"
+"Test Results: <span id='_SJTestGood'>"+good+"</span> / <span id='_SJTestTotal'>"+total+"</span>"
+"<button title='Close Tests' type='button' "+(SJTest.styling? "style='float:right;'":'')+" class='close' aria-hidden='true' onclick=\"$('#SJTestDisplay').remove();\">×</button>"
+"</h2></div>");
/** @ignore */
/* DOM table fo results */
SJTest._displayTable = SJTestUtils.$create("<table class='table table-bordered'></table>");
SJTest._displayTable.append("<tr><th></th><th>Name</th><th>Result</th><th>Details / Stack</th></tr>");
SJTest._displayPanel.append(SJTest._displayTable);
for(var i=0; i<SJTest.tests.length; i++) {
var test = SJTest.tests[i];
SJTest._displayTest(test);
}
}; // display()
/** @ignore */
/* Add a row to the display table
*
* @param test {ATest}
*/
SJTest._displayTest = function(test) {
assertMatch(test, ATest);
// Name includes a repeat button
var trid = "tr_SJTest_"+test._id;
var tr = SJTestUtils.$getById(trid);
if (!tr || ! tr.length) {
tr = SJTestUtils.$create("<tr id='"+trid+"'></tr>");
assert(tr);
SJTest._displayTable.append(tr);
//console.log('make it', trid);
} //else console.log('got it',trid);
if (SJTest.styling) {
var col = test.getStatus()==='pass'? '#9f9' : test.getStatus()==='skip'? '#ccf' : test.getStatus()==='running...'||test.getStatus()==='waiting'? '#ff9' : '#f99';
tr.css({border:'1px solid #333', 'background-color':col});
}
tr.html("<td><a href='#' title='Re-run this test' onclick='SJTest.runTest(\""+test.name+"\"); return false;'>↻</a></td><td>"
+test.name
+"</td><td>"+test.getStatus()+"</td><td>"+(test.error || SJTestUtils.str(test.details) || '-')
+" "+(test.stack || '')+"</td>");
// update scores
var good=0,total=SJTest.tests.length;
for(var i=0; i<SJTest.tests.length; i++) {
var test = SJTest.tests[i];
if (test.getStatus()==='pass') good++;
}
SJTestUtils.$getById('_SJTestGood').html(''+good);
SJTestUtils.$getById('_SJTestTotal').html(''+total);
};
/**
* An assert function.
* Error handling can be overridden by replacing SJTest.assertFailed()
* @param betrue
* If true (or any truthy value), do nothing. If falsy, console.error and throw an Error.
* HACK: As a special convenience, the empty jQuery result (ie a jquery select which find nothing) is considered to be false!
* For testing jQuery selections: Use e.g. $('#foo').length
* @param msg
* Message on error. This can be an object (which will be logged to console as-is,
* and converted to a string for the error).
* @returns betrue on success. This allows assert() to be used as a transparent wrapper.
* E.g. you might write <code>var x = assert(mything.propertyWhichMustExist);</code>
*/
SJTest.assert = function(betrue, msg) {
if (betrue) {
if (betrue.jquery && betrue.length===0) {
// empty jquery selection - treat as false
if ( ! msg) msg = "empty jquery selection";
SJTest.assertFailed(msg);
return;
}
// success
return betrue;
}
SJTest.assertFailed(msg || betrue);
};
/**
* Handle assert() failures. Users can replace this with a custom handler.
*/
SJTest.assertFailed = function(msg) {
console.error("assert", msg);
// A nice string?
var smsg = SJTestUtils.str(msg);
throw new Error("assert-failed: "+smsg);
};
/**
* Convenience for assert(match(value, matcher), value+" !~ "+matcher);
* Because it's a common use case.
* Arguments are alternating [airs of value, matcher (as many pairs as you like).
* E.g. assertMatch(myNumericValue, Number); or assertMatch(myNumericValue, Number, specificStringValue, "foo|bar");
*/
SJTest.assertMatch = function() {
SJTest.assert(arguments.length % 2 == 0, arguments);
for(var i=0; i<arguments.length; i+=2) {
var v = arguments[i];
var m = arguments[i+1];
SJTest.assert(SJTest.match(v, m), (arguments.length>2? ((i/2)+1)+') ':'')+ v +" !~ "+m);
}
};
/**
* Like instanceof, but more robust.
*
* @param obj
* Can be null/undefined (returns false)
* @param klass
* e.g. Number
* @returns {Boolean} true if obj is an example of klass.
*/
SJTest.isa = function(obj, klass) {
if (obj === klass) return true; // This can be too lenient, e.g. Number is not a Number. But it's generally correct for a prototype language.
if (obj instanceof klass) return true;
//assert(klass.constructor);
for(var i=0; i<10; i++) { // limit the recursion 10-deep for safety
if (obj === null || obj === undefined) return false;
if ( ! obj.constructor) return false;
if (obj.constructor == klass) return true;
obj = obj.prototype;
}
return false;
};
/** Flexible matching test
* @param value
* @param matcher Can be another value.
Or a Class.
Or a JSDoc-style class spec such as "?Number" or "Number|Function".
Or a regex (for matching against strings).
Or true/false (which match based on ifs semantics, e.g. '' matches false).
Or an object (which does partial matching, allowing value to have extra properties).
@returns true if value matches, false otherwise
*/
SJTest.match = function(value, matcher) {
// TODO refactor to be cleaner & recursive
// simple
if (value == matcher) return true;
var sValue = ""+value;
if (typeof matcher==='string') {
// JSDoc optional type? e.g. ?Number
if (matcher[0] === '?' && (value===null || value===undefined)) {
return true;
}
if (value===null || value===undefined) return false;
// Get the class function(s)
var ms = matcher.split("|");
for(var mi=0; mi<ms.length; mi++) {
var mArr = ms[mi].match(/^\??(\w+?)!?$/);
if ( ! mArr) break;
var m = mArr[1];
if (sValue===m) return true;
if (m==='Number'||m==='number') { // allow string to number conversion
if (typeof value === 'number' && ! isNaN(value)) return true;
var nv = parseFloat(value);
if (nv || nv===0) return true;
continue;
}
try {
// eval the class-name
var fn = new Function("return "+m);
var klass = fn();
if (SJTest.isa(value, klass)) {
return true;
}
} catch(err) {
// eval(m) failed to find a class
// A non-global ES6 class?
// Note: this is just for the string matcher. If the user put in a proper class object, we're fine.
var v = value;
while(true) {
if (v.constructor && v.constructor.name === m) {
return true;
}
v = Object.getPrototypeOf(v);
if ( ! v) break;
}
} // ./try class test
} // ./ for matcher-bit
return false;
} // string matcher
// lenient true/false
if(matcher===false && ! value) return true;
if (matcher===true && value) return true;
// RegExp?
if (matcher instanceof RegExp) {
try {
// var re = new RegExp("^"+matcher+"$"); // whole string match
var matched = matcher.test(sValue);
if (matched) return true;
} catch(ohwell) {}
}
var lazyMatcher = null;
if (matcher===Number) { // allow string to number conversion
if (typeof value === 'number' && ! isNaN(value)) return true;
var nv = parseFloat(value);
return (nv || nv===0);
}
if (typeof matcher==='function') {
// Class instanceof test
if (matcher.constructor /*
* fn + constructor => this is a class
* object, so _could_ be a prototype
*/) {
if (SJTest.isa(value, matcher)) {
return true;
} else {
return false;
}
} else {
// matcher is a function -- lazy value?
try {
lazyMatcher = matcher();
if (value==lazyMatcher) return true;
} catch(ohwell) {}
}
}
// Lazy value?
if (typeof value==='function') {
try {
var hardValue = value();
if (hardValue==matcher) return true;
// Both lazy?
if (lazyMatcher && hardValue==lazyMatcher) return true;
} catch(ohwell) {}
}
// partial object match? e.g. {a:1} matches {a:1, b:2}
if (typeof matcher==='object' && typeof value==='object') {
for(var p in matcher) {
var mv = matcher[p];
var vv = value[p];
if (mv != vv) {
return false;
}
}
return true;
}
return false;
};
/** array utility: remove by value. @return array */
SJTest.removeValue = function(item, array) {
var index = array.indexOf(item);
if (index==-1) return array;
array.splice(index, 1);
return array;
};
/** @ignore */
/* Queue */
SJTest._scriptsInProcessing = [];
/**
* @param url {String} Can be absolute or relative to the page.
* @param after {?Function} Optional callback to run after loading.
*/
SJTest.runScript = function(url, after) {
if ( ! SJTest.on) return;
console.log(SJTest.LOGTAG, 'runScript', url);
SJTest._scriptsInProcessing.push(url);
SJTestUtils.load(url, function() {
//console.log('runScript Loaded And Done', url);
SJTest.removeValue(url, SJTest._scriptsInProcessing);
if (after) after();
},
/* fail function */ function(err) {
SJTest.runTest("runScript", function() {
console.error(SJTest.LOGTAG,url,err);
throw "Could not load "+url+". See console for details.";
});
});
};
/**
* Use with SJTest=PathToMyScript in the page url.
* Call this to run a script (if there is one) specified by SJTest=path in the url parameters.
* <p>
* Note: This is restricted to relative urls for security. This may still have
* security implications. If you call this, you allow a potentially malicious link
* to run arbitrary scripts from the same domain in the page. So do not
* use if an attacker could place a script onto the same domain, or abuse
* one of yours.
*/
SJTest.runScriptFromUrl = function() {
// Is there a script in the url?
var locn = ""+window.location;
var queryParser = /[?&]SJTest=([^&]+)?/;
var m = locn.match(queryParser);
if ( ! m) return;
var script = m[1];
if ( ! script) return;
if ( ! SJTest.on) {
// Not on!
console.log(SJTest.LOGTAG, "NOT on, so not running script "+script);
return;
}
console.log(SJTest.LOGTAG, "runScriptFromUrl: "+script);
// Security check: must be a relative url
if (script.indexOf('//') != -1) {
console.warn(SJTest.LOGTAG, "NOT running. For security, you cannot run cross-domain test scripts.");
return;
}
SJTest.runScript(script);
};
/**
* Waitfor, adapted from http://blog.jeffscudder.com/2012/07/waitfor-javascript.html
* This does *not* block, but calls the callback when ready and any then/done/fail deferred functions.
* <p>
* Note: Why no blocking? Blocking is problematic given that normal javascript is single-threaded with switching.
* Usually you're waiting on an ajax request. Blocking would deny the ajax handler a chance to run. So you would
* block forever.
*
@param condition {Function} return true when ready. Errors are ignored.
@param callback {?Function} Called once condition is true.
@param timeout {?Number} Max time in milliseconds. If unset: wait indefinitely.
@param onTimeout {?Function} Called if timeout occurs.
@returns A jQuery deferred object (IF jQuery is present), so you can do waitFor(X).then(Y). null if no jQuery.
*
*/
SJTest.waitFor = function(condition, callback, timeout, onTimeout) {
SJTest.assertMatch(condition, Function, callback, "?Function", timeout, "?Number", onTimeout, "?Function");
var deferred = window.jQuery? new jQuery.Deferred() : null;
SJTest.waitFor.waitingFor.push([condition, callback, timeout? new Date().getTime()+timeout : false, onTimeout, deferred]);
SJTest.waitFor.check();
return deferred;
};
SJTest.waitFor.waitingFor = [];
/**
* {Number} Check every n milliseconds. Default: 50
*/
SJTest.waitFor.period = 50;
/**
* Check all the things we're waiting on. Schedule another check a bit later if we're still waiting.
*/
SJTest.waitFor.check = function() {
var stillWaitingFor = [];
for (var i = 0; i < SJTest.waitFor.waitingFor.length; i++) {
var row = SJTest.waitFor.waitingFor[i];
var condMet = false;
try {
condMet = row[0]();
} catch (e) {}
if (condMet) {
// Done! ...Callback
if (row[1]) row[1](condMet);
// ...Deferred
if (row[4]) row[4].resolve();
continue;
}
if (row[2] && new Date().getTime() > row[2]) {
// time out!
console.log("waitFor timeout "+row[0]);
if (row[3]) row[3]();
// deferred fail
if (row[4]) row[4].reject();
continue;
}
stillWaitingFor.push(SJTest.waitFor.waitingFor[i]);
}
SJTest.waitFor.waitingFor = stillWaitingFor;
if (stillWaitingFor.length > 0) {
//console.log("still waiting: ",stillWaitingFor);
setTimeout(SJTest.waitFor.check, SJTest.waitFor.period);
}
};
/**
* How many tests should we see? If less than this, then the page's tests are not yet done.
* Use-case: for async / delayed tests, to make sure they aren't skipped.
*
* This is itself a test (so you can see it pass/fail) -- but it is _not_ counted as one of the n.
*
* Note: Currently, this can only be called once per page.
*
* @param n {Number} How many tests does this page have?
* (excludes the expectTests one which this call will make)
* @param timeout {?Number} Milliseconds. Defaults to 10,000 (10 seconds)
*/
SJTest.expectTests = function(n, timeout) {
if ( ! SJTest.on) return;
assert( ! SJTest._expectTests, "Already expecting "+SJTest._expectTests+" "+n);
// Store n for possible reflection
/** @ignore */
/* How many tests do we expect? */
SJTest._expectTests = n;
if ( ! timeout) timeout = 10000;
SJTest.runTest("expectTests_"+n,
function(){},
function(){return SJTest.tests.length > n;}, timeout);
};
// ***************************
// **** SJTestUtils ****
// ***************************
/**
* Singleton for utility functions.
*/
var SJTestUtils = {
_initFlag: false
};
/** late-run to allow other polyfillers first shot */
SJTestUtils.init = function() {
if (SJTestUtils._initFlag) return;
SJTestUtils._initFlag = true;
// No JQuery!
if (window.$ === undefined) {
// TODO html() append() and dummy css() or other functions
}
// console
if ( ! window.console) {
// WTF? IE6? Oh well -- may as well play safe
window.console = {};
window.console.log = function(){};
window.console.error = function(){};
}
/**
* url {string}, callback {function}, fail {?Function} Only supported with jQuery
*/
SJTestUtils.load = function(url, callback, onFail) {
console.log(SJTest.LOGTAG, "loading...", url, callback);
if (window.$ && $.getScript) {
// Use jQuery if we can
console.log(SJTest.LOGTAG, "load by jQuery...", url, callback);
var gs = $.getScript(url, callback);
if (onFail) gs.fail(onFail);
return;
}
var oHead = document.getElementsByTagName('head')[0];
var oScript = document.createElement('script');
oScript.type = 'text/javascript';
oScript.src = url;
oScript.onload = callback;
oHead.appendChild(oScript);
};// load()
/**
* @param fn {function} Run once the document is loaded.
*/
SJTestUtils.onLoad = window.$;
if ( ! SJTestUtils.onLoad) {
SJTestUtils.onLoad = function(fn) {
setTimeout(fn,10); //hack: use a delay to avoid messing with window.onload behaviour
};
}
/**
* str -- Robust stringify. Use Winterwell's printer.str() if available. Else a simple version.
*/
if (window.printer && printer.str) {
SJTestUtils.str = printer.str;
} else {
SJTestUtils.str = function(obj) {
try {
var msg = JSON.stringify(obj);
return msg;
} catch(circularRefError) {
if (obj instanceof Array) {
var safe = [];
for(var i=0; i<obj.length; i++) {
safe[i] = SJTestUtils.str(obj[i]);
}
return JSON.stringify(safe);
}
// safety first
var safe = {};
for(var p in obj) {
var v = obj[p];
if (typeof(v) == 'function') continue;
else safe[p] = ""+v;
}
return JSON.stringify(safe);
}
};
} // str()
SJTestUtils.$ = function(thing) {
if ( ! thing) return thing;
if (window.$) return $(thing);
var $thing = {"$el": thing};
$thing.append = function(child) {
assert(child);
//console.log("append", thing, child);
return SJTestUtils.$append(thing, child);
};
$thing.html = function(html) {
if (html !== undefined) thing.innerHTML = html;
return thing.innerHTML;
};
$thing.length = 1;
$thing.css = function(){};
return $thing;
};
SJTestUtils.$getById = function(id) {
if (window.$) return $('#'+id);
assert(id);
return SJTestUtils.$(document.getElementById(id));
};
SJTestUtils.$create = function(html) {
if (window.$) return $(html);
assert(html);
//console.log("$create", html);
var el = document.createElement('div');
el.innerHTML = html;
var el2 = el.childNodes && el.childNodes[0]? el.childNodes[0] : el;
assert(el2, el);
return SJTestUtils.$(el2);
};
SJTestUtils.$append = function(element, child) {
if (window.$) return $(element).append(child);
assert(element);
assert(child);
if (typeof child === 'string') {
child = SJTestUtils.$create(child);
assert(child);
}
if (child.$el) child = child.$el;
assert(child);
console.log("$append", element, child);
element.appendChild(child);
};
// Make SJTest functions global??
if (SJTest.expose) {
// But don't override anything
if ( ! window.assert) {
window.assert = SJTest.assert;
}
if ( ! window.match) {
window.match = SJTest.match;
}
if ( ! window.waitFor) {
window.waitFor = SJTest.waitFor;
}
if ( ! window.assertMatch) {
window.assertMatch = SJTest.assertMatch;
}
if ( ! window.isa) {
window.isa = SJTest.isa;
}
if ( ! window.str) {
window.str = SJTestUtils.str;
}
}
// Run a script from a url request? No, it'd be a security hole :(
}; // ./ init
// / END FUNCTIONS *** START SCRIPT ///
// PhantomJS?
if (typeof(navigator)!=='undefined' && navigator.userAgent
&& navigator.userAgent.toLowerCase().indexOf("phantomjs")!=-1) {
// In Phantom -- but top level or inside a page?
if ( ! window.location.hostname &&
( ! window.location.pathname || window.location.pathname.length < 2 || window.location.pathname.substr(-'SJTest.js'.length)==='SJTest.js'))
{
SJTest.phantomjsTopLevel = true;
} else {
// disable display
SJTest.display = function(){};
SJTest._displayTest = function(){};
}
}
// Run the polyfill
SJTestUtils.init();
// Phantom Runner?
var SJTest4Phantom = {};
/**
* Go through the queue loading & running tests
*/
SJTest4Phantom._doThemAll = function() {
// if (SJTest.q.length==0) {
// setTimeout(SJTest._doThemAll(), 50);
// return;
// }
var url = SJTest4Phantom._pagesToLoad.pop();
var cback = function() {
SJTest4Phantom._doThemAll();
};
// console.log("url="+url+" from ", SJTest.q);
assert(url);
assert(SJTest.phantomjsTopLevel, SJTest);
var page = require('webpage').create();
// echo console messages
page.onConsoleMessage = function(msg){
console.log(msg); // filter by LOGTAG
var m = msg.match(/SJTest:(\S+) (.+)/);
if ( ! m) return;
var mcode=m[1], testName=m[2];
if (mcode==='pass') {
SJTest4Phantom.passed.push(testName);
} else if (mcode==='fail') {
SJTest4Phantom.failed.push(testName);
} else if (mcode==='skip') {
SJTest4Phantom.skipped.push(testName);
}
};
// Switch SJTest on!
if (url.indexOf('SJTest=')==-1) {
if (url.indexOf('?')!=-1) url += "&SJTest=on"; else url += "?SJTest=on";
}
page.open(url, cback);
SJTest4Phantom._pagesInProcessing.push(page);
console.log("PhantomJS opened: "+url+"...");
}; // ./ doThemAll
SJTest4Phantom.passed = [];
SJTest4Phantom.failed = [];
SJTest4Phantom.skipped = [];
SJTest4Phantom.goPhantom = function() {
var args = require('system').args;
if (args[0].substr( - 'SJTest.js'.length) === 'SJTest.js') {
SJTest.on = true;
} else {
console.warn("SJTest OFF: Did not recognise script "+args[0]);
}
if (args.length === 1) {
console.log('SJTest version '+SJTest.version+' by Daniel Winterstein');
console.log('Usage: phantomjs SJTest.js MyTestFile1.html MyTestFile2.html ...');
phantom.exit();
return;
}
// TODO support globs, e.g. test/*.js
// https://github.com/ariya/phantomjs/wiki/API-Reference-FileSystem
// https://github.com/ariya/phantomjs/blob/master/examples/echoToFile.js
for(var i=1; i<args.length; i++) {
var url = args[i];
console.log("PhantomJS queuing: "+url+"...");
SJTest4Phantom._pagesToLoad.push(url);
}
console.log("GO");
SJTest4Phantom._doThemAll();
SJTest.waitFor(SJTest4Phantom.isDoneTopLevel,
function() {
var p = SJTest4Phantom.passed.length, f = SJTest4Phantom.failed.length, s = SJTest4Phantom.skipped.length;
console.log("");
console.log(SJTest.LOGTAG, "Passed: "+SJTest4Phantom.passed);
console.log(SJTest.LOGTAG, "Failed: "+SJTest4Phantom.failed);
console.log("");
console.log(SJTest.LOGTAG, "Tests: "+(p+s+f)+"\tPassed: "+p+"\tSkipped: "+s+"\tFailed: "+f);
if (f==0) {
console.log(SJTest.LOGTAG, ":)");
phantom.exit();
} else {
console.log(SJTest.LOGTAG, ":(");
phantom.exit(1);
}
});
};
SJTest4Phantom.isDoneTopLevel = function() {
//console.log("isDoneTopLevel?");
assert(SJTest.phantomjsTopLevel);
// Are the pages done?
for(var i=0; i<SJTest4Phantom._pagesInProcessing.length; i++) {
var page = SJTest4Phantom._pagesInProcessing[i];
var done = page.evaluate(function() {
try {
return SJTest.isDone();
} catch(err) {
if ( ! window._SJTestStart) window._SJTestStart = new Date().getTime();
if (new Date().getTime() - window._SJTestStart < 10000) { // upto 10 seconds to handle setup messiness
return false;
}
console.log("SJTest:fail "+window.location+" "+err);
return true;
}
});
//console.log(SJTest.phantomjsTopLevel+" "+page.url+" done "+done);
if (done) {
//console.log("Remove page!",page);
SJTest.removeValue(page, SJTest4Phantom._pagesInProcessing);
try {page.close();} catch(err) {}
}
}
if (SJTest4Phantom._pagesToLoad.length > 0 || SJTest4Phantom._pagesInProcessing.length > 0) {
//console.log("Q", SJTest4Phantom._pagesToLoad, "Pages", SJTest4Phantom._pagesInProcessing);
return false;
}
//console.log("DoneTopLevel!");
return true;
};
/**
* Scripts and tests to load & run before we're done.
*/
SJTest4Phantom._pagesToLoad = [];
SJTest4Phantom._pagesInProcessing = [];
if ( ! SJTest.phantomjsTopLevel) {
// pause momentarily to allow SJTest.on to maybe be set manually
SJTestUtils.onLoad(function() {
setTimeout(SJTest.display, 1);
});
} else {
SJTest4Phantom.goPhantom();
}
// EXPORT
if (typeof(module)!=='undefined') {
module.exports = SJTest;
}
// }(_window, _module));
// end !SJTest wrapper function