diff --git a/README.md b/README.md index d538ab5..9ac9e71 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,25 @@ Postal.js is a JavaScript pub/sub library that can be used in the browser, or on the server-side using Node.js. It extends the "eventing" paradigm most JavaScript developers are already familiar with by providing an in-memory message bus to which your code/components/modules/etc can subscribe & publish. ## Why would I use it? -If you are looking to decouple the various components/libraries/plugins you use (client-or-server-side), applying messaging can enable you to not only easily separate concerns, but also enable you to more painlessly plug in additional components/functionality in the future. A pub/sub library like Postal.js can assist you in picking & choosing the libraries that best address the problems you're trying to solve, without burdening you with the requirement that those libraries have to be natively interoperable. For example: +If you are looking to decouple the various components/libraries/plugins you use (client-or-server-side), applying messaging can enable you to not only easily separate concerns, but also enable you to more painlessly plug in additional components/functionality in the future. A pub/sub library like postal.js can assist you in picking & choosing the libraries that best address the problems you're trying to solve, without burdening you with the requirement that those libraries have to be natively interoperable. For example: * If you're using a client-side binding framework, and either don't have - or don't like - the request/communication abstractions provided, then grab a library like [amplify.js](http://amplifyjs.com) or [reqwest](https://github.com/ded/reqwest). Then, instead of tightly coupling the two, have the request success/error callbacks publish messages with the appropriate data and any subscribers you've wired up can handle applying the data to the specific objects/elements they're concerned with. * Do you need two view models to communicate, but you don't want them to need to know about each other? Have them subscribe to the topics about which they are interested in receiving messages. From there, whenever a view model needs to alert any listeners of specific data/events, just publish a message to the bus. If the other view model is present, it will receive the notification. * Want to wire up your own binding framework? Want to control the number of times subscription callbacks get invoked within a given time frame? Want to keep subscriptions from being fired until after data stops arriving? Want to keep events from being acted upon until the UI event loop is done processing other events? These - and more - are all things Postal can do for you. -## Wut? Another pub/sub library? -Why, yes. There are great alternatives to Postal. If you need something leaner for client-side development, look at amplify.js. If you're in Node.js and can get by with EventEmitter, great. However, I discovered that as my needs quickly grew, I wanted something that was as lean as possible, without sacrificing some of the more complex functionality that's not provided by libraries like amplify.js, and the EventEmitter object in Node. +## Philosophy +Postal.js is in good company - there are many options for pub/sub in the browser. However, I grew frustrated with most of them because they often closely followed a DOM-eventing-paradigm, instead of providing a more substantial in-memory message bus. Central to postal.js are two things: + +* channels +* hierarchical topics (which allow plan string or wildcard bindings) + +### Channels? WAT? +A channel is a logical partition of topics. Conceptually, it's like a dedicated highway for a specific set of communication. At first glance it might seem like that's overkill for an environment that runs in an event loop, but it actually proves to be quite useful. Every library has architectural opinions that it either imposes or nudges you toward. Channel-oriented messaging nudges you to separate your communication by bounded context, and enables you the kind of fine-tuned visibility you need into the interactions between components as your application grows. + +### Hierarchical Topics +In my experience, seeing publish and subscribe calls all over application logic is usually strong code smell. Ideally, the majority of message-bus integration should be concealed within app infrastructure. Have a hierarchical-wildcard-bindable topic system makes it very easy to keep things concise (especially subscribe calls!). For example, if you have a module that needs to listen to ever message published on the ShoppingCart channel, you'd simply subscribe to "*", and never have to worry about additional subscribes on that channel again - even if you add new messages in the future. If you need to capture all messages with ".validation" at the end of the topic, you'd simply subscribe to "*.validation". ## How do I use it? -In a nutshell, Postal provides an in-memory message bus, where clients subscribe to a topic (which can include wildcards, as we'll see), and publishers publish messages (passing a topic along with it). Postal's "bindingResolver" handles matching a published message's topic to subscribers who should be notified of the message. When a client subscribes, they pass a callback that should be invoked whenever a message comes through. This callback takes one argument - the "data" payload of the message. (Messages do not *have* to include data - they can simply be used to indicate an event, and not transmit additional state). Additional options/constraints can be set on a subscription (see examples below, and check out the fluent calls available on the SubscriptionDefinition prototype). Here are four examples of using Postal. All of these examples - AND MORE! - can be run live here: [http://jsfiddle.net/ifandelse/FdFM3/](http://jsfiddle.net/ifandelse/FdFM3/) @@ -111,8 +119,9 @@ dupSubscription.unsubscribe(); ``` ## How can I extend it? -There are two main ways you can extend Postal: +There are three main ways you can extend Postal: +* Write a plugin. Need more complex behavior that the built-in SubscriptionDefinition doesn't offer? Write a plugin that you can attach to the global postal object. See [postal.when]() for an example of how to do this. * First, you can write an entirely new bus implementation (want to tie into a real broker like RabbitMQ by hitting the [experimental] JSON RPC endpoints and wrap it with Postal's API? This is how you'd do it.). If you want to do this, look over the `localBus` implementation to see how the core version works. Then, you can simply swap the bus implementation out by calling: `postal.configuration.bus = myWayBetterBusImplementation`. * The second way you can extend Postal is to change how the `bindingResolver` works. You may not care for the RabbitMQ-style bindings functionality. No problem! Write your own resolver object that implements a `compare` method and swap the core version out with your implementation by calling: `postal.configuration.resolver = myWayBetterResolver`. diff --git a/example/amd/index.html b/example/amd/index.html index 2c014bc..26bb8a4 100644 --- a/example/amd/index.html +++ b/example/amd/index.html @@ -33,23 +33,18 @@
- Example 6 - using whenHandledThenExecute(X) + Example 6 - using withConstraint() to apply a predicate to subscription callback
- Example 7 - using withConstraint() to apply a predicate to subscription callback + Example 7 - using withContext to set the "this" context
- Example 8 - using withContext to set the "this" context + Example 8 - using withDelay to delay evaluation of subscription
- -
- Example 9 - using withDelay to delay evaluation of subscription - -
\ No newline at end of file diff --git a/example/amd/js/examples.js b/example/amd/js/examples.js index 48ec40d..ea146b6 100644 --- a/example/amd/js/examples.js +++ b/example/amd/js/examples.js @@ -77,22 +77,11 @@ define(['postal', 'postaldiags'], function(postal, diags){ .publish({ value:"Donna Noble has left the library." }); daSubscription.unsubscribe(); - // Using whenHandledThenExecute() to invoke a function after handling a message - var whteChannel = postal.channel("Donna.Noble.*"), - whteSubscription = whteChannel.subscribe(function(data) { - $('
  • ' + data.value + '
  • ').appendTo("#example6"); - }).whenHandledThenExecute(function() { - $('
  • [Kind of a frivolous example...but this line resulted from the whenHandledThenExecute() callback]
  • ').appendTo("#example6"); - }); - postal.channel("Donna.Noble.*") - .publish({ value:"Donna Noble has left the library." }); - whteSubscription.unsubscribe(); - // Using withConstraint to apply a predicate to the subscription var drIsInTheTardis = false, wcChannel = postal.channel("Tardis.Depart"), wcSubscription = wcChannel.subscribe(function(data) { - $('
  • ' + data.value + '
  • ').appendTo("#example7"); + $('
  • ' + data.value + '
  • ').appendTo("#example6"); }).withConstraint(function() { return drIsInTheTardis; } ); postal.channel("Tardis.Depart") .publish({ value:"Time for time travel....fantastic!" }); @@ -107,7 +96,7 @@ define(['postal', 'postaldiags'], function(postal, diags){ var ctxChannel = postal.channel("Dalek.Meet.CyberMen"), ctxSubscription = ctxChannel.subscribe(function(data) { $('
  • ' + data.value + '
  • ').appendTo(this); - }).withContext($("#example8")); + }).withContext($("#example7")); postal.channel("Dalek.Meet.CyberMen") .publish({ value:"Exterminate!" }); postal.channel("Dalek.Meet.CyberMen") @@ -117,7 +106,7 @@ define(['postal', 'postaldiags'], function(postal, diags){ // Using withDelay() to delay the subscription evaluation var wdChannel = postal.channel("He.Will.Knock.Four.Times"), wdSubscription = wdChannel.subscribe(function(data) { - $('
  • ' + data.value + '
  • ').appendTo($("#example9")); + $('
  • ' + data.value + '
  • ').appendTo($("#example8")); }).withDelay(5000); postal.channel("He.Will.Knock.Four.Times") .publish({ value:"Knock!" }); diff --git a/example/amd/js/libs/postal/postal.js b/example/amd/js/libs/postal/postal.js index 65a6ed0..42e37e7 100644 --- a/example/amd/js/libs/postal/postal.js +++ b/example/amd/js/libs/postal/postal.js @@ -147,14 +147,6 @@ SubscriptionDefinition.prototype = { return this; }, - whenHandledThenExecute: function(callback) { - if(! _.isFunction(callback)) { - throw "Value provided to 'whenHandledThenExecute' must be a function"; - } - this.onHandled = callback; - return this; - }, - withConstraint: function(predicate) { if(! _.isFunction(predicate)) { throw "Predicate constraint must be a function"; @@ -191,7 +183,9 @@ SubscriptionDefinition.prototype = { } var fn = this.callback; this.callback = function(data) { - setTimeout(fn, milliseconds, data); + setTimeout(function(){ + fn(data); + }, milliseconds); }; return this; }, @@ -405,6 +399,17 @@ var postal = { }); }); return result; + }, + + reset: function() { + // we check first in case a custom bus or resolver + // doesn't expose subscriptions or a resolver cache + if(postal.configuration.bus.subscriptions) { + postal.configuration.bus.subscriptions = {}; + } + if(postal.configuration.resolver.cache) { + postal.configuration.resolver.cache = {}; + } } }; diff --git a/example/standard/index.html b/example/standard/index.html index a3fcfe0..1319733 100644 --- a/example/standard/index.html +++ b/example/standard/index.html @@ -37,23 +37,18 @@
    - Example 6 - using whenHandledThenExecute(X) + Example 6 - using withConstraint() to apply a predicate to subscription callback
    - Example 7 - using withConstraint() to apply a predicate to subscription callback + Example 7 - using withContext to set the "this" context
    - Example 8 - using withContext to set the "this" context + Example 8 - using withDelay to delay evaluation of subscription
    - -
    - Example 9 - using withDelay to delay evaluation of subscription - -
    \ No newline at end of file diff --git a/example/standard/js/main.js b/example/standard/js/main.js index c1e431c..0e994c9 100644 --- a/example/standard/js/main.js +++ b/example/standard/js/main.js @@ -77,22 +77,11 @@ $(function(){ .publish({ value:"Donna Noble has left the library." }); daSubscription.unsubscribe(); - // Using whenHandledThenExecute() to invoke a function after handling a message - var whteChannel = postal.channel("Donna.Noble.*"), - whteSubscription = whteChannel.subscribe(function(data) { - $('
  • ' + data.value + '
  • ').appendTo("#example6"); - }).whenHandledThenExecute(function() { - $('
  • [Kind of a frivolous example...but this line resulted from the whenHandledThenExecute() callback]
  • ').appendTo("#example6"); - }); - postal.channel("Donna.Noble.*") - .publish({ value:"Donna Noble has left the library." }); - whteSubscription.unsubscribe(); - // Using withConstraint to apply a predicate to the subscription var drIsInTheTardis = false, wcChannel = postal.channel("Tardis.Depart"), wcSubscription = wcChannel.subscribe(function(data) { - $('
  • ' + data.value + '
  • ').appendTo("#example7"); + $('
  • ' + data.value + '
  • ').appendTo("#example6"); }).withConstraint(function() { return drIsInTheTardis; } ); postal.channel("Tardis.Depart") .publish({ value:"Time for time travel....fantastic!" }); @@ -107,7 +96,7 @@ $(function(){ var ctxChannel = postal.channel("Dalek.Meet.CyberMen"), ctxSubscription = ctxChannel.subscribe(function(data) { $('
  • ' + data.value + '
  • ').appendTo(this); - }).withContext($("#example8")); + }).withContext($("#example7")); postal.channel("Dalek.Meet.CyberMen") .publish({ value:"Exterminate!" }); postal.channel("Dalek.Meet.CyberMen") @@ -117,7 +106,7 @@ $(function(){ // Using withDelay() to delay the subscription evaluation var wdChannel = postal.channel("He.Will.Knock.Four.Times"), wdSubscription = wdChannel.subscribe(function(data) { - $('
  • ' + data.value + '
  • ').appendTo($("#example9")); + $('
  • ' + data.value + '
  • ').appendTo($("#example8")); }).withDelay(5000); postal.channel("He.Will.Knock.Four.Times") .publish({ value:"Knock!" }); diff --git a/example/standard/js/postal.js b/example/standard/js/postal.js index 65a6ed0..42e37e7 100644 --- a/example/standard/js/postal.js +++ b/example/standard/js/postal.js @@ -147,14 +147,6 @@ SubscriptionDefinition.prototype = { return this; }, - whenHandledThenExecute: function(callback) { - if(! _.isFunction(callback)) { - throw "Value provided to 'whenHandledThenExecute' must be a function"; - } - this.onHandled = callback; - return this; - }, - withConstraint: function(predicate) { if(! _.isFunction(predicate)) { throw "Predicate constraint must be a function"; @@ -191,7 +183,9 @@ SubscriptionDefinition.prototype = { } var fn = this.callback; this.callback = function(data) { - setTimeout(fn, milliseconds, data); + setTimeout(function(){ + fn(data); + }, milliseconds); }; return this; }, @@ -405,6 +399,17 @@ var postal = { }); }); return result; + }, + + reset: function() { + // we check first in case a custom bus or resolver + // doesn't expose subscriptions or a resolver cache + if(postal.configuration.bus.subscriptions) { + postal.configuration.bus.subscriptions = {}; + } + if(postal.configuration.resolver.cache) { + postal.configuration.resolver.cache = {}; + } } }; diff --git a/ext/pavlov.js b/ext/pavlov.js old mode 100644 new mode 100755 index a68ff3f..139472e --- a/ext/pavlov.js +++ b/ext/pavlov.js @@ -1,105 +1,139 @@ /** - * Pavlov - Behavioral API over QUnit - * - * version 0.2.3 - * - * http://michaelmonteleone.net/projects/pavlov + * Pavlov - Test framework-independent behavioral API + * + * version 0.3.0pre + * * http://github.com/mmonteleone/pavlov * - * Copyright (c) 2009 Michael Monteleone + * Copyright (c) 2009-2011 Michael Monteleone * Licensed under terms of the MIT License (README.markdown) */ -(function(){ - // capture reference to global scope - var globalScope = this; - +(function (global) { + // =========== // = Helpers = // =========== - - // Trimmed versions of jQuery helpers for use only within pavlov - - /** - * Iterates over an object or array - * @param {Object|Array} object object or array to iterate - * @param {Function} callback callback for each iterated item - */ - var each = function(object, callback) { - var name; - var i = 0; - var length = object.length; - if ( length === undefined ) { - for ( name in object ) { - if ( callback.call( object[ name ], name, object[ name ] ) === false ) { - break; + 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]) { } } - } else { - for ( var 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 - */ - var makeArray = function(array) { - var ret = []; - - var i = array.length; - while( i ) { ret[--i] = array[i]; } - - return ret; - }; - - /** - * returns whether or not an object is an array - * @param {Object} obj object to test - * @returns whether or not object is array - */ - var 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 - */ - var extend = function(dest, src) { - for(var prop in src) { - dest[prop] = src[prop]; - } - }; - - /** - * minimalist (and yes, non-optimal/leaky) event binder - * not meant for wide use. only for jquery-less internal use in pavlov - * @param {Element} elem Event-triggering Element - * @param {String} type name of event - * @param {Function} fn callback - */ - var addEvent = function(elem, type, fn){ - if ( elem.addEventListener ) { - elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, fn ); + 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 = []; - var currentExample; + 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 @@ -108,172 +142,162 @@ * 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) { - // private - - if(parent) { + */ + function Example(parent) { + if (parent) { // if there's a parent, append self as nested example - parent.children.push(this); + this.parent = parent; + this.parent.children.push(this); } else { // otherwise, add this as a new root example examples.push(this); } - var thisExample = this; - - /** - * 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 - */ - var rollup = function(prop) { - var items = []; - var node = thisExample; - while(node !== null) { - items.push(node[prop]); - node = node.parent; - } - return items; - }; - - // public - - // parent example - this.parent = parent ? parent : null; - // nested examples this.children = []; - // name of this description - this.name = ''; - // function to happen before all contained specs - this.before = function() {}; - // function to happen after all contained specs - this.after = function() {}; - // array of it() tests 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 arrayt of functions + * @returns array of functions */ - this.befores = function(){ - return rollup('before').reverse(); - }; + befores: function () { + return rollup(this, 'before').reverse(); + }, /** * Rolls up this and ancestor's after functions * @returns array of functions */ - this.afters = function(){ - return rollup('after'); - }; + afters: function () { + return rollup(this, 'after'); + }, /** - * Rolls up this and ancestor's description names, joined + * Rolls up this and ancestor's description names, joined * @returns string of joined description names */ - this.names = function(){ - return rollup('name').reverse().join(', '); - }; - } - + names: function () { + return rollup(this, 'name').reverse().join(', '); + } + }); + // ============== // = Assertions = // ============== - - /** - * Collection of default-bundled assertion implementations - */ - var assertions = { - equals: function(actual, expected, message) { - equals(actual, expected, message); - }, - isEqualTo: function(actual, expected, message) { - equals(actual, expected, message); - }, - isNotEqualTo: function(actual, expected, message) { - ok(actual !== expected, message); - }, - isSameAs: function(actual, expected, message) { - same(actual, expected, message); - }, - isNotSameAs: function(actual, expected, message) { - ok(!QUnit.equiv(actual, expected), message); - }, - isTrue: function(actual, message) { - ok(actual, message); - }, - isFalse: function(actual, message) { - ok(!actual, message); - }, - isNull: function(actual, message) { - ok(actual === null, message); - }, - isNotNull: function(actual, message) { - ok(actual !== null, message); - }, - isDefined: function(actual, message) { - ok(typeof(actual) !== 'undefined', message); - }, - isUndefined: function(actual, message) { - ok(typeof(actual) === 'undefined', message); - }, - pass: function(actual, message) { - ok(true, message); - }, - fail: function(actual, message) { - ok(!true, message); - }, - throwsException: function(actual, expectedErrorDescription, message) { - /* can optionally accept expected error message */ - try{ - actual(); - ok(!true, message); - } catch(e) { - if(arguments.length > 1) { - ok(e === expectedErrorDescription, message); - } else { - ok(true, message); - } - } - } - }; /** * AssertionHandler - * represents instance of an assertion regarding a particular + * 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 */ - var assertHandler = function(value) { + function AssertionHandler(value) { this.value = value; - }; + } + /** - * Appends assertion methods to the assertHandler prototype + * Appends assertion methods to the AssertionHandler prototype * For each provided assertion implementation, adds an identically named - * assertion function to assertionHandler prototype which can run impl + * assertion function to assertionHandler prototype which can run implementation * @param {Object} asserts Object containing assertion implementations */ - var addAssertions = function(asserts) { - each(asserts, function(name, fn){ - assertHandler.prototype[name] = function() { + var addAssertions = function (asserts) { + util.each(asserts, function (name, fn) { + AssertionHandler.prototype[name] = function () { // implement this handler against backend - // by pre-pending assertHandler's current value to args - var args = makeArray(arguments); - args.unshift(this.value); + // 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); }; - }); + }); }; - // pre-add all the default bundled assertions - addAssertions(assertions); + /** + * 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 = + // = pavlov Public API = // ===================== @@ -286,138 +310,160 @@ * @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"); + 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{ + try { // create new current example for construction - currentExample = new example(currentExample); + currentExample = new Example(currentExample); currentExample.name = description; - fn(); + 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 + * @param {Function} fn Function to be executed */ - before: function(fn) { - if(arguments.length === 0) { - throw("'fn' argument is required"); + 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 + * @param {Function} fn Function to be executed */ - after: function(fn) { - if(arguments.length === 0) { - throw("'fn' argument is required"); + 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"); + it: function (specification, fn) { + if (arguments.length === 0) { + throw "'specification' argument is required"; } - thisApi = this; - if(fn) { + 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 - thisApi.it(specification, function(){thisApi.assert.fail('Not Implemented');}); + api.it(specification, function () {api.assert.fail('Not Implemented');}); } }, /** - * Generates a row spec for each argument passed, applying + * 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 + * @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"); + 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]; } - var args = makeArray(arguments); - var thisIt = this.it; return { /** * Defines a row spec (test) which is applied against each * of the given's arguments. */ - it: function(specification, fn) { - each(args, function(){ - var arg = this; - thisIt("given " + arg + ", " + specification, function(){ - fn.apply(this, isArray(arg) ? arg : [arg]); + 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 assertHandler instance to fluently perform an assertion with + * @returns an AssertionHandler instance to fluently perform an assertion with */ - assert: function(value) { - return new assertHandler(value); + 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 + * @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"); + wait: function (ms, fn) { + if (arguments.length < 2) { + throw "both 'ms' and 'fn' arguments are required"; } - stop(); - QUnit.specify.globalObject.setTimeout(function(){ + adapter.pause(); + global.setTimeout(function () { fn(); - start(); + 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 syntax for blank pass and fail - extend(api.assert, { - /** - * Shortcuts assert().pass() with assert.pass() - * @param {String} message Assertion message (optional) - */ - pass: function(message){ - (new assertHandler()).pass(message); - }, - /** - * Shortcuts assert().fail() with assert.fail() - * @param {String} message Assertion message (optional) - */ - fail: function(message){ - (new assertHandler()).fail(message); - } + // 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); + }; }); /** @@ -428,27 +474,27 @@ * @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 + * @returns Modified version of original function with extra scope. Can still * accept parameters of original function */ - var extendScope = function(fn, thisArg, extraScope) { + var extendScope = function (fn, thisArg, extraScope) { // get a string of the fn's parameters - var params = fn.toString().match(/\(([^\)]*)\)/)[1]; + var params = fn.toString().match(/\(([^\)]*)\)/)[1], // get a string of fn's body - var source = fn.toString().match(/^[^\{]*\{((.*\s*)*)\}/m)[1]; + 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, + // 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(){ + return function () { var args = [extraScope]; - each(arguments,function(){ + util.each(arguments,function () { args.push(this); }); fn.apply(thisArg, args); @@ -456,121 +502,239 @@ }; /** - * Top-level Specify method. Declares a new QUnit.specify context + * 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 + * @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") + 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 - document.title = name + " Specifications"; - addEvent(window,'load',function(){ - // document.getElementsByTag('h1').innerHTML = name; - var h1s = document.getElementsByTagName('h1'); - if(h1s.length > 0) - h1s[0].innerHTML = document.title; - }); - - if(QUnit.specify.globalApi) { - // if set to extend global api, - // extend global api and run example builder - extend(globalScope, api); - fn(); - } else { - // otherwise, extend example builder's scope with api - // and run example builder - extendScope(fn, this, api)(); + name += " Specifications"; + if (typeof document !== 'undefined') { + document.title = name + ' - Pavlov - ' + adapter.name; } - // compile examples into flat qunit statements - var qunitStatements = compile(examples); + // run the adapter initiation + adapter.initiate(name); - // run qunit tests - each(qunitStatements, function(){ this(); }); - }; + 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)(); + } - - - - // ========================================== - // = Example-to-QUnit Statement Compilation = - // ========================================== - - /** - * Compiles nested set of examples into flat array of QUnit statements - * @param {Array} examples Array of possibly nested Example instances - * @returns array of QUnit statements each wrapped in an anonymous fn - */ - var compile = function(examples) { - var statements = []; - - /** - * 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(); - var 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 statements; + // 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 = // ===================== - // extend QUnit - QUnit.specify = specify; - // add global settings onto QUnit.specify - extend(specify, { - version: '0.2.3', + // 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 - globalObject: window // injectable global containing setTimeout and pals + 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); +}()); diff --git a/ext/qunit.css b/ext/qunit.css index 5714bf4..2685391 100644 --- a/ext/qunit.css +++ b/ext/qunit.css @@ -1,119 +1,228 @@ +/** + * QUnit v1.3.0pre - A JavaScript Unit Testing Framework + * + * http://docs.jquery.com/QUnit + * + * Copyright (c) 2011 John Resig, Jörn Zaefferer + * Dual licensed under the MIT (MIT-LICENSE.txt) + * or GPL (GPL-LICENSE.txt) licenses. + * Pulled Live from Git Sat Feb 11 19:20:01 UTC 2012 + * Last Commit: 0712230bb203c262211649b32bd712ec7df5f857 + */ -ol#qunit-tests { - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - margin:0; - padding:0; - list-style-position:inside; +/** Font Family and Sizes */ - font-size: smaller; +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; } -ol#qunit-tests li{ - padding:0.4em 0.5em 0.4em 2.5em; - border-bottom:1px solid #fff; - font-size:small; - list-style-position:inside; + +#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { + margin: 0; + padding: 0; } -ol#qunit-tests li ol{ + + +/** Header */ + +#qunit-header { + padding: 0.5em 0 0.5em 1em; + + color: #8699a4; + background-color: #0d3349; + + font-size: 1.5em; + line-height: 1em; + font-weight: normal; + + border-radius: 15px 15px 0 0; + -moz-border-radius: 15px 15px 0 0; + -webkit-border-top-right-radius: 15px; + -webkit-border-top-left-radius: 15px; +} + +#qunit-header a { + text-decoration: none; + color: #c2ccd1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #fff; +} + +#qunit-banner { + height: 5px; +} + +#qunit-testrunner-toolbar { + padding: 0.5em 0 0.5em 2em; + color: #5E740B; + background-color: #eee; +} + +#qunit-userAgent { + padding: 0.5em 0 0.5em 2.5em; + background-color: #2b81af; + color: #fff; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} + + +/** Tests: Pass/Fail */ + +#qunit-tests { + list-style-position: inside; +} + +#qunit-tests li { + padding: 0.4em 0.5em 0.4em 2.5em; + border-bottom: 1px solid #fff; + list-style-position: inside; +} + +#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { + display: none; +} + +#qunit-tests li strong { + cursor: pointer; +} + +#qunit-tests li a { + padding: 0.5em; + color: #c2ccd1; + text-decoration: none; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests ol { + margin-top: 0.5em; + padding: 0.5em; + + background-color: #fff; + + border-radius: 15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; + box-shadow: inset 0px 2px 13px #999; -moz-box-shadow: inset 0px 2px 13px #999; -webkit-box-shadow: inset 0px 2px 13px #999; - margin-top:0.5em; - margin-left:0; - padding:0.5em; - background-color:#fff; - border-radius:15px; - -moz-border-radius: 15px; - -webkit-border-radius: 15px; -} -ol#qunit-tests li li{ - border-bottom:none; - margin:0.5em; - background-color:#fff; - list-style-position: inside; - padding:0.4em 0.5em 0.4em 0.5em; } -ol#qunit-tests li li.pass{ - border-left:26px solid #C6E746; - background-color:#fff; - color:#5E740B; - } -ol#qunit-tests li li.fail{ - border-left:26px solid #EE5757; - background-color:#fff; - color:#710909; +#qunit-tests table { + border-collapse: collapse; + margin-top: .2em; } -ol#qunit-tests li.pass{ - background-color:#D2E0E6; - color:#528CE0; + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 .5em 0 0; } -ol#qunit-tests li.fail{ - background-color:#EE5757; - color:#000; + +#qunit-tests td { + vertical-align: top; } -ol#qunit-tests li strong { - cursor:pointer; + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; } -h1#qunit-header{ - background-color:#0d3349; - margin:0; - padding:0.5em 0 0.5em 1em; - color:#fff; - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - border-top-right-radius:15px; - border-top-left-radius:15px; - -moz-border-radius-topright:15px; - -moz-border-radius-topleft:15px; - -webkit-border-top-right-radius:15px; - -webkit-border-top-left-radius:15px; - text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; + +#qunit-tests del { + background-color: #e0f2be; + color: #374e0c; + text-decoration: none; } -h2#qunit-banner{ - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - height:5px; - margin:0; - padding:0; + +#qunit-tests ins { + background-color: #ffcaca; + color: #500; + text-decoration: none; } -h2#qunit-banner.qunit-pass{ - background-color:#C6E746; + +/*** Test Counts */ + +#qunit-tests b.counts { color: black; } +#qunit-tests b.passed { color: #5E740B; } +#qunit-tests b.failed { color: #710909; } + +#qunit-tests li li { + margin: 0.5em; + padding: 0.4em 0.5em 0.4em 0.5em; + background-color: #fff; + border-bottom: none; + list-style-position: inside; } -h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { - background-color:#EE5757; + +/*** Passing Styles */ + +#qunit-tests li li.pass { + color: #5E740B; + background-color: #fff; + border-left: 26px solid #C6E746; } -#qunit-testrunner-toolbar { - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - padding:0; - /*width:80%;*/ - padding:0em 0 0.5em 2em; - font-size: small; + +#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } +#qunit-tests .pass .test-name { color: #366097; } + +#qunit-tests .pass .test-actual, +#qunit-tests .pass .test-expected { color: #999999; } + +#qunit-banner.qunit-pass { background-color: #C6E746; } + +/*** Failing Styles */ + +#qunit-tests li li.fail { + color: #710909; + background-color: #fff; + border-left: 26px solid #EE5757; + white-space: pre; } -h2#qunit-userAgent { - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - background-color:#2b81af; - margin:0; - padding:0; - color:#fff; - font-size: small; - padding:0.5em 0 0.5em 2.5em; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; + +#qunit-tests > li:last-child { + border-radius: 0 0 15px 15px; + -moz-border-radius: 0 0 15px 15px; + -webkit-border-bottom-right-radius: 15px; + -webkit-border-bottom-left-radius: 15px; } -p#qunit-testresult{ - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - margin:0; - font-size: small; - color:#2b81af; - border-bottom-right-radius:15px; - border-bottom-left-radius:15px; - -moz-border-radius-bottomright:15px; - -moz-border-radius-bottomleft:15px; - -webkit-border-bottom-right-radius:15px; - -webkit-border-bottom-left-radius:15px; - background-color:#D2E0E6; - padding:0.5em 0.5em 0.5em 2.5em; + +#qunit-tests .fail { color: #000000; background-color: #EE5757; } +#qunit-tests .fail .test-name, +#qunit-tests .fail .module-name { color: #000000; } + +#qunit-tests .fail .test-actual { color: #EE5757; } +#qunit-tests .fail .test-expected { color: green; } + +#qunit-banner.qunit-fail { background-color: #EE5757; } + + +/** Result */ + +#qunit-testresult { + padding: 0.5em 0.5em 0.5em 2.5em; + + color: #2b81af; + background-color: #D2E0E6; + + border-bottom: 1px solid white; } -strong b.fail{ - color:#710909; - } -strong b.pass{ - color:#5E740B; - } + +/** Fixture */ + +#qunit-fixture { + position: absolute; + top: -10000px; + left: -10000px; +} \ No newline at end of file diff --git a/ext/qunit.js b/ext/qunit.js index 2420ea7..509507c 100644 --- a/ext/qunit.js +++ b/ext/qunit.js @@ -1,754 +1,1086 @@ -/* - * QUnit - A JavaScript Unit Testing Framework - * +/** + * QUnit v1.3.0pre - A JavaScript Unit Testing Framework + * * http://docs.jquery.com/QUnit * - * Copyright (c) 2009 John Resig, Jörn Zaefferer + * Copyright (c) 2011 John Resig, Jörn Zaefferer * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. + * or GPL (GPL-LICENSE.txt) licenses. + * Pulled Live from Git Sat Feb 11 19:20:01 UTC 2012 + * Last Commit: 0712230bb203c262211649b32bd712ec7df5f857 */ (function(window) { +var defined = { + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + try { + return !!sessionStorage.getItem; + } catch(e) { + return false; + } + })() +}; + +var testId = 0, + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty; + +var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { + this.name = name; + this.testName = testName; + this.expected = expected; + this.testEnvironmentArg = testEnvironmentArg; + this.async = async; + this.callback = callback; + this.assertions = []; +}; +Test.prototype = { + init: function() { + var tests = id("qunit-tests"); + if (tests) { + var b = document.createElement("strong"); + b.innerHTML = "Running " + this.name; + var li = document.createElement("li"); + li.appendChild( b ); + li.className = "running"; + li.id = this.id = "test-output" + testId++; + tests.appendChild( li ); + } + }, + setup: function() { + if (this.module != config.previousModule) { + if ( config.previousModule ) { + runLoggingCallbacks('moduleDone', QUnit, { + name: config.previousModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + } ); + } + config.previousModule = this.module; + config.moduleStats = { all: 0, bad: 0 }; + runLoggingCallbacks( 'moduleStart', QUnit, { + name: this.module + } ); + } + + config.current = this; + this.testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, this.moduleTestEnvironment); + if (this.testEnvironmentArg) { + extend(this.testEnvironment, this.testEnvironmentArg); + } + + runLoggingCallbacks( 'testStart', QUnit, { + name: this.testName, + module: this.module + }); + + // allow utility functions to access the current test environment + // TODO why?? + QUnit.current_testEnvironment = this.testEnvironment; + + try { + if ( !config.pollution ) { + saveGlobal(); + } + + this.testEnvironment.setup.call(this.testEnvironment); + } catch(e) { + QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); + } + }, + run: function() { + config.current = this; + if ( this.async ) { + QUnit.stop(); + } + + if ( config.notrycatch ) { + this.callback.call(this.testEnvironment); + return; + } + try { + this.callback.call(this.testEnvironment); + } catch(e) { + fail("Test " + this.testName + " died, exception and test follows", e, this.callback); + QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + QUnit.start(); + } + } + }, + teardown: function() { + config.current = this; + try { + this.testEnvironment.teardown.call(this.testEnvironment); + checkPollution(); + } catch(e) { + QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); + } + }, + finish: function() { + config.current = this; + if ( this.expected != null && this.expected != this.assertions.length ) { + QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); + } + + var good = 0, bad = 0, + tests = id("qunit-tests"); + + config.stats.all += this.assertions.length; + config.moduleStats.all += this.assertions.length; + + if ( tests ) { + var ol = document.createElement("ol"); + + for ( var i = 0; i < this.assertions.length; i++ ) { + var assertion = this.assertions[i]; + + var li = document.createElement("li"); + li.className = assertion.result ? "pass" : "fail"; + li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + // store result when possible + if ( QUnit.config.reorder && defined.sessionStorage ) { + if (bad) { + sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); + } else { + sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); + } + } + + if (bad == 0) { + ol.style.display = "none"; + } + + var b = document.createElement("strong"); + b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; + + var a = document.createElement("a"); + a.innerHTML = "Rerun"; + a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); + + addEvent(b, "click", function() { + var next = b.nextSibling.nextSibling, + display = next.style.display; + next.style.display = display === "none" ? "block" : "none"; + }); + + addEvent(b, "dblclick", function(e) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { + target = target.parentNode; + } + if ( window.location && target.nodeName.toLowerCase() === "strong" ) { + window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); + } + }); + + var li = id(this.id); + li.className = bad ? "fail" : "pass"; + li.removeChild( li.firstChild ); + li.appendChild( b ); + li.appendChild( a ); + li.appendChild( ol ); + + } else { + for ( var i = 0; i < this.assertions.length; i++ ) { + if ( !this.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + } + + try { + QUnit.reset(); + } catch(e) { + fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); + } + + runLoggingCallbacks( 'testDone', QUnit, { + name: this.testName, + module: this.module, + failed: bad, + passed: this.assertions.length - bad, + total: this.assertions.length + } ); + }, + + queue: function() { + var test = this; + synchronize(function() { + test.init(); + }); + function run() { + // each of these can by async + synchronize(function() { + test.setup(); + }); + synchronize(function() { + test.run(); + }); + synchronize(function() { + test.teardown(); + }); + synchronize(function() { + test.finish(); + }); + } + // defer when previous test run passed, if storage is available + var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); + if (bad) { + run(); + } else { + synchronize(run, true); + }; + } + +}; + var QUnit = { - // Initialize the configuration options - init: function() { - config = { - stats: { all: 0, bad: 0 }, - moduleStats: { all: 0, bad: 0 }, - started: +new Date, - blocking: false, - autorun: false, - assertions: [], - filters: [], - queue: [] - }; + // call on start of module test to prepend name to all tests + module: function(name, testEnvironment) { + config.currentModule = name; + config.currentModuleTestEnviroment = testEnvironment; + }, - var tests = id("qunit-tests"), - banner = id("qunit-banner"), - result = id("qunit-testresult"); + asyncTest: function(testName, expected, callback) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } - if ( tests ) { - tests.innerHTML = ""; - } + QUnit.test(testName, expected, callback, true); + }, - if ( banner ) { - banner.className = ""; - } + test: function(testName, expected, callback, async) { + var name = '' + escapeInnerText(testName) + '', testEnvironmentArg; - if ( result ) { - result.parentNode.removeChild( result ); - } - }, - - // call on start of module test to prepend name to all tests - module: function(name, testEnvironment) { - config.currentModule = name; + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + // is 2nd argument a testEnvironment? + if ( expected && typeof expected === 'object') { + testEnvironmentArg = expected; + expected = null; + } - synchronize(function() { - if ( config.currentModule ) { - QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); - } + if ( config.currentModule ) { + name = '' + config.currentModule + ": " + name; + } - config.currentModule = name; - config.moduleTestEnvironment = testEnvironment; - config.moduleStats = { all: 0, bad: 0 }; + if ( !validTest(config.currentModule + ": " + testName) ) { + return; + } - QUnit.moduleStart( name, testEnvironment ); - }); - }, + var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); + test.module = config.currentModule; + test.moduleTestEnvironment = config.currentModuleTestEnviroment; + test.queue(); + }, - asyncTest: function(testName, expected, callback) { - if ( arguments.length === 2 ) { - callback = expected; - expected = 0; - } + /** + * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. + */ + expect: function(asserts) { + config.current.expected = asserts; + }, - QUnit.test(testName, expected, callback, true); - }, - - test: function(testName, expected, callback, async) { - var name = testName, testEnvironment, testEnvironmentArg; + /** + * Asserts true. + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function(a, msg) { + a = !!a; + var details = { + result: a, + message: msg + }; + msg = escapeInnerText(msg); + runLoggingCallbacks( 'log', QUnit, details ); + config.current.assertions.push({ + result: a, + message: msg + }); + }, - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - // is 2nd argument a testEnvironment? - if ( expected && typeof expected === 'object') { - testEnvironmentArg = expected; - expected = null; - } + /** + * Checks that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * + * Prefered to ok( actual == expected, message ) + * + * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); + * + * @param Object actual + * @param Object expected + * @param String message (optional) + */ + equal: function(actual, expected, message) { + QUnit.push(expected == actual, actual, expected, message); + }, - if ( config.currentModule ) { - name = config.currentModule + " module: " + name; - } + notEqual: function(actual, expected, message) { + QUnit.push(expected != actual, actual, expected, message); + }, - if ( !validTest(name) ) { - return; - } + deepEqual: function(actual, expected, message) { + QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); + }, - synchronize(function() { - QUnit.testStart( testName ); + notDeepEqual: function(actual, expected, message) { + QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); + }, - testEnvironment = extend({ - setup: function() {}, - teardown: function() {} - }, config.moduleTestEnvironment); - if (testEnvironmentArg) { - extend(testEnvironment,testEnvironmentArg); - } + strictEqual: function(actual, expected, message) { + QUnit.push(expected === actual, actual, expected, message); + }, - // allow utility functions to access the current test environment - QUnit.current_testEnvironment = testEnvironment; - - config.assertions = []; - config.expected = expected; + notStrictEqual: function(actual, expected, message) { + QUnit.push(expected !== actual, actual, expected, message); + }, - try { - if ( !config.pollution ) { - saveGlobal(); - } + raises: function(block, expected, message) { + var actual, ok = false; - testEnvironment.setup.call(testEnvironment); - } catch(e) { - QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); - } + if (typeof expected === 'string') { + message = expected; + expected = null; + } - if ( async ) { - QUnit.stop(); - } + try { + block(); + } catch (e) { + actual = e; + } - try { - callback.call(testEnvironment); - } catch(e) { - fail("Test " + name + " died, exception and test follows", e, callback); - QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); - // else next test will carry the responsibility - saveGlobal(); + if (actual) { + // we don't want to validate thrown error + if (!expected) { + ok = true; + // expected is a regexp + } else if (QUnit.objectType(expected) === "regexp") { + ok = expected.test(actual); + // expected is a constructor + } else if (actual instanceof expected) { + ok = true; + // expected is a validation function which returns true is validation passed + } else if (expected.call({}, actual) === true) { + ok = true; + } + } - // Restart the tests if they're blocking - if ( config.blocking ) { - start(); - } - } - }); + QUnit.ok(ok, message); + }, - synchronize(function() { - try { - checkPollution(); - testEnvironment.teardown.call(testEnvironment); - } catch(e) { - QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); - } + start: function(count) { + config.semaphore -= count || 1; + if (config.semaphore > 0) { + // don't start until equal number of stop-calls + return; + } + if (config.semaphore < 0) { + // ignore if start is called more often then stop + config.semaphore = 0; + } + // A slight delay, to avoid any current callbacks + if ( defined.setTimeout ) { + window.setTimeout(function() { + if (config.semaphore > 0) { + return; + } + if ( config.timeout ) { + clearTimeout(config.timeout); + } - try { - QUnit.reset(); - } catch(e) { - fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); - } + config.blocking = false; + process(true); + }, 13); + } else { + config.blocking = false; + process(true); + } + }, - if ( config.expected && config.expected != config.assertions.length ) { - QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); - } + stop: function(count) { + config.semaphore += count || 1; + config.blocking = true; - var good = 0, bad = 0, - tests = id("qunit-tests"); - - config.stats.all += config.assertions.length; - config.moduleStats.all += config.assertions.length; - - if ( tests ) { - var ol = document.createElement("ol"); - ol.style.display = "none"; - - for ( var i = 0; i < config.assertions.length; i++ ) { - var assertion = config.assertions[i]; - - var li = document.createElement("li"); - li.className = assertion.result ? "pass" : "fail"; - li.innerHTML = assertion.message || "(no message)"; - ol.appendChild( li ); - - if ( assertion.result ) { - good++; - } else { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - - var b = document.createElement("strong"); - b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; - - addEvent(b, "click", function() { - var next = b.nextSibling, display = next.style.display; - next.style.display = display === "none" ? "block" : "none"; - }); - - addEvent(b, "dblclick", function(e) { - var target = e && e.target ? e.target : window.event.srcElement; - if ( target.nodeName.toLowerCase() === "strong" ) { - var text = "", node = target.firstChild; - - while ( node.nodeType === 3 ) { - text += node.nodeValue; - node = node.nextSibling; - } - - text = text.replace(/(^\s*|\s*$)/g, ""); - - if ( window.location ) { - window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text); - } - } - }); - - var li = document.createElement("li"); - li.className = bad ? "fail" : "pass"; - li.appendChild( b ); - li.appendChild( ol ); - tests.appendChild( li ); - - if ( bad ) { - var toolbar = id("qunit-testrunner-toolbar"); - if ( toolbar ) { - toolbar.style.display = "block"; - id("qunit-filter-pass").disabled = null; - id("qunit-filter-missing").disabled = null; - } - } - - } else { - for ( var i = 0; i < config.assertions.length; i++ ) { - if ( !config.assertions[i].result ) { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - } - - QUnit.testDone( testName, bad, config.assertions.length ); - - if ( !window.setTimeout && !config.queue.length ) { - done(); - } - }); - - if ( window.setTimeout && !config.doneTimer ) { - config.doneTimer = window.setTimeout(function(){ - if ( !config.queue.length ) { - done(); - } else { - synchronize( done ); - } - }, 13); - } - }, - - /** - * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. - */ - expect: function(asserts) { - config.expected = asserts; - }, - - /** - * Asserts true. - * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); - */ - ok: function(a, msg) { - QUnit.log(a, msg); - - config.assertions.push({ - result: !!a, - message: msg - }); - }, - - /** - * Checks that the first two arguments are equal, with an optional message. - * Prints out both actual and expected values. - * - * Prefered to ok( actual == expected, message ) - * - * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); - * - * @param Object actual - * @param Object expected - * @param String message (optional) - */ - equal: function(actual, expected, message) { - push(expected == actual, actual, expected, message); - }, - - notEqual: function(actual, expected, message) { - push(expected != actual, actual, expected, message); - }, - - deepEqual: function(a, b, message) { - push(QUnit.equiv(a, b), a, b, message); - }, - - notDeepEqual: function(a, b, message) { - push(!QUnit.equiv(a, b), a, b, message); - }, - - strictEqual: function(actual, expected, message) { - push(expected === actual, actual, expected, message); - }, - - notStrictEqual: function(actual, expected, message) { - push(expected !== actual, actual, expected, message); - }, - - start: function() { - // A slight delay, to avoid any current callbacks - if ( window.setTimeout ) { - window.setTimeout(function() { - if ( config.timeout ) { - clearTimeout(config.timeout); - } - - config.blocking = false; - process(); - }, 13); - } else { - config.blocking = false; - process(); - } - }, - - stop: function(timeout) { - config.blocking = true; - - if ( timeout && window.setTimeout ) { - config.timeout = window.setTimeout(function() { - QUnit.ok( false, "Test timed out" ); - QUnit.start(); - }, timeout); - } - }, - - /** - * Resets the test setup. Useful for tests that modify the DOM. - */ - reset: function() { - if ( window.jQuery ) { - jQuery("#main").html( config.fixture ); - jQuery.event.global = {}; - jQuery.ajaxSettings = extend({}, config.ajaxSettings); - } - }, - - /** - * Trigger an event on an element. - * - * @example triggerEvent( document.body, "click" ); - * - * @param DOMElement elem - * @param String type - */ - triggerEvent: function( elem, type, event ) { - if ( document.createEvent ) { - event = document.createEvent("MouseEvents"); - event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - elem.dispatchEvent( event ); - - } else if ( elem.fireEvent ) { - elem.fireEvent("on"+type); - } - }, - - // Safe object type checking - is: function( type, obj ) { - return Object.prototype.toString.call( obj ) === "[object "+ type +"]"; - }, - - // Logging callbacks - done: function(failures, total) {}, - log: function(result, message) {}, - testStart: function(name) {}, - testDone: function(name, failures, total) {}, - moduleStart: function(name, testEnvironment) {}, - moduleDone: function(name, failures, total) {} + if ( config.testTimeout && defined.setTimeout ) { + clearTimeout(config.timeout); + config.timeout = window.setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + config.semaphore = 1; + QUnit.start(); + }, config.testTimeout); + } + } }; +//We want access to the constructor's prototype +(function() { + function F(){}; + F.prototype = QUnit; + QUnit = new F(); + //Make F QUnit's constructor so that we can add to the prototype later + QUnit.constructor = F; +})(); + // Backwards compatibility, deprecated QUnit.equals = QUnit.equal; QUnit.same = QUnit.deepEqual; // Maintain internal state var config = { - // The queue of tests to run - queue: [], + // The queue of tests to run + queue: [], - // block until document ready - blocking: true + // block until document ready + blocking: true, + + // when enabled, show only failing tests + // gets persisted through sessionStorage and can be changed in UI via checkbox + hidepassed: false, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + // by default, modify document.title when suite is done + altertitle: true, + + urlConfig: ['noglobals', 'notrycatch'], + + //logging callback queues + begin: [], + done: [], + log: [], + testStart: [], + testDone: [], + moduleStart: [], + moduleDone: [] }; // Load paramaters (function() { - var location = window.location || { search: "", protocol: "file:" }, - GETParams = location.search.slice(1).split('&'); + var location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}, + current; - for ( var i = 0; i < GETParams.length; i++ ) { - GETParams[i] = decodeURIComponent( GETParams[i] ); - if ( GETParams[i] === "noglobals" ) { - GETParams.splice( i, 1 ); - i--; - config.noglobals = true; - } else if ( GETParams[i].search('=') > -1 ) { - GETParams.splice( i, 1 ); - i--; - } - } - - // restrict modules/tests by get parameters - config.filters = GETParams; - - // Figure out if we're running the tests from a server or not - QUnit.isLocal = !!(location.protocol === 'file:'); + if ( params[ 0 ] ) { + for ( var i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + urlParams[ current[ 0 ] ] = current[ 1 ]; + } + } + + QUnit.urlParams = urlParams; + config.filter = urlParams.filter; + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = !!(location.protocol === 'file:'); })(); // Expose the API as global variables, unless an 'exports' // object exists, in that case we assume we're in CommonJS if ( typeof exports === "undefined" || typeof require === "undefined" ) { - extend(window, QUnit); - window.QUnit = QUnit; + extend(window, QUnit); + window.QUnit = QUnit; } else { - extend(exports, QUnit); - exports.QUnit = QUnit; + extend(exports, QUnit); + exports.QUnit = QUnit; } -if ( typeof document === "undefined" || document.readyState === "complete" ) { - config.autorun = true; -} +// define these after exposing globals to keep them in these QUnit namespace only +extend(QUnit, { + config: config, -addEvent(window, "load", function() { - // Initialize the config, saving the execution queue - var oldconfig = extend({}, config); - QUnit.init(); - extend(config, oldconfig); + // Initialize the configuration options + init: function() { + extend(config, { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date, + updateRate: 1000, + blocking: false, + autostart: true, + autorun: false, + filter: "", + queue: [], + semaphore: 0 + }); - config.blocking = false; + var tests = id( "qunit-tests" ), + banner = id( "qunit-banner" ), + result = id( "qunit-testresult" ); - var userAgent = id("qunit-userAgent"); - if ( userAgent ) { - userAgent.innerHTML = navigator.userAgent; - } - - var toolbar = id("qunit-testrunner-toolbar"); - if ( toolbar ) { - toolbar.style.display = "none"; - - var filter = document.createElement("input"); - filter.type = "checkbox"; - filter.id = "qunit-filter-pass"; - filter.disabled = true; - addEvent( filter, "click", function() { - var li = document.getElementsByTagName("li"); - for ( var i = 0; i < li.length; i++ ) { - if ( li[i].className.indexOf("pass") > -1 ) { - li[i].style.display = filter.checked ? "none" : ""; - } - } - }); - toolbar.appendChild( filter ); + if ( tests ) { + tests.innerHTML = ""; + } - var label = document.createElement("label"); - label.setAttribute("for", "qunit-filter-pass"); - label.innerHTML = "Hide passed tests"; - toolbar.appendChild( label ); + if ( banner ) { + banner.className = ""; + } - var missing = document.createElement("input"); - missing.type = "checkbox"; - missing.id = "qunit-filter-missing"; - missing.disabled = true; - addEvent( missing, "click", function() { - var li = document.getElementsByTagName("li"); - for ( var i = 0; i < li.length; i++ ) { - if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { - li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; - } - } - }); - toolbar.appendChild( missing ); + if ( result ) { + result.parentNode.removeChild( result ); + } - label = document.createElement("label"); - label.setAttribute("for", "qunit-filter-missing"); - label.innerHTML = "Hide missing tests (untested code is broken code)"; - toolbar.appendChild( label ); - } + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = 'Running...
     '; + } + }, - var main = id('main'); - if ( main ) { - config.fixture = main.innerHTML; - } + /** + * Resets the test setup. Useful for tests that modify the DOM. + * + * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. + */ + reset: function() { + if ( window.jQuery ) { + jQuery( "#qunit-fixture" ).html( config.fixture ); + } else { + var main = id( 'qunit-fixture' ); + if ( main ) { + main.innerHTML = config.fixture; + } + } + }, - if ( window.jQuery ) { - config.ajaxSettings = window.jQuery.ajaxSettings; - } + /** + * Trigger an event on an element. + * + * @example triggerEvent( document.body, "click" ); + * + * @param DOMElement elem + * @param String type + */ + triggerEvent: function( elem, type, event ) { + if ( document.createEvent ) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + elem.dispatchEvent( event ); - QUnit.start(); + } else if ( elem.fireEvent ) { + elem.fireEvent("on"+type); + } + }, + + // Safe object type checking + is: function( type, obj ) { + return QUnit.objectType( obj ) == type; + }, + + objectType: function( obj ) { + if (typeof obj === "undefined") { + return "undefined"; + + // consider: typeof null === object + } + if (obj === null) { + return "null"; + } + + var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; + + switch (type) { + case 'Number': + if (isNaN(obj)) { + return "nan"; + } else { + return "number"; + } + case 'String': + case 'Boolean': + case 'Array': + case 'Date': + case 'RegExp': + case 'Function': + return type.toLowerCase(); + } + if (typeof obj === "object") { + return "object"; + } + return undefined; + }, + + push: function(result, actual, expected, message) { + var details = { + result: result, + message: message, + actual: actual, + expected: expected + }; + + message = escapeInnerText(message) || (result ? "okay" : "failed"); + message = '' + message + ""; + expected = escapeInnerText(QUnit.jsDump.parse(expected)); + actual = escapeInnerText(QUnit.jsDump.parse(actual)); + var output = message + ''; + if (actual != expected) { + output += ''; + output += ''; + } + if (!result) { + var source = sourceFromStacktrace(); + if (source) { + details.source = source; + output += ''; + } + } + output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; + + runLoggingCallbacks( 'log', QUnit, details ); + + config.current.assertions.push({ + result: !!result, + message: output + }); + }, + + url: function( params ) { + params = extend( extend( {}, QUnit.urlParams ), params ); + var querystring = "?", + key; + for ( key in params ) { + if ( !hasOwn.call( params, key ) ) { + continue; + } + querystring += encodeURIComponent( key ) + "=" + + encodeURIComponent( params[ key ] ) + "&"; + } + return window.location.pathname + querystring.slice( 0, -1 ); + }, + + extend: extend, + id: id, + addEvent: addEvent }); +//QUnit.constructor is set to the empty F() above so that we can add to it's prototype later +//Doing this allows us to tell if the following methods have been overwritten on the actual +//QUnit object, which is a deprecated way of using the callbacks. +extend(QUnit.constructor.prototype, { + // Logging callbacks; all receive a single argument with the listed properties + // run test/logs.html for any related changes + begin: registerLoggingCallback('begin'), + // done: { failed, passed, total, runtime } + done: registerLoggingCallback('done'), + // log: { result, actual, expected, message } + log: registerLoggingCallback('log'), + // testStart: { name } + testStart: registerLoggingCallback('testStart'), + // testDone: { name, failed, passed, total } + testDone: registerLoggingCallback('testDone'), + // moduleStart: { name } + moduleStart: registerLoggingCallback('moduleStart'), + // moduleDone: { name, failed, passed, total } + moduleDone: registerLoggingCallback('moduleDone') +}); + +if ( typeof document === "undefined" || document.readyState === "complete" ) { + config.autorun = true; +} + +QUnit.load = function() { + runLoggingCallbacks( 'begin', QUnit, {} ); + + // Initialize the config, saving the execution queue + var oldconfig = extend({}, config); + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + var urlConfigHtml = '', len = config.urlConfig.length; + for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { + config[val] = QUnit.urlParams[val]; + urlConfigHtml += ''; + } + + var userAgent = id("qunit-userAgent"); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + var banner = id("qunit-header"); + if ( banner ) { + banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; + addEvent( banner, "change", function( event ) { + var params = {}; + params[ event.target.name ] = event.target.checked ? true : undefined; + window.location = QUnit.url( params ); + }); + } + + var toolbar = id("qunit-testrunner-toolbar"); + if ( toolbar ) { + var filter = document.createElement("input"); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + addEvent( filter, "click", function() { + var ol = document.getElementById("qunit-tests"); + if ( filter.checked ) { + ol.className = ol.className + " hidepass"; + } else { + var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; + ol.className = tmp.replace(/ hidepass /, " "); + } + if ( defined.sessionStorage ) { + if (filter.checked) { + sessionStorage.setItem("qunit-filter-passed-tests", "true"); + } else { + sessionStorage.removeItem("qunit-filter-passed-tests"); + } + } + }); + if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { + filter.checked = true; + var ol = document.getElementById("qunit-tests"); + ol.className = ol.className + " hidepass"; + } + toolbar.appendChild( filter ); + + var label = document.createElement("label"); + label.setAttribute("for", "qunit-filter-pass"); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + } + + var main = id('qunit-fixture'); + if ( main ) { + config.fixture = main.innerHTML; + } + + if (config.autostart) { + QUnit.start(); + } +}; + +addEvent(window, "load", QUnit.load); + +// addEvent(window, "error") gives us a useless event object +window.onerror = function( message, file, line ) { + if ( QUnit.config.current ) { + ok( false, message + ", " + file + ":" + line ); + } else { + test( "global failure", function() { + ok( false, message + ", " + file + ":" + line ); + }); + } +}; + function done() { - if ( config.doneTimer && window.clearTimeout ) { - window.clearTimeout( config.doneTimer ); - config.doneTimer = null; - } + config.autorun = true; - if ( config.queue.length ) { - config.doneTimer = window.setTimeout(function(){ - if ( !config.queue.length ) { - done(); - } else { - synchronize( done ); - } - }, 13); + // Log the last module results + if ( config.currentModule ) { + runLoggingCallbacks( 'moduleDone', QUnit, { + name: config.currentModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + } ); + } - return; - } + var banner = id("qunit-banner"), + tests = id("qunit-tests"), + runtime = +new Date - config.started, + passed = config.stats.all - config.stats.bad, + html = [ + 'Tests completed in ', + runtime, + ' milliseconds.
    ', + '', + passed, + ' tests of ', + config.stats.all, + ' passed, ', + config.stats.bad, + ' failed.' + ].join(''); - config.autorun = true; + if ( banner ) { + banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); + } - // Log the last module results - if ( config.currentModule ) { - QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); - } + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } - var banner = id("qunit-banner"), - tests = id("qunit-tests"), - html = ['Tests completed in ', - +new Date - config.started, ' milliseconds.
    ', - '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); + if ( config.altertitle && typeof document !== "undefined" && document.title ) { + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + (config.stats.bad ? "\u2716" : "\u2714"), + document.title.replace(/^[\u2714\u2716] /i, "") + ].join(" "); + } - if ( banner ) { - banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); - } - - if ( tests ) { - var result = id("qunit-testresult"); - - if ( !result ) { - result = document.createElement("p"); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests.nextSibling ); - } - - result.innerHTML = html; - } - - QUnit.done( config.stats.bad, config.stats.all ); + runLoggingCallbacks( 'done', QUnit, { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + } ); } function validTest( name ) { - var i = config.filters.length, - run = false; + var filter = config.filter, + run = false; - if ( !i ) { - return true; - } - - while ( i-- ) { - var filter = config.filters[i], - not = filter.charAt(0) == '!'; + if ( !filter ) { + return true; + } - if ( not ) { - filter = filter.slice(1); - } + var not = filter.charAt( 0 ) === "!"; + if ( not ) { + filter = filter.slice( 1 ); + } - if ( name.indexOf(filter) !== -1 ) { - return !not; - } + if ( name.indexOf( filter ) !== -1 ) { + return !not; + } - if ( not ) { - run = true; - } - } + if ( not ) { + run = true; + } - return run; + return run; } -function push(result, actual, expected, message) { - message = message || (result ? "okay" : "failed"); - QUnit.ok( result, result ? message + ": " + expected : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) ); +// so far supports only Firefox, Chrome and Opera (buggy) +// could be extended in the future to use something like https://github.com/csnover/TraceKit +function sourceFromStacktrace() { + try { + throw new Error(); + } catch ( e ) { + if (e.stacktrace) { + // Opera + return e.stacktrace.split("\n")[6]; + } else if (e.stack) { + // Firefox, Chrome + return e.stack.split("\n")[4]; + } else if (e.sourceURL) { + // Safari, PhantomJS + // TODO sourceURL points at the 'throw new Error' line above, useless + //return e.sourceURL + ":" + e.line; + } + } } -function synchronize( callback ) { - config.queue.push( callback ); - - if ( config.autorun && !config.blocking ) { - process(); - } +function escapeInnerText(s) { + if (!s) { + return ""; + } + s = s + ""; + return s.replace(/[\&<>]/g, function(s) { + switch(s) { + case "&": return "&"; + case "<": return "<"; + case ">": return ">"; + default: return s; + } + }); } -function process() { - while ( config.queue.length && !config.blocking ) { - config.queue.shift()(); - } +function synchronize( callback, last ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process(last); + } +} + +function process( last ) { + var start = new Date().getTime(); + config.depth = config.depth ? config.depth + 1 : 1; + + while ( config.queue.length && !config.blocking ) { + if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { + config.queue.shift()(); + } else { + window.setTimeout( function(){ + process( last ); + }, 13 ); + break; + } + } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); + } } function saveGlobal() { - config.pollution = []; - - if ( config.noglobals ) { - for ( var key in window ) { - config.pollution.push( key ); - } - } + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + if ( !hasOwn.call( window, key ) ) { + continue; + } + config.pollution.push( key ); + } + } } function checkPollution( name ) { - var old = config.pollution; - saveGlobal(); - - var newGlobals = diff( old, config.pollution ); - if ( newGlobals.length > 0 ) { - ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); - config.expected++; - } + var old = config.pollution; + saveGlobal(); - var deletedGlobals = diff( config.pollution, old ); - if ( deletedGlobals.length > 0 ) { - ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); - config.expected++; - } + var newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); + } + + var deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); + } } // returns a new Array with the elements that are in a but not in b function diff( a, b ) { - var result = a.slice(); - for ( var i = 0; i < result.length; i++ ) { - for ( var j = 0; j < b.length; j++ ) { - if ( result[i] === b[j] ) { - result.splice(i, 1); - i--; - break; - } - } - } - return result; + var result = a.slice(); + for ( var i = 0; i < result.length; i++ ) { + for ( var j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice(i, 1); + i--; + break; + } + } + } + return result; } function fail(message, exception, callback) { - if ( typeof console !== "undefined" && console.error && console.warn ) { - console.error(message); - console.error(exception); - console.warn(callback.toString()); + if ( typeof console !== "undefined" && console.error && console.warn ) { + console.error(message); + console.error(exception); + console.error(exception.stack); + console.warn(callback.toString()); - } else if ( window.opera && opera.postError ) { - opera.postError(message, exception, callback.toString); - } + } else if ( window.opera && opera.postError ) { + opera.postError(message, exception, callback.toString); + } } function extend(a, b) { - for ( var prop in b ) { - a[prop] = b[prop]; - } + for ( var prop in b ) { + if ( b[prop] === undefined ) { + delete a[prop]; - return a; + // Avoid "Member not found" error in IE8 caused by setting window.constructor + } else if ( prop !== "constructor" || a !== window ) { + a[prop] = b[prop]; + } + } + + return a; } function addEvent(elem, type, fn) { - if ( elem.addEventListener ) { - elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, fn ); - } else { - fn(); - } + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, fn ); + } else { + fn(); + } } function id(name) { - return !!(typeof document !== "undefined" && document && document.getElementById) && - document.getElementById( name ); + return !!(typeof document !== "undefined" && document && document.getElementById) && + document.getElementById( name ); +} + +function registerLoggingCallback(key){ + return function(callback){ + config[key].push( callback ); + }; +} + +// Supports deprecated method of completely overwriting logging callbacks +function runLoggingCallbacks(key, scope, args) { + //debugger; + var callbacks; + if ( QUnit.hasOwnProperty(key) ) { + QUnit[key].call(scope, args); + } else { + callbacks = config[key]; + for( var i = 0; i < callbacks.length; i++ ) { + callbacks[i].call( scope, args ); + } + } } // Test for equality any JavaScript type. -// Discussions and reference: http://philrathe.com/articles/equiv -// Test suites: http://philrathe.com/tests/equiv // Author: Philippe Rathé QUnit.equiv = function () { var innerEquiv; // the real equiv function var callers = []; // stack to decide between skip/abort functions - - - // Determine what is o. - function hoozit(o) { - if (QUnit.is("String", o)) { - return "string"; - - } else if (QUnit.is("Boolean", o)) { - return "boolean"; - - } else if (QUnit.is("Number", o)) { - - if (isNaN(o)) { - return "nan"; - } else { - return "number"; - } - - } else if (typeof o === "undefined") { - return "undefined"; - - // consider: typeof null === object - } else if (o === null) { - return "null"; - - // consider: typeof [] === object - } else if (QUnit.is( "Array", o)) { - return "array"; - - // consider: typeof new Date() === object - } else if (QUnit.is( "Date", o)) { - return "date"; - - // consider: /./ instanceof Object; - // /./ instanceof RegExp; - // typeof /./ === "function"; // => false in IE and Opera, - // true in FF and Safari - } else if (QUnit.is( "RegExp", o)) { - return "regexp"; - - } else if (typeof o === "object") { - return "object"; - - } else if (QUnit.is( "Function", o)) { - return "function"; - } else { - return undefined; - } - } + var parents = []; // stack to avoiding loops from circular referencing // Call the o related callback with the given arguments. function bindCallbacks(o, callbacks, args) { - var prop = hoozit(o); + var prop = QUnit.objectType(o); if (prop) { - if (hoozit(callbacks[prop]) === "function") { + if (QUnit.objectType(callbacks[prop]) === "function") { return callbacks[prop].apply(callbacks, args); } else { return callbacks[prop]; // or undefined } } } - + + var getProto = Object.getPrototypeOf || function (obj) { + return obj.__proto__; + }; + var callbacks = function () { // for string, boolean, number and null function useStrictEquality(b, a) { if (b instanceof a.constructor || a instanceof b.constructor) { - // to catch short annotaion VS 'new' annotation of a declaration + // to catch short annotaion VS 'new' annotation of a + // declaration // e.g. var i = 1; - // var j = new Number(1); + // var j = new Number(1); return a == b; } else { return a === b; @@ -756,43 +1088,44 @@ QUnit.equiv = function () { } return { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, + "string" : useStrictEquality, + "boolean" : useStrictEquality, + "number" : useStrictEquality, + "null" : useStrictEquality, + "undefined" : useStrictEquality, - "nan": function (b) { + "nan" : function(b) { return isNaN(b); }, - "date": function (b, a) { - return hoozit(b) === "date" && a.valueOf() === b.valueOf(); + "date" : function(b, a) { + return QUnit.objectType(b) === "date" + && a.valueOf() === b.valueOf(); }, - "regexp": function (b, a) { - return hoozit(b) === "regexp" && - a.source === b.source && // the regex itself - a.global === b.global && // and its modifers (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline; + "regexp" : function(b, a) { + return QUnit.objectType(b) === "regexp" + && a.source === b.source && // the regex itself + a.global === b.global && // and its modifers + // (gmi) ... + a.ignoreCase === b.ignoreCase + && a.multiline === b.multiline; }, // - skip when the property is a method of an instance (OOP) // - abort otherwise, - // initial === would have catch identical references anyway - "function": function () { + // initial === would have catch identical references anyway + "function" : function() { var caller = callers[callers.length - 1]; - return caller !== Object && - typeof caller !== "undefined"; + return caller !== Object && typeof caller !== "undefined"; }, - "array": function (b, a) { - var i; + "array" : function(b, a) { + var i, j, loop; var len; // b could be an object literal here - if ( ! (hoozit(b) === "array")) { + if (!(QUnit.objectType(b) === "array")) { return false; } @@ -800,65 +1133,100 @@ QUnit.equiv = function () { if (len !== b.length) { // safe and faster return false; } + + // track reference to avoid circular references + parents.push(a); for (i = 0; i < len; i++) { - if ( ! innerEquiv(a[i], b[i])) { + loop = false; + for (j = 0; j < parents.length; j++) { + if (parents[j] === a[i]) { + loop = true;// dont rewalk array + } + } + if (!loop && !innerEquiv(a[i], b[i])) { + parents.pop(); return false; } } + parents.pop(); return true; }, - "object": function (b, a) { - var i; + "object" : function(b, a) { + var i, j, loop; var eq = true; // unless we can proove it - var aProperties = [], bProperties = []; // collection of strings + var aProperties = [], bProperties = []; // collection of + // strings - // comparing constructors is more strict than using instanceof - if ( a.constructor !== b.constructor) { - return false; + // comparing constructors is more strict than using + // instanceof + if (a.constructor !== b.constructor) { + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if (!((getProto(a) === null && getProto(b) === Object.prototype) || + (getProto(b) === null && getProto(a) === Object.prototype))) + { + return false; + } } // stack constructor before traversing properties callers.push(a.constructor); + // track reference to avoid circular references + parents.push(a); - for (i in a) { // be strict: don't ensures hasOwnProperty and go deep - + for (i in a) { // be strict: don't ensures hasOwnProperty + // and go deep + loop = false; + for (j = 0; j < parents.length; j++) { + if (parents[j] === a[i]) + loop = true; // don't go down the same path + // twice + } aProperties.push(i); // collect a's properties - if ( ! innerEquiv(a[i], b[i])) { + if (!loop && !innerEquiv(a[i], b[i])) { eq = false; + break; } } callers.pop(); // unstack, we are done + parents.pop(); for (i in b) { bProperties.push(i); // collect b's properties } // Ensures identical properties name - return eq && innerEquiv(aProperties.sort(), bProperties.sort()); + return eq + && innerEquiv(aProperties.sort(), bProperties + .sort()); } }; }(); - innerEquiv = function () { // can take multiple arguments + innerEquiv = function() { // can take multiple arguments var args = Array.prototype.slice.apply(arguments); if (args.length < 2) { return true; // end transition } - return (function (a, b) { + return (function(a, b) { if (a === b) { return true; // catch the most you can - } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) { + } else if (a === null || b === null || typeof a === "undefined" + || typeof b === "undefined" + || QUnit.objectType(a) !== QUnit.objectType(b)) { return false; // don't lose time with error prone cases } else { - return bindCallbacks(a, callbacks, [b, a]); + return bindCallbacks(a, callbacks, [ b, a ]); } - // apply transition with (1..n) arguments - })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1)); + // apply transition with (1..n) arguments + })(args[0], args[1]) + && arguments.callee.apply(this, args.splice(1, + args.length - 1)); }; return innerEquiv; @@ -866,177 +1234,367 @@ QUnit.equiv = function () { }(); /** - * jsDump - * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com - * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) - * Date: 5/15/2008 + * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | + * http://flesler.blogspot.com Licensed under BSD + * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 + * * @projectDescription Advanced and extensible data dumping for Javascript. * @version 1.0.0 * @author Ariel Flesler * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} */ QUnit.jsDump = (function() { - function quote( str ) { - return '"' + str.toString().replace(/"/g, '\\"') + '"'; - }; - function literal( o ) { - return o + ''; - }; - function join( pre, arr, post ) { - var s = jsDump.separator(), - base = jsDump.indent(), - inner = jsDump.indent(1); - if ( arr.join ) - arr = arr.join( ',' + s + inner ); - if ( !arr ) - return pre + post; - return [ pre, inner + arr, base + post ].join(s); - }; - function array( arr ) { - var i = arr.length, ret = Array(i); - this.up(); - while ( i-- ) - ret[i] = this.parse( arr[i] ); - this.down(); - return join( '[', ret, ']' ); - }; - - var reName = /^function (\w+)/; - - var jsDump = { - parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance - var parser = this.parsers[ type || this.typeOf(obj) ]; - type = typeof parser; - - return type == 'function' ? parser.call( this, obj ) : - type == 'string' ? parser : - this.parsers.error; - }, - typeOf:function( obj ) { - var type; - if ( obj === null ) { - type = "null"; - } else if (typeof obj === "undefined") { - type = "undefined"; - } else if (QUnit.is("RegExp", obj)) { - type = "regexp"; - } else if (QUnit.is("Date", obj)) { - type = "date"; - } else if (QUnit.is("Function", obj)) { - type = "function"; - } else if (QUnit.is("Array", obj)) { - type = "array"; - } else if (QUnit.is("Window", obj) || QUnit.is("global", obj)) { - type = "window"; - } else if (QUnit.is("HTMLDocument", obj)) { - type = "document"; - } else if (QUnit.is("HTMLCollection", obj) || QUnit.is("NodeList", obj)) { - type = "nodelist"; - } else if (/^\[object HTML/.test(Object.prototype.toString.call( obj ))) { - type = "node"; - } else { - type = typeof obj; - } - return type; - }, - separator:function() { - return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; - }, - indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing - if ( !this.multiline ) - return ''; - var chr = this.indentChar; - if ( this.HTML ) - chr = chr.replace(/\t/g,' ').replace(/ /g,' '); - return Array( this._depth_ + (extra||0) ).join(chr); - }, - up:function( a ) { - this._depth_ += a || 1; - }, - down:function( a ) { - this._depth_ -= a || 1; - }, - setParser:function( name, parser ) { - this.parsers[name] = parser; - }, - // The next 3 are exposed so you can use them - quote:quote, - literal:literal, - join:join, - // - _depth_: 1, - // This is the list of parsers, to modify them, use jsDump.setParser - parsers:{ - window: '[Window]', - document: '[Document]', - error:'[ERROR]', //when no parser is found, shouldn't happen - unknown: '[Unknown]', - 'null':'null', - undefined:'undefined', - 'function':function( fn ) { - var ret = 'function', - name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE - if ( name ) - ret += ' ' + name; - ret += '('; - - ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); - return join( ret, this.parse(fn,'functionCode'), '}' ); - }, - array: array, - nodelist: array, - arguments: array, - object:function( map ) { - var ret = [ ]; - this.up(); - for ( var key in map ) - ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); - this.down(); - return join( '{', ret, '}' ); - }, - node:function( node ) { - var open = this.HTML ? '<' : '<', - close = this.HTML ? '>' : '>'; - - var tag = node.nodeName.toLowerCase(), - ret = open + tag; - - for ( var a in this.DOMAttrs ) { - var val = node[this.DOMAttrs[a]]; - if ( val ) - ret += ' ' + a + '=' + this.parse( val, 'attribute' ); - } - return ret + close + open + '/' + tag + close; - }, - functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function - var l = fn.length; - if ( !l ) return ''; - - var args = Array(l); - while ( l-- ) - args[l] = String.fromCharCode(97+l);//97 is 'a' - return ' ' + args.join(', ') + ' '; - }, - key:quote, //object calls it internally, the key part of an item in a map - functionCode:'[code]', //function calls it internally, it's the content of the function - attribute:quote, //node calls it internally, it's an html attribute value - string:quote, - date:quote, - regexp:literal, //regex - number:literal, - 'boolean':literal - }, - DOMAttrs:{//attributes to dump from nodes, name=>realName - id:'id', - name:'name', - 'class':'className' - }, - HTML:true,//if true, entities are escaped ( <, >, \t, space and \n ) - indentChar:' ',//indentation unit - multiline:true //if true, items in a collection, are separated by a \n, else just a space. - }; + function quote( str ) { + return '"' + str.toString().replace(/"/g, '\\"') + '"'; + }; + function literal( o ) { + return o + ''; + }; + function join( pre, arr, post ) { + var s = jsDump.separator(), + base = jsDump.indent(), + inner = jsDump.indent(1); + if ( arr.join ) + arr = arr.join( ',' + s + inner ); + if ( !arr ) + return pre + post; + return [ pre, inner + arr, base + post ].join(s); + }; + function array( arr, stack ) { + var i = arr.length, ret = Array(i); + this.up(); + while ( i-- ) + ret[i] = this.parse( arr[i] , undefined , stack); + this.down(); + return join( '[', ret, ']' ); + }; - return jsDump; + var reName = /^function (\w+)/; + + var jsDump = { + parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance + stack = stack || [ ]; + var parser = this.parsers[ type || this.typeOf(obj) ]; + type = typeof parser; + var inStack = inArray(obj, stack); + if (inStack != -1) { + return 'recursion('+(inStack - stack.length)+')'; + } + //else + if (type == 'function') { + stack.push(obj); + var res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + // else + return (type == 'string') ? parser : this.parsers.error; + }, + typeOf:function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if (typeof obj === "undefined") { + type = "undefined"; + } else if (QUnit.is("RegExp", obj)) { + type = "regexp"; + } else if (QUnit.is("Date", obj)) { + type = "date"; + } else if (QUnit.is("Function", obj)) { + type = "function"; + } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { + type = "window"; + } else if (obj.nodeType === 9) { + type = "document"; + } else if (obj.nodeType) { + type = "node"; + } else if ( + // native arrays + toString.call( obj ) === "[object Array]" || + // NodeList objects + ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) + ) { + type = "array"; + } else { + type = typeof obj; + } + return type; + }, + separator:function() { + return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; + }, + indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing + if ( !this.multiline ) + return ''; + var chr = this.indentChar; + if ( this.HTML ) + chr = chr.replace(/\t/g,' ').replace(/ /g,' '); + return Array( this._depth_ + (extra||0) ).join(chr); + }, + up:function( a ) { + this._depth_ += a || 1; + }, + down:function( a ) { + this._depth_ -= a || 1; + }, + setParser:function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote:quote, + literal:literal, + join:join, + // + _depth_: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers:{ + window: '[Window]', + document: '[Document]', + error:'[ERROR]', //when no parser is found, shouldn't happen + unknown: '[Unknown]', + 'null':'null', + 'undefined':'undefined', + 'function':function( fn ) { + var ret = 'function', + name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE + if ( name ) + ret += ' ' + name; + ret += '('; + + ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); + return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); + }, + array: array, + nodelist: array, + arguments: array, + object:function( map, stack ) { + var ret = [ ]; + QUnit.jsDump.up(); + for ( var key in map ) { + var val = map[key]; + ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack)); + } + QUnit.jsDump.down(); + return join( '{', ret, '}' ); + }, + node:function( node ) { + var open = QUnit.jsDump.HTML ? '<' : '<', + close = QUnit.jsDump.HTML ? '>' : '>'; + + var tag = node.nodeName.toLowerCase(), + ret = open + tag; + + for ( var a in QUnit.jsDump.DOMAttrs ) { + var val = node[QUnit.jsDump.DOMAttrs[a]]; + if ( val ) + ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); + } + return ret + close + open + '/' + tag + close; + }, + functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function + var l = fn.length; + if ( !l ) return ''; + + var args = Array(l); + while ( l-- ) + args[l] = String.fromCharCode(97+l);//97 is 'a' + return ' ' + args.join(', ') + ' '; + }, + key:quote, //object calls it internally, the key part of an item in a map + functionCode:'[code]', //function calls it internally, it's the content of the function + attribute:quote, //node calls it internally, it's an html attribute value + string:quote, + date:quote, + regexp:literal, //regex + number:literal, + 'boolean':literal + }, + DOMAttrs:{//attributes to dump from nodes, name=>realName + id:'id', + name:'name', + 'class':'className' + }, + HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) + indentChar:' ',//indentation unit + multiline:true //if true, items in a collection, are separated by a \n, else just a space. + }; + + return jsDump; })(); -})(this); +// from Sizzle.js +function getText( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += getText( elem.childNodes ); + } + } + + return ret; +}; + +//from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; +} + +/* + * Javascript Diff Algorithm + * By John Resig (http://ejohn.org/) + * Modified by Chu Alan "sprite" + * + * Released under the MIT license. + * + * More Info: + * http://ejohn.org/projects/javascript-diff-algorithm/ + * + * Usage: QUnit.diff(expected, actual) + * + * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" + */ +QUnit.diff = (function() { + function diff(o, n) { + var ns = {}; + var os = {}; + + for (var i = 0; i < n.length; i++) { + if (ns[n[i]] == null) + ns[n[i]] = { + rows: [], + o: null + }; + ns[n[i]].rows.push(i); + } + + for (var i = 0; i < o.length; i++) { + if (os[o[i]] == null) + os[o[i]] = { + rows: [], + n: null + }; + os[o[i]].rows.push(i); + } + + for (var i in ns) { + if ( !hasOwn.call( ns, i ) ) { + continue; + } + if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { + n[ns[i].rows[0]] = { + text: n[ns[i].rows[0]], + row: os[i].rows[0] + }; + o[os[i].rows[0]] = { + text: o[os[i].rows[0]], + row: ns[i].rows[0] + }; + } + } + + for (var i = 0; i < n.length - 1; i++) { + if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && + n[i + 1] == o[n[i].row + 1]) { + n[i + 1] = { + text: n[i + 1], + row: n[i].row + 1 + }; + o[n[i].row + 1] = { + text: o[n[i].row + 1], + row: i + 1 + }; + } + } + + for (var i = n.length - 1; i > 0; i--) { + if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && + n[i - 1] == o[n[i].row - 1]) { + n[i - 1] = { + text: n[i - 1], + row: n[i].row - 1 + }; + o[n[i].row - 1] = { + text: o[n[i].row - 1], + row: i - 1 + }; + } + } + + return { + o: o, + n: n + }; + } + + return function(o, n) { + o = o.replace(/\s+$/, ''); + n = n.replace(/\s+$/, ''); + var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); + + var str = ""; + + var oSpace = o.match(/\s+/g); + if (oSpace == null) { + oSpace = [" "]; + } + else { + oSpace.push(" "); + } + var nSpace = n.match(/\s+/g); + if (nSpace == null) { + nSpace = [" "]; + } + else { + nSpace.push(" "); + } + + if (out.n.length == 0) { + for (var i = 0; i < out.o.length; i++) { + str += '' + out.o[i] + oSpace[i] + ""; + } + } + else { + if (out.n[0].text == null) { + for (n = 0; n < out.o.length && out.o[n].text == null; n++) { + str += '' + out.o[n] + oSpace[n] + ""; + } + } + + for (var i = 0; i < out.n.length; i++) { + if (out.n[i].text == null) { + str += '' + out.n[i] + nSpace[i] + ""; + } + else { + var pre = ""; + + for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { + pre += '' + out.o[n] + oSpace[n] + ""; + } + str += " " + out.n[i].text + nSpace[i] + pre; + } + } + } + + return str; + }; +})(); + +})(this); \ No newline at end of file diff --git a/lib/browser/postal.diagnostics.min.gz.js b/lib/browser/postal.diagnostics.min.gz.js index 58d371d..f7228e7 100644 Binary files a/lib/browser/postal.diagnostics.min.gz.js and b/lib/browser/postal.diagnostics.min.gz.js differ diff --git a/lib/browser/postal.js b/lib/browser/postal.js index 65a6ed0..42e37e7 100644 --- a/lib/browser/postal.js +++ b/lib/browser/postal.js @@ -147,14 +147,6 @@ SubscriptionDefinition.prototype = { return this; }, - whenHandledThenExecute: function(callback) { - if(! _.isFunction(callback)) { - throw "Value provided to 'whenHandledThenExecute' must be a function"; - } - this.onHandled = callback; - return this; - }, - withConstraint: function(predicate) { if(! _.isFunction(predicate)) { throw "Predicate constraint must be a function"; @@ -191,7 +183,9 @@ SubscriptionDefinition.prototype = { } var fn = this.callback; this.callback = function(data) { - setTimeout(fn, milliseconds, data); + setTimeout(function(){ + fn(data); + }, milliseconds); }; return this; }, @@ -405,6 +399,17 @@ var postal = { }); }); return result; + }, + + reset: function() { + // we check first in case a custom bus or resolver + // doesn't expose subscriptions or a resolver cache + if(postal.configuration.bus.subscriptions) { + postal.configuration.bus.subscriptions = {}; + } + if(postal.configuration.resolver.cache) { + postal.configuration.resolver.cache = {}; + } } }; diff --git a/lib/browser/postal.min.gz.js b/lib/browser/postal.min.gz.js index b8bfca0..8c43ca7 100644 Binary files a/lib/browser/postal.min.gz.js and b/lib/browser/postal.min.gz.js differ diff --git a/lib/browser/postal.min.js b/lib/browser/postal.min.js index 53e359e..261f99e 100644 --- a/lib/browser/postal.min.js +++ b/lib/browser/postal.min.js @@ -1 +1 @@ -(function(a,b,c){typeof define=="function"&&define.amd?define(["underscore"],function(d){return c(d,a,b)}):c(a._,a,b)})(this,document,function(a,b,c,d){var e="/",f=50,g=0,h="postal",i=function(){},j=function(){var b;return function(c){var d=!1;return a.isString(c)?(d=c===b,b=c):(d=a.isEqual(c,b),b=a.clone(c)),!d}},k=function(a,b){this.channel=a||e,this._topic=b||""};k.prototype={subscribe:function(){var a=arguments.length;if(a===1)return new l(this.channel,this._topic,arguments[0]);if(a===2)return new l(this.channel,arguments[0],arguments[1])},publish:function(a){var b={channel:this.channel,topic:this._topic,data:a};a.topic&&a.data&&(b=a,b.channel=b.channel||this.channel),b.timeStamp=new Date,p.configuration.bus.publish(b)},topic:function(a){return a===this._topic?this:new k(this.channel,a)}};var l=function(a,b,c){this.channel=a,this.topic=b,this.callback=c,this.priority=f,this.constraints=new Array(0),this.maxCalls=g,this.onHandled=i,this.context=null,p.configuration.bus.publish({channel:h,topic:"subscription.created",timeStamp:new Date,data:{event:"subscription.created",channel:a,topic:b}}),p.configuration.bus.subscribe(this)};l.prototype={unsubscribe:function(){p.configuration.bus.unsubscribe(this),p.configuration.bus.publish({channel:h,topic:"subscription.removed",timeStamp:new Date,data:{event:"subscription.removed",channel:this.channel,topic:this.topic}})},defer:function(){var a=this.callback;return this.callback=function(b){setTimeout(a,0,b)},this},disposeAfter:function(b){if(a.isNaN(b)||b<=0)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var c=this.onHandled,d=a.after(b,a.bind(function(){this.unsubscribe(this)},this));return this.onHandled=function(){c.apply(this.context,arguments),d()},this},ignoreDuplicates:function(){return this.withConstraint(new j),this},whenHandledThenExecute:function(b){if(!a.isFunction(b))throw"Value provided to 'whenHandledThenExecute' must be a function";return this.onHandled=b,this},withConstraint:function(b){if(!a.isFunction(b))throw"Predicate constraint must be a function";return this.constraints.push(b),this},withConstraints:function(b){var c=this;return a.isArray(b)&&a.each(b,function(a){c.withConstraint(a)}),c},withContext:function(a){return this.context=a,this},withDebounce:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=a.debounce(c,b),this},withDelay:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=function(a){setTimeout(c,b,a)},this},withPriority:function(b){if(a.isNaN(b))throw"Priority must be a number";return this.priority=b,this},withThrottle:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=a.throttle(c,b),this},subscribe:function(a){return this.callback=a,this}};var m={cache:{},compare:function(a,b){if(this.cache[b]&&this.cache[b][a])return!0;var c=new RegExp("^"+a.replace(/\./g,"\\.").replace(/\*/g,".*").replace(/#/g,"[A-Z,a-z,0-9]*")+"$"),d=c.test(b);return d&&(this.cache[b]||(this.cache[b]={}),this.cache[b][a]=!0),d}},n={subscriptions:{},wireTaps:new Array(0),publish:function(b){a.each(this.wireTaps,function(a){a(b.data,b)}),a.each(this.subscriptions[b.channel],function(c){a.each(c,function(c){p.configuration.resolver.compare(c.topic,b.topic)&&a.all(c.constraints,function(a){return a(b.data)})&&typeof c.callback=="function"&&(c.callback.apply(c.context,[b.data,b]),c.onHandled())})})},subscribe:function(a){var b,c,d,e=this.subscriptions[a.channel],f;e||(e=this.subscriptions[a.channel]={}),f=this.subscriptions[a.channel][a.topic],f||(f=this.subscriptions[a.channel][a.topic]=new Array(0)),b=f.length-1;for(;b>=0;b--)if(f[b].priority<=a.priority){f.splice(b+1,0,a),c=!0;break}return c||f.unshift(a),a},unsubscribe:function(a){if(this.subscriptions[a.channel][a.topic]){var b=this.subscriptions[a.channel][a.topic].length,c=0;for(;c=0;b--)if(f[b].priority<=a.priority){f.splice(b+1,0,a),c=!0;break}return c||f.unshift(a),a},unsubscribe:function(a){if(this.subscriptions[a.channel][a.topic]){var b=this.subscriptions[a.channel][a.topic].length,c=0;for(;c