curl-amd
Version:
curl.js is small, fast, extensible module loader that handles AMD, CommonJS Modules/1.1, CSS, HTML/text, and legacy scripts.
320 lines (278 loc) • 9.91 kB
JavaScript
/** MIT License (c) copyright 2010-2013 B Cavalier & J Hann */
/**
* curl createContext module
*
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*
*/
define(['curl', 'curl/_privileged', './undefine'], function (curl, priv, undefine) {
;
var Promise, runQueue, undef;
Promise = priv['Promise'];
/**
* Creates an objet with a "run" function and an "undefine" function.
* The run function accepts a "testing" function that can be
* used to test asynchronous modules. (Note: curl.js doesn't
* supply or advocate any particular testing framework.) If provided,
* the "setup" and "teardown" functions will execute immediately before
* and after the testing function.
*
* The setup function is provided a `require` function as its first
* parameter. The setup function may be used to
* `define` or `require` any mock or stub modules that the developer
* deems necessary to isolate the functionality of the module being
* tested. Mocks or stubs may also be created in the testing function.
* However, sync `require` behavior (R-value require) is not supported
* inside the testing function. (Note: async require *is* supported
* inside the module being tested!)
*
* If the setup function needs to perform async operations, it must
* supply a second `done` parameter and call it when it is done
* performaing async tasks. This callback also has a promise-like
* interface for developers who prefer to work with promises. If this
* parameter is not supplied by the developer, the setup function is
* assumed to be synchronous.
*
* Note: if any modules (or plugin-based resources) are fetched using
* the `require` supplied to the setup, testing, or teardown functions,
* the functions will wait for the required modules/resources to
* resolve before proceeding. In summary: the developer does *not*
* need to provide the `done` parameter if using the provided `require`.
*
* There can be several testing functions. Each one should be passed
* individually to the run function.
*
* The teardown function can be used to clean up any resources used
* in the setup or testing functions. Any modules/resources created
* by the supplied `require` or the standard `define` are cleaned up
* automatically. So, in general, you should only need to supply a
* teardown function if your code creates non-AMD resources.
*
* The testing functions and the teardown function take the same
* parameters as the setup function. Be sure to supply and call the
* second `done` parameter if these functions perform async tasks.
*
* All functions may run async. Your testing must be able to handle
* async tests or it wil likely fail.
*
* Each of the testing functions is run in isolation from the others
* (and in isolation from any other testing functions created by other
* invocations of the runner function). Actually, the functions are
* sequenced temporally so they don't execute at the same time. In
* addition, at the end of each function's execution, it will restore
* curl.js's cache back to the state it was before execution, so each
* function can be assured it has a clean environment.
*
* The undefine function can be used to explicitly undefine a
* module or resource inside a testing function (or anywhere else).
*
* Promises returned by curl.js or this runner module are *not*
* compliant to the CommonJS Promises/A standard. Use a library
* such as when.js (http://github.com/cujojs/when) to create compliant
* promises. See example 2.
*
* @param [require] {Function}
* @param [setup] {isolatedFunction} if defined, this
* function will be run immediately before the returned function.
* @param [teardown] {isolatedFunction} if defined, this
* function will be run immediately after the returned function.
* @returns {Object}
* @returns {Object.run} function run (testFunc, callback) {}
* @returns {Object.undefine} function run (idOrArray) {}
*
* @example 1
*
* define(['curl/tdd/runner', 'require'], function (runner, require) {
* var r1;
*
* // no need for `done` callback if using supplied `require`.
* function setup (require) {
* var xhrResult = [{ foo: 'bar' }];
* // load and configure some mocks
* require(['mocks/xhr', 'mocks/rpc'], function (xhr, rpc) {
* define('my/xhr', xhr.config(xhrResult));
* define('my/rpc', rpc.config(xhr));
* });
* // define any other resources
* define('my/transform', function () {
* return function (val) { return val; };
* });
* }
*
* // configure runner.
* // (no need for teardown since mocks were created using supplied
* // `require` and standard `define`)
* r1 = runner(require, setup);
*
* // queue a testing function to be run.
* // setup will be run beforehand and all modules will be
* // cleaned up automatically.
* r1.run(function (require) {
* require(['my/module1-to-test'], function (m1) {
* // perform assertions here
* });
* });
*
* // queue another testing function to be run.
* // setup will be run beforehand and all modules will be
* // cleaned up automatically.
* r1.run(function (require) {
* define('my/other-data-to-test-with', { name: 'Fred' });
* require(['my/module2-to-test'], function (m2) {
* // perform assertions here
* });
* });
* });
*
* @example 2
*
* define(['curl/tdd/runner', 'require', 'when'], function (runner, require, when) {
* // convert a returned promise to a Promises/A promise using when.js
* var cjsPromise = when(r1.run(null, function () {
* // do tests here
* });
* });
*
*/
return function runner (require, setup, teardown) {
var promise;
if (!require) require = curl;
function run (testFunc) {
var cacheSnapshot, callback;
callback = arguments[1];
// enqueue cache snapshot
enqueue(function _copyCache () {
cacheSnapshot = copyCache(priv['cache']);
});
// enqueue setup
if (setup) enqueue(waitForAsyncTasks(setup, require));
// enqueue testFunc
enqueue(waitForAsyncTasks(testFunc, require));
// enqueue teardown
if (teardown) enqueue(waitForAsyncTasks(teardown, require));
promise = new Promise();
// enqueue cache restore and a hook for outside code
enqueue(function _restoreCache () {
restoreCache(priv['cache'], cacheSnapshot);
if (callback) callback();
promise.resolve();
});
return promise;
}
return {
run: run,
undefine: undefine
};
};
/**
* The signature of the functions supplied to runner.
* @param require {Function} standard AMD `require`
* @param [done] {Function|Promise} if included in the function
* parameters, this callback must be called when all async tasks
* are completed. This function has a promise-like interface,
* including `done.resolve(val)` and `done.reject(ex)` for
* those who would rather use promises.
*/
function isolatedFunction (require) {}
/**
* Enqueues a function that provides a promise.
* @private
* @param promiseProvider {Function} must return a promise if it
* has async tasks.
*/
function enqueue (promiseProvider) {
var next;
function dequeue () {
when(promiseProvider(), next.resolve, next.reject);
}
next = new Promise();
when(runQueue, dequeue, runQueue && runQueue.reject);
runQueue = next;
}
/**
* Waits for async require calls and/or done.
* @private
* @param func {Function}
* standard signature is `function (require, [done]) {}`
*/
function waitForAsyncTasks (func, require) {
var promise, otherAsyncDone, requiresDone, trackedRequire;
promise = new Promise();
otherAsyncDone = new Promise();
requiresDone = new Promise();
trackedRequire = createTrackedRequire(require, requiresDone.resolve);
// last param is `done`
if (func.length > 1) {
// turn otherAsyncDone into a dual callback/promise thingy
otherAsyncDone = (function (promise) {
var dual = promise.resolve;
dual.resolve = promise.resolve;
dual.reject = promise.reject;
dual.then = promise.then;
return dual;
}(otherAsyncDone));
}
else {
// pre-resolve
otherAsyncDone.resolve();
}
// wait for promises
requiresDone.then(function _otherAsyncDone () {
otherAsyncDone.then(promise.resolve, promise.reject);
});
// return a queueable function
return function _waitForAsyncTasks () {
// call function
func(trackedRequire, otherAsyncDone);
// check if there were no async `require` calls
if (trackedRequire.notAsync()) {
requiresDone.resolve();
}
// return promise
return promise;
};
}
function createTrackedRequire (require, modulesAllFetched) {
var callCount = 0;
function trackedRequire (idOrArray, callback) {
var cb;
callCount++;
cb = function () {
callback.apply(this, arguments);
// if this is the last require
if (--callCount == 0) modulesAllFetched();
};
return require(idOrArray, cb);
}
// preserve AMD API
trackedRequire.toUrl = require.toUrl;
// helpful
trackedRequire.notAsync = function () { return callCount == 0; };
return trackedRequire;
}
function copyCache (cache) {
var copy = {};
for (var p in cache) {
copy[p] = cache[p];
}
return copy;
}
function restoreCache (cache, copy) {
for (var p in cache) {
if (!(p in copy)) {
undefine(p);
}
}
}
function when (promiseOrValue, callback, errback, progback) {
// we can't just sniff for then(). if we do, resources that have a
// then() method will make dependencies wait!
if (promiseOrValue && typeof promiseOrValue.then == 'function') {
return promiseOrValue.then(callback, errback, progback);
}
else {
return callback(promiseOrValue);
}
}
});