• jasq.js

  • ¶
    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, 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;
    });