// Jasq v0.4.2 - AMD dependency injector integrated with Jasmine // // https://github.com/biril/jasq // Licensed and freely distributed under the MIT License // Copyright (c) 2013-2014 Alex Lambiris /*jshint browser:true */ /*global define:false, require:false */ define(function () { "use strict"; var // Helpers noOp = function () {}, isString = function (s) { return Object.prototype.toString.call(s) === "[object String]"; }, isFunction = function (f) { return Object.prototype.toString.call(f) === "[object Function]"; }, isStrictlyObject = function (o) { return Object.prototype.toString.call(o) === "[object Object]"; }, each = function (obj, iterator) { var i, l, key; if (!obj) { return; } if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) { obj.forEach(iterator); return; } if (obj.length === +obj.length) { for (i = 0, l = obj.length; i < l; i++) { iterator(obj[i], i, obj); } return; } for (key in obj) { if (obj.hasOwnProperty(key)) { iterator(obj[key], key, obj); } } }, extend = function () { var target = {}; each(arguments, function (source) { each(source, function (v, k) { target[k] = v; }); }); return target; }, // jasmineApiNames = ["describe", "xdescribe", "it", "xit"], // Jasmine's native (non-jasq-patched) global API jasmineNativeApi = {}, // jasmineEnv = null, // jasq = {}, // Get a value indicating whether Jasmine is available on the global scope isJasmineInGlobalScope = function () { return window.jasmine && isFunction(window.jasmine.getEnv); }, // Generate a context-id for given `suiteDescription` / `specDescription` pair createContextId = (function () { var uid = 0; return function (suiteDescription, specDescription) { return suiteDescription + " " + specDescription + " " + (uid++); }; }()), // Re-configure require for context of given id, getting a loader-function. All requirejs // [configuration options](http://requirejs.org/docs/api.html#config), except for the // context itself, are copied over from the default context `_` configRequireForContext = function (contextId) { var c = {}; each(require.s.contexts._.config, function (val, key) { if (key !== "deps") { c[key] = val; } }); c.context = contextId; return require.config(c); }, // ### suiteConfigs // A stack of suite configs, the topmost being the 'current'. For each jasq-`describe` call // (i.e. those that define a module and only those) a config is pushed to the stack which // includes the (name of the) module under test and optionally a mocking function. The module // will be made available to all specs defined within _that_ (or any _nested_) suite. The // mocking function, if present in the configuration, will be invoked on every spec to // instantiate mocks. (Mocks defined on the spec itself (in the specConfig provided during the // invocation of `it`) will override those defined in the suiteConfig) // suiteConfigs = (function () { var sc = []; // Get the current suite-config. Or a falsy value if no such thing sc.getCurrent = function () { return sc[sc.length - 1]; }; // Get the path of the current suite-config. Or an empty array if no such thing // A suite's path is defined as an array of suite descriptions where // * `path[0]`: descr. of the top-level suite == (n-1)th parent of current suite // * `path[1]`: descr. of (n-2)th parent of current suite // * `path[path.length - 1]`: descr. of current suite sc.getCurrentPath = function () { var p = []; each(sc, function () { p.push(sc.description); }); return p; }; return sc; }()), // ### createJasqSpec // Create a function to execute the spec of given `specDescription` and `specConfig` after // (re)loading the tested module and mocking its dependencies as specified at the (current) // suite and (given) spec level // createJasqSpec = function (specDescription, specConfig) { var contextId, load, suiteConfig, mock; // Mods will load in a new requirejs context, specific to this spec. This is its id contextId = createContextId(suiteConfigs.getCurrentPath(), specDescription); // Create the context, configuring require appropriately and obtaining a loader load = configRequireForContext(contextId); // Configuration of current suite (name of module to load & mock function) suiteConfig = suiteConfigs.getCurrent(); // Modules to mock, as specified at the suite level as well as the spec level mock = extend(suiteConfig.mock ? suiteConfig.mock() : {}, specConfig.mock); return function (done) { // Re-define modules using given mocks (if any), before they're loaded each(mock, function (mod, modName) { define(modName, mod); }); // And require the tested module load(suiteConfig.moduleName ? [suiteConfig.moduleName] : [], function (module) { // After module & deps are loaded, just run the original spec's expectations. // Dependencies (mocked and non-mocked) should be available through the // `dependencies` hash. (Note that a (shallow) copy of dependencies is passed, to // avoid exposing the original hash that require maintains) specConfig.expect(module, extend(require.s.contexts[contextId].defined), done); // In the event that the expectation-function is _not_ meant to complete // asynchronously (<=> the expectation-function did _not_ 'request' a `done` // argument) then it's already completed. Invoke `done` if (specConfig.expect.length < 3) { done(); } }); }; }, // ### describe // Get the jasq version of Jasmine's `(x)describe` // getJasqDescribe = function (isX) { var jasmineDescribe = jasmineNativeApi[isX ? "xdescribe" : "describe"]; // `(x)describe`, Jasq version // * `suiteDescription`: Description of this suite, as in Jasmine's native `describe` // * `moduleName`: Name of the module to which this test suite refers // * `specify`: The function to execute the suite's specs, as in Jasmine's `describe` // // OR // * `suiteDescription`: Description of this suite, as in Jasmine's native `describe` // * `suiteConfig`: Configuration of the suite. A hash containing // * `moduleName`: Name of the module to which this test suite refers // * `mock`: Optionally a function that returns a hash of mocks // * `specify`: The function to execute the suite's specs // return function (suiteDescription) { var args, suite; // Parse given arguments as if they were suitable for the jasq-version of `describe`. // `args` will contain the expected `moduleName`, `mock` and `specify` properties if they // are, or will be falsy if they're not. In the latter case, just delegate to the native // jasmine version args = (function (args) { // Either `suiteDescription`, `moduleName`, `specify` .. if (isString(args[0]) && isString(args[1]) && isFunction(args[2])) { return { moduleName: args[1], specify: args[2] }; } // .. or `suiteDescription`, `suiteConfig` if (isString(args[0]) && isStrictlyObject(args[1])) { return args[1]; } }(arguments)); if (!args) { return jasmineDescribe.apply(null, arguments); } // Push the current suite-config onto the stack of suite-configs, making it the current // suite-config. All specs (and nested suites) will make use of this configuration. (if // this is an `xdescribe` call, it makes no difference as the suite's specs will never // execute anyway. However, it's simpler to always `push` here and always `pop` later, // avoiding an extra layer of logic) suiteConfigs.push({ description: suiteDescription, moduleName: args.moduleName, mock: args.mock }); // Ultimately, the native Jasmine version is run. The crucial step was setting a // suite-config for further use in specs and nested suites suite = jasmineDescribe(suiteDescription, args.specify); // Pop the current suite-config suiteConfigs.pop(); return suite; }; }, // ### it // Get the jasq version of Jasmine's `(x)it` // getJasqIt = function (isX) { var jasmineIt = jasmineNativeApi[isX ? "xit" : "it"]; // `(x)it`, Jasq version // * `specDescription`: Description of this spec, as in Jasmine's native `it` // * `specConfig`: Configuration of the spec. A hash containing: // * `store`: An array of neames of the modules to 'store': These will be // exposed in the spec through `dependencies.store` - a hash of modules // * `mock`: A hash of mocks, mapping module (name) to mock. These will be // exposed in the spec through `dependencies.mocks` - a hash of modules // * `expect`: The expectation function: A callback to be invoked with // `module` and `dependencies` arguments // return function (specDescription, specConfig) { // In the event that there's no current suite-config (no module to pass to the spec) then // just run the native Jasmine version - this will avoid forcing spec to run // asynchronously. Also run the native version in the case the the caller invoked `xit` - // the spec will not execute so there's no reason to incur the module (re)loading overhead if (!suiteConfigs.getCurrent() || isX) { // We tolerate the caller passing an expectation-hash into a spec which is not nested // within a jasq-suite - in this case we're only interested in the expectation-function if (isStrictlyObject(specConfig)) { specConfig = specConfig.expect; } return jasmineIt.call(null, specDescription, specConfig); } // Create a specConfig, in case the caller passed an expectation-function instead if (!isStrictlyObject(specConfig)) { specConfig = { expect: specConfig }; } // Execute Jasmine's `(x)it` on an appropriately modified _asynchronous_ spec return jasmineIt(specDescription, createJasqSpec(specDescription, specConfig)); }; }, // ### init // Ensure that `jasmineEnv` and `jasmineNativeApi` have been set and create the // patched version of Jasmine's API. Will only run once // init = function () { if (!isJasmineInGlobalScope()) { throw "Jasmine is not available in global scope (not loaded?)"; } // Store Jasmine's globals jasmineEnv = window.jasmine.getEnv(); each(jasmineApiNames, function (name) { jasmineNativeApi[name] = window[name]; }); // Create patched version of Jasmine's API jasq.describe = getJasqDescribe(); jasq.xdescribe = getJasqDescribe(true); jasq.it = getJasqIt(); jasq.xit = getJasqIt(true); each(jasmineApiNames, function (name) { jasq[name].isJasq = true; }); // Don't `init` more than once init = noOp; }; // jasq.applyGlobals = function () { init(); each(jasmineApiNames, function (name) { window[name] = jasq[name]; }); }; // jasq.resetGlobals = function () { init(); each(jasmineApiNames, function (name) { window[name] = jasmineNativeApi[name]; }); }; // If Jasmine is already in global scope then go ahead and apply globals - this will also // initialize jasq if (isJasmineInGlobalScope()) { jasq.applyGlobals(); } return jasq; });