/** * Pavlov - Test framework-independent behavioral API * * version 0.3.0pre * * http://github.com/mmonteleone/pavlov * * Copyright (c) 2009-2011 Michael Monteleone * Licensed under terms of the MIT License (README.markdown) */ (function ( global ) { // =========== // = Helpers = // =========== var util = { /** * Iterates over an object or array * @param {Object|Array} object object or array to iterate * @param {Function} callback callback for each iterated item */ each : function ( object, callback ) { if ( typeof object === 'undefined' || typeof callback === 'undefined' || object === null || callback === null ) { throw "both 'target' and 'callback' arguments are required"; } var name, i = 0, length = object.length, value; if ( length === undefined ) { for ( name in object ) { if ( object.hasOwnProperty( name ) ) { if ( callback.call( object[name], name, object[name] ) === false ) { break; } } } } else { for ( value = object[0]; i < length && callback.call( value, i, value ) !== false; value = object[++i] ) { } } return object; }, /** * converts an array-like object to an array * @param {Object} array array-like object * @returns array */ makeArray : function ( array ) { return Array.prototype.slice.call( array ); }, /** * returns whether or not an object is an array * @param {Object} obj object to test * @returns whether or not object is array */ isArray : function ( obj ) { return Object.prototype.toString.call( obj ) === "[object Array]"; }, /** * merges properties form one object to another * @param {Object} dest object to receive merged properties * @param {Object} src object containing properies to merge */ extend : function ( dest, src ) { if ( typeof dest === 'undefined' || typeof src === 'undefined' || dest === null || src === null ) { throw "both 'source' and 'target' arguments are required"; } var prop; for ( prop in src ) { if ( src.hasOwnProperty( prop ) ) { dest[prop] = src[prop]; } } }, /** * Naive display serializer for objects which wraps the objects' * own toString() value with type-specific delimiters. * [] for array * "" for string * Does not currently go nearly detailed enough for JSON use, * just enough to show small values within test results * @param {Object} obj object to serialize * @returns naive display-serialized string representation of the object */ serialize : function ( obj ) { if ( typeof obj === 'undefined' ) { return ""; } else if ( Object.prototype.toString.call( obj ) === "[object Array]" ) { return '[' + obj.toString() + ']'; } else if ( Object.prototype.toString.call( obj ) === "[object Function]" ) { return "function()"; } else if ( typeof obj === "string" ) { return '"' + obj + '"'; } else { return obj; } }, /** * transforms a camel or pascal case string * to all lower-case space-separated phrase * @param {string} value pascal or camel-cased string * @returns all-lower-case space-separated phrase */ phraseCase : function ( value ) { return value.replace( /([A-Z])/g, ' $1' ).toLowerCase(); } }; // ==================== // = Example Building = // ==================== var examples = [], currentExample, /** * Rolls up list of current and ancestors values for given prop name * @param {String} prop Name of property to roll up * @returns array of values corresponding to prop name */ rollup = function ( example, prop ) { var items = []; while ( example !== null ) { items.push( example[prop] ); example = example.parent; } return items; }; /** * Example Class * Represents an instance of an example (a describe) * contains references to parent and nested examples * exposes methods for returning combined lists of before, after, and names * @constructor * @param {example} parent example to append self as child to (optional) */ function Example( parent ) { if ( parent ) { // if there's a parent, append self as nested example this.parent = parent; this.parent.children.push( this ); } else { // otherwise, add this as a new root example examples.push( this ); } this.children = []; this.specs = []; } util.extend( Example.prototype, { name : '', // name of this description parent : null, // parent example children : [], // nested examples specs : [], // array of it() tests/specs before : function () { }, // called before all contained specs after : function () { }, // called after all contained specs /** * rolls up this and ancestor's before functions * @returns array of functions */ befores : function () { return rollup( this, 'before' ).reverse(); }, /** * Rolls up this and ancestor's after functions * @returns array of functions */ afters : function () { return rollup( this, 'after' ); }, /** * Rolls up this and ancestor's description names, joined * @returns string of joined description names */ names : function () { return rollup( this, 'name' ).reverse().join( ', ' ); } } ); // ============== // = Assertions = // ============== /** * AssertionHandler * represents instance of an assertion regarding a particular * actual value, and provides an api around asserting that value * against any of the bundled assertion handlers and custom ones. * @constructor * @param {Object} value A test-produced value to assert against */ function AssertionHandler( value ) { this.value = value; } /** * Appends assertion methods to the AssertionHandler prototype * For each provided assertion implementation, adds an identically named * assertion function to assertionHandler prototype which can run implementation * @param {Object} asserts Object containing assertion implementations */ var addAssertions = function ( asserts ) { util.each( asserts, function ( name, fn ) { AssertionHandler.prototype[name] = function () { // implement this handler against backend // by pre-pending AssertionHandler's current value to args var args = util.makeArray( arguments ); args.unshift( this.value ); // if no explicit message was given with the assertion, // then let's build our own friendly one if ( fn.length === 2 ) { args[1] = args[1] || 'asserting ' + util.serialize( args[0] ) + ' ' + util.phraseCase( name ); } else if ( fn.length === 3 ) { var expected = util.serialize( args[1] ); args[2] = args[2] || 'asserting ' + util.serialize( args[0] ) + ' ' + util.phraseCase( name ) + (expected ? ' ' + expected : expected); } fn.apply( this, args ); }; } ); }; /** * Add default assertions */ addAssertions( { equals : function ( actual, expected, message ) { adapter.assert( actual == expected, message ); }, isEqualTo : function ( actual, expected, message ) { adapter.assert( actual == expected, message ); }, isNotEqualTo : function ( actual, expected, message ) { adapter.assert( actual != expected, message ); }, isStrictlyEqualTo : function ( actual, expected, message ) { adapter.assert( actual === expected, message ); }, isNotStrictlyEqualTo : function ( actual, expected, message ) { adapter.assert( actual !== expected, message ); }, isTrue : function ( actual, message ) { adapter.assert( actual, message ); }, isFalse : function ( actual, message ) { adapter.assert( !actual, message ); }, isNull : function ( actual, message ) { adapter.assert( actual === null, message ); }, isNotNull : function ( actual, message ) { adapter.assert( actual !== null, message ); }, isDefined : function ( actual, message ) { adapter.assert( typeof actual !== 'undefined', message ); }, isUndefined : function ( actual, message ) { adapter.assert( typeof actual === 'undefined', message ); }, pass : function ( actual, message ) { adapter.assert( true, message ); }, fail : function ( actual, message ) { adapter.assert( false, message ); }, isFunction : function ( actual, message ) { return adapter.assert( typeof actual === "function", message ); }, isNotFunction : function ( actual, message ) { return adapter.assert( typeof actual !== "function", message ); }, throwsException : function ( actual, expectedErrorDescription, message ) { // can optionally accept expected error message try { actual(); adapter.assert( false, message ); } catch ( e ) { // so, this bit of weirdness is basically a way to allow for the fact // that the test may have specified a particular type of error to catch, or not. // and if not, e would always === e. adapter.assert( e === (expectedErrorDescription || e), message ); } } } ); // ===================== // = pavlov Public API = // ===================== /** * Object containing methods to be made available as public API */ var api = { /** * Initiates a new Example context * @param {String} description Name of what's being "described" * @param {Function} fn Function containing description (before, after, specs, nested examples) */ describe : function ( description, fn ) { if ( arguments.length < 2 ) { throw "both 'description' and 'fn' arguments are required"; } // capture reference to current example before construction var originalExample = currentExample; try { // create new current example for construction currentExample = new Example( currentExample ); currentExample.name = description; fn(); } finally { // restore original reference after construction currentExample = originalExample; } }, /** * Sets a function to occur before all contained specs and nested examples' specs * @param {Function} fn Function to be executed */ before : function ( fn ) { if ( arguments.length === 0 ) { throw "'fn' argument is required"; } currentExample.before = fn; }, /** * Sets a function to occur after all contained tests and nested examples' tests * @param {Function} fn Function to be executed */ after : function ( fn ) { if ( arguments.length === 0 ) { throw "'fn' argument is required"; } currentExample.after = fn; }, /** * Creates a spec (test) to occur within an example * When not passed fn, creates a spec-stubbing fn which asserts fail "Not Implemented" * @param {String} specification Description of what "it" "should do" * @param {Function} fn Function containing a test to assert that it does indeed do it (optional) */ it : function ( specification, fn ) { if ( arguments.length === 0 ) { throw "'specification' argument is required"; } if ( fn ) { if ( fn.async ) { specification += " asynchronously"; } currentExample.specs.push( [specification, fn] ); } else { // if not passed an implementation, create an implementation that simply asserts fail api.it( specification, function () { api.assert.fail( 'Not Implemented' ); } ); } }, /** * wraps a spec (test) implementation with an initial call to pause() the test runner * The spec must call resume() when ready * @param {Function} fn Function containing a test to assert that it does indeed do it (optional) */ async : function ( fn ) { var implementation = function () { adapter.pause(); fn.apply( this, arguments ); }; implementation.async = true; return implementation; }, /** * Generates a row spec for each argument passed, applying * each argument to a new call against the spec * @returns an object with an it() function for defining * function to be called for each of given's arguments * @param {Array} arguments either list of values or list of arrays of values */ given : function () { if ( arguments.length === 0 ) { throw "at least one argument is required"; } var args = util.makeArray( arguments ); if ( arguments.length === 1 && util.isArray( arguments[0] ) ) { args = args[0]; } return { /** * Defines a row spec (test) which is applied against each * of the given's arguments. */ it : function ( specification, fn ) { util.each( args, function () { var arg = this; api.it( "given " + arg + ", " + specification, function () { fn.apply( this, util.isArray( arg ) ? arg : [arg] ); } ); } ); } }; }, /** * Assert a value against any of the bundled or custom assertions * @param {Object} value A value to be asserted * @returns an AssertionHandler instance to fluently perform an assertion with */ assert : function ( value ) { return new AssertionHandler( value ); }, /** * specifies test runner to synchronously wait * @param {Number} ms Milliseconds to wait * @param {Function} fn Function to execute after ms has * passed before resuming */ wait : function ( ms, fn ) { if ( arguments.length < 2 ) { throw "both 'ms' and 'fn' arguments are required"; } adapter.pause(); global.setTimeout( function () { fn(); adapter.resume(); }, ms ); }, /** * specifies test framework to pause test runner */ pause : function () { adapter.pause(); }, /** * specifies test framework to resume test runner */ resume : function () { adapter.resume(); } }; // extend api's assert function for easier access to // parameter-less assert.pass() and assert.fail() calls util.each( ['pass', 'fail'], function ( i, method ) { api.assert[method] = function ( message ) { api.assert()[method]( message ); }; } ); /** * Extends a function's scope * applies the extra scope to the function returns un-run new version of fn * inspired by Yehuda Katz's metaprogramming Screw.Unit * different in that new function can still accept all parameters original function could * @param {Function} fn Target function for extending * @param {Object} thisArg Object for the function's "this" to refer * @param {Object} extraScope object whose members will be added to fn's scope * @returns Modified version of original function with extra scope. Can still * accept parameters of original function */ var extendScope = function ( fn, thisArg, extraScope ) { // get a string of the fn's parameters var params = fn.toString().match( /\(([^\)]*)\)/ )[1], // get a string of fn's body source = fn.toString().match( /^[^\{]*\{((.*\s*)*)\}/m )[1]; // create a new function with same parameters and // body wrapped in a with(extraScope) { } fn = new Function( "extraScope" + (params ? ", " + params : ""), "with(extraScope) {" + source + "}" ); // returns a fn wrapper which takes passed args, // pre-pends extraScope arg, and applies to modified fn return function () { var args = [extraScope]; util.each( arguments, function () { args.push( this ); } ); fn.apply( thisArg, args ); }; }; /** * Top-level Specify method. Declares a new pavlov context * @param {String} name Name of what's being specified * @param {Function} fn Function containing exmaples and specs */ var specify = function ( name, fn ) { if ( arguments.length < 2 ) { throw "both 'name' and 'fn' arguments are required"; } examples = []; currentExample = null; // set the test suite title name += " Specifications"; if ( typeof document !== 'undefined' ) { document.title = name + ' - Pavlov - ' + adapter.name; } // run the adapter initiation adapter.initiate( name ); if ( specify.globalApi ) { // if set to extend global api, // extend global api and run example builder util.extend( global, api ); fn(); } else { // otherwise, extend example builder's scope with api // and run example builder extendScope( fn, this, api )(); } // compile examples against the adapter and then run them adapter.compile( name, examples )(); }; // ==================================== // = Test Framework Adapter Interface = // ==================================== // abstracts functionality of underlying testing framework var adapter = { /** * adapter-specific initialization code * which is called once before any tests are run * @param {String} suiteName name of the pavlov suite name */ initiate : function ( suiteName ) { }, /** * adapter-specific assertion method * @param {bool} expr Boolean expression to assert against * @param {String} message message to pass along with assertion */ assert : function ( expr, message ) { throw "'assert' must be implemented by a test framework adapter"; }, /** * adapter-specific compilation method. Translates a nested set of * pre-constructed Pavlov example objects into a callable function which, when run * will execute the tests within the backend test framework * @param {String} suiteName name of overall test suite * @param {Array} examples Array of example object instances, possibly nesteds */ compile : function ( suiteName, examples ) { throw "'compile' must be implemented by a test framework adapter"; }, /** * adapter-specific pause method. When an adapter implements, * allows for its test runner to pause its execution */ pause : function () { throw "'pause' not implemented by current test framework adapter"; }, /** * adapter-specific resume method. When an adapter implements, * allows for its test runner to resume after a pause */ resume : function () { throw "'resume' not implemented by current test framework adapter"; } }; // ===================== // = Expose Public API = // ===================== // add global settings onto pavlov global.pavlov = { version : '0.3.0pre', specify : specify, adapter : adapter, adapt : function ( frameworkName, testFrameworkAdapter ) { if ( typeof frameworkName === "undefined" || typeof testFrameworkAdapter === "undefined" || frameworkName === null || testFrameworkAdapter === null ) { throw "both 'frameworkName' and 'testFrameworkAdapter' arguments are required"; } adapter.name = frameworkName; util.extend( adapter, testFrameworkAdapter ); }, util : { each : util.each, extend : util.extend }, api : api, globalApi : false, // when true, adds api to global scope extendAssertions : addAssertions // function for adding custom assertions }; }( window )); // ========================= // = Default QUnit Adapter = // ========================= (function () { if ( typeof QUnit === 'undefined' ) { return; } pavlov.adapt( "QUnit", { initiate : function ( name ) { var addEvent = function ( elem, type, fn ) { if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, fn ); } }; // after suite loads, set the header on the report page addEvent( window, 'load', function () { // document.getElementsByTag('h1').innerHTML = name; var h1s = document.getElementsByTagName( 'h1' ); if ( h1s.length > 0 ) { h1s[0].innerHTML = name; } } ); }, /** * Implements assert against QUnit's `ok` */ assert : function ( expr, msg ) { ok( expr, msg ); }, /** * Implements pause against QUnit's stop() */ pause : function () { stop(); }, /** * Implements resume against QUnit's start() */ resume : function () { start(); }, /** * Compiles nested set of examples into flat array of QUnit statements * returned bound up in a single callable function * @param {Array} examples Array of possibly nested Example instances * @returns function of which, when called, will execute all translated QUnit statements */ compile : function ( name, examples ) { var statements = [], each = pavlov.util.each; /** * Comples a single example and its children into QUnit statements * @param {Example} example Single example instance * possibly with nested instances */ var compileDescription = function ( example ) { // get before and after rollups var befores = example.befores(), afters = example.afters(); // create a module with setup and teardown // that executes all current befores/afters statements.push( function () { module( example.names(), { setup : function () { each( befores, function () { this(); } ); }, teardown : function () { each( afters, function () { this(); } ); } } ); } ); // create a test for each spec/"it" in the example each( example.specs, function () { var spec = this; statements.push( function () { test( spec[0], spec[1] ); } ); } ); // recurse through example's nested examples each( example.children, function () { compileDescription( this ); } ); }; // compile all root examples each( examples, function () { compileDescription( this, statements ); } ); // return a single function which, when called, // executes all qunit statements return function () { each( statements, function () { this(); } ); }; } } ); pavlov.extendAssertions( { /** * Asserts two objects are deeply equivalent, proxying QUnit's deepEqual assertion */ isSameAs : function ( actual, expected, message ) { deepEqual( actual, expected, message ); }, /* * Asserts two objects are deeply in-equivalent, proxying QUnit's notDeepEqual assertion */ isNotSameAs : function ( actual, expected, message ) { notDeepEqual( actual, expected, message ); } } ); // alias pavlov.specify as QUnit.specify for legacy support QUnit.specify = pavlov.specify; pavlov.util.extend( QUnit.specify, pavlov ); }());