/** * Pavlov - Test framework-independent behavioral API * * version 0.4.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.4.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); }());