Source: easy-test.js

/**
 * Framework for making Angular Unit Test's easier to write.
 */
(function() {
  'use strict';

  /**
   * @class EasyTest
   */
  function EasyTest() {}

  /**
   * @function injectify
   * @memberof EasyTest
   * @description
   *
   * Injects a number of services and returns an object that has, as its
   * properties, the injected services.
   *
   * @param {array} injectibles An array of the names of the services to inject.
   *
   * @returns {object} An object that has, as its properties, the services that
   * were injected.
   *
   * @example
   * var services = EasyTest.injectify(['MyServiceOne', 'MyServiceTwo']);
   * expect(services.MyServiceOne).to.be.an.instanceOf(MyServiceOne);
   */
  EasyTest.injectify = function injectify(injectibles) {
    var obj = [];
    injectibles.push(function() {
      for (var i = 0, l = arguments.length; i < l; i++) {
        obj[injectibles[i]] = arguments[i];
      }
    });
    inject(injectibles);
    return obj;
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Mocks a specified module that has already been registered with angular.
   * Optionally registers a number of services with the mocked module.
   *
   * @param {string} moduleName The name of the module to mock.
   * @param {array | object} services An array of objects representing fake
   * services to register with the module. Each object must have a `name` property
   * representing the name of the service, and then one of `provider`, `constant`,
   * `value`, `factory`, or `service`. Alternately, you can supply an object matching
   * names to factories as this is typically the most default use case.
   *
   * @example
   * // Mock MyModule and provide the passed fake services to it.
   * EasyTest.mockModule('MyModule', [{
   *   name: 'ServiceName',
   *   factory: function() {}
   * },{
   *   name: 'MyFakeConstant',
   *   constant: 1337
   * }]);
   *
   * @example
   * // Mock MyModule and provide the passed fake services to it.
   * EasyTest.mockModule('MyOtherModule', {
   *   FooFactory: { foo: function() {} },
   *   BarFactory: { bar: function() {} }
   * });
   */
  EasyTest.mockModule = function mockModule(moduleName, services) {
    services = services || [];

    // if services is just a hash, convert to an array of all factories
    services = Array.isArray(services) ? services :
      Object.keys(services).map(function(name) {
        return {
          name: name,
          factory: services[name]
        };
      });

    // iterate over each service, mocking it by type
    var mockTypes = [ 'factory', 'service', 'provider', 'value', 'constant' ];
    angular.mock.module(moduleName, function($provide) {
      services.forEach(function serviceProvide(service) {
        mockTypes.forEach(function mockIfExists(mocktype) {
          if (service[mocktype]) {
            if (mocktype === 'factory' || mocktype === 'service') {
              // if mocktype is factory or service, mock it into $getFn
              $provide[mocktype](service.name, [ function() {
                return service[mocktype];
              }]);
            }
            else {
              // else, mock normally (?)
              $provide[mocktype](service.name, service[mocktype]);
            }
          }
        });
      });
    });
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Mocks a series of modules. The arguments that are passed into this function
   * can be passed in a very flexible manner. String arguments will be split
   * on their spaces and mocked directly, object arguments will be expected to
   * have a `name` property and `values` property which is an array of the
   * values to be provided for the module. See the {@link EasyTest.mockModule}
   * function for more information on how that works.
   *
   * @param {string|object} arguments A series of string or object parameters.
   *
   * @example
   * // Simply mock three modules
   * EasyTest.mockModules('one two three');
   * // or
   * EasyTest.mockModules('one two', 'three');
   * // etc, can mix and match.
   *
   * @example
   * // Mock two simple modules and a third with a fake service.
   * EasyTest.mockModules('one two', { name: 'three', values: [{
   *   name: 'FakeService',
   *   service: { get: function() {} }
   * }]});
   */
  EasyTest.mockModules = function mockModules() {
    var modules =  Array.prototype.slice.call(arguments, 0);
    modules.forEach(function(arg) {
      if (typeof arg === 'string') {
        return arg.split(' ').forEach(function(moduleName) {
          angular.mock.module(moduleName); // Go directly to angular.mock
        });
      }
      EasyTest.mockModule(arg.name, arg.values);
    });
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Injects a specific service and returns a reference to it.
   *
   * @param {string} serviceName The name of the service to inject.
   *
   * @returns {object} The service that was injected.
   *
   * @example
   * var $q = EasyTest.getService('$q');
   */
  EasyTest.getService = function getService(serviceName) {
    return EasyTest.injectify([ serviceName ])[serviceName];
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Creates a 'test context' for a particular controller. Note that currently
   * you need to mock any modules that this controller may need beforehand.
   * See the {@link EasyTest.mockModule} function.
   *
   * @param {string} controller The name of the controller to load and create a context for.
   *
   * @returns {object} An object with a $scope and controller property representing the
   * controller that has been loaded and its scope.
   *
   * @example
   * var context = EasyTest.createTextContext('MyControllerName');
   * expect(context.$scope).to.have.property('MyExpectedProperty');
   * expect(context.controller.myFunc).to.be.a('function');
   */
  EasyTest.createTestContext = function createTestContext(controller) {
    var testContext = {};
    inject(function($rootScope, $controller) {
      var $scope = $rootScope.$new();
      testContext = {
        $scope: $scope,
        controller: $controller(controller, {
          $scope: $scope
        })
      };
    });
    return testContext;
  };

  /**
  * @memberof EasyTest
  * @description
  *
  * Gets a specific controller and returns a reference to it.
  *
  * @param {string} controllerName The name of the controller to get.
  *
  * @returns {object} The controller that was retreived.
  *
  * @example
  * var myController = EasyTest.getController('myController');
  */
  EasyTest.getController = function getController(controllerName) {
    return EasyTest.createTestContext(controllerName).controller;
  };

  /**
  * @memberof EasyTest
  * @description
  *
  * Gets a specific controller and returns a reference to it. Use this
  * instead of `getController` if the controller belongs to a directive
  * that has `bindToController` set to true.
  *
  * @param {string} controllerName The name of the controller to get.
  *
  * @param {object} scope a object that will be inserted into the controller's 
  * scope.
  *
  * @returns {object} The controller that was retreived.
  *
  * @example
  * var scope = { value: 'some value' };
  * var myController = EasyTest.getBoundController('myController', scope);
  */
  EasyTest.getBoundController = function getBoundController(controllerName, scope) {
    var controller = {};

    inject(function($rootScope, $controller) {
      // The third arg for $controller is an undocumented parameter
      // that allows us to delay the instantiation of the controller.
      // This approach may break in a future version of angular, but it is
      // currently the only way to deal with `bindToController` controllers.
      controller = $controller(controllerName, {
        $scope: $rootScope.$new()
      }, true);
    });

    if (scope) {
      angular.extend(controller.instance, scope);
    }

    // Instantiate the controller.
    return controller();
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Compiles a directive and returns the top level element in the compiled
   * directive's HTML. Note that currently you need to mock any modules that
   * this directive may need beforehand. See the {@link EasyTest.mockModule} function.
   *
   * @param {string} directiveHTML The HTML of the directive to be compiled.
   * @param {object} scope a dictionary of variables that will be inserted into the element's scope.
   * @param {HTMLElement|angular.element} parent an element to attach the compiled directive as a child.
   *  If <code>scope</code> is an HTMLElement or an Angular element, it will be treated as the parent.
   *
   * @returns {HTMLElement} The top level HTML element of the compiled directive.
   *
   * @example
   * var element = EasyTest.compileDirective('<p directive></p>');
   * expect(element.length).to.equal(expectedLength);
   *
   * @example
   * var scope = { a: 1, b: 'something' };
   * var element = EasyTest.compileDirective('<p>{{a}} {{b}}</p>', scope);
   * expect(element.text()).to.equal('1 something');
   *
   * @example
   * var scope = { foo: 'bar' };
   * var parent = angular.element('<div></div>');
   * var element = EasyTest.compileDirective('<p>{{foo}}</p>', scope, parent);
   * expect(angular.element(parent.children()[0]).text()).to.equal('bar');
   *
   * @example
   * var parent = angular.element('<div></div>');
   * var element = EasyTest.compileDirective('<p>baz</p>', parent);
   * expect(angular.element(parent.children()[0]).text()).to.equal('baz');
   */
  EasyTest.compileDirective = function compileDirective(directiveHTML, scope, parent) {
    // if scope is a Node or angular.element, set parent to scope 
    //  and ignore parent
    if (Node.prototype.isPrototypeOf(scope) || 
        angular.element.prototype.isPrototypeOf(scope)) {
      parent = scope;
      scope = null;
    }

    var $el;
    inject(function($compile, $rootScope) {
      var $scope = $rootScope.$new();
      if (scope) {
        angular.extend($scope, scope);
      }
      $el = angular.element(directiveHTML);

      if (parent) {
        // since angular.element(foo) doesn't do anything if foo is
        //  already an angular.element, we can simply call it anyways
        parent = angular.element(parent);
        parent.append($el);
      }

      $compile($el)($scope);
      $scope.$digest();
    });
    return $el;
  };

  /**
  * @memberof EasyTest
  * @description
  *
  * Allows access to the controller scope for directive that has
  * `bindToController` set to true.
  *
  * @param {object} angularElement An angular element gotten from compiled
  * HTML.
  *
  * @returns {object} The scope of the bound controller.
  */
  EasyTest.getBoundScope = function getBoundScope(angularElement) {
    return angularElement.data('$scope').$$childHead.ctrl;
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Tests an HTMLElement or Angular/jQuery element to see if it contains
   * some attribute, optionally with some given value.
   *
   * @param {HTMLElement|angular.element} element the element to be tested.
   * @param {string} attr the name of the attribute being tested.
   * @param {string} val optional: the value of the attribute being tested.
   *
   * @returns {boolean} Whether the element contains the attribute `attr`.
   * If `val` is provided, returns true if and only if the value of `attr` is
   * also `val`.
   *
   * @example
   * var element = EasyTest.compileDirective('<p foo="bar"></p>');
   * expect(EasyTest.hasAttr(element[0], 'foo')).to.be.true;
   * expect(EasyTest.hasAttr(element[0], 'foo', 'bar')).to.be.true;
   */
  EasyTest.hasAttr = function hasAttr(element, attr, val) {
    element = angular.element.prototype.isPrototypeOf(element) ? 
      element[0] : element;

    var attrs = element.attributes;
    for (var k in attrs) {
      var objAttr = attrs[k];
      if ((objAttr.name === attr) && (typeof val === 'undefined' || objAttr.value === val)) {
        return true;
      }
    }
    return false;
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Tests an object against a JSON specification object.
   *
   * @param {object} object The object to test.
   * @param {object} spec The 'specification' to test the object against. Should
   * have the expected types as properties and the names of the properties with
   * that type as their value, separated by spaces.
   *
   * @returns {object} An error object if anything failed, nothing if it passed.
   *
   * @example
   * // Using Chai you could pass the result directly back to 'done'
   * it('test object', function(done) {
   *   var res = EasyTest.looksLike(object, {
   *     'function': 'funcOne funcTwo',
   *     'number': 'numberOne numberTwo numberThree'
   *   });
   *   done(res);
   * });
   */
  EasyTest.looksLike = function looksLike(object, spec) {
    var types = Object.getOwnPropertyNames(spec);
    for (var i = 0; i < types.length; i++) {
      var type = types[i];
      var properties = spec[type].split(' ');
      for (var y = 0; y < properties.length; y++) {
        var property = properties[y];
        if (!(property in object)) {
          return new Error('Expected object to have the property \'' +
                           property + '\'.');
        }
        if (typeof object[property] !== type) {
          return new Error('Expected property \'' + property + '\' to be of ' +
                           'type ' + type + '.');
        }
      }
    }
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Tests a controller's scope by seeing if it conforms to a JSON specficiation
   * object. See {@link EasyTest.looksLike} for more information.
   *
   * @param {string|object} context The name of the controller to create a test
   * context for or the context itself.
   * @param {array} scopeSpec The scope's spec. An object with the names of the
   * expected types as its keys and a string for its value of all the properties,
   * separated by spaces.
   *
   * @example
   * // Will load the controller and test it using the passed spec.
   * EasyTest.testScope('MyControllerName', {
   *   'function': 'funcOne funcTwo funcThree',
   *   'number': 'numberOne numberTwo',
   *   'boolean': 'hasStuff'
   * });
   */
  EasyTest.testScope = function testScope(context, scopeSpec) {
    if (typeof context === 'string') {
      context = EasyTest.createTestContext(context);
    }
    return EasyTest.looksLike(context.$scope, scopeSpec);
  };

  /**
   * @memberof EasyTest
   * @description
   *
   * Tests a controller to see if it conforms to a JSON specification object.
   * See {@link EasyTest.looksLike} for more information.
   *
   * @param {string|object} controller The name of the controller to create or a
   * reference to a controller.
   * @param {array} spec The controller's spec. An object with the names of the
   * expected types as its keys and a string for its value of all the properties,
   * separated by spaces.
   *
   * @example
   * // Will load the controller and test it using the passed spec.
   * EasyTest.testController('MyControllerName', {
   *   'function': 'funcOne funcTwo funcThree',
   *   'number': 'numberOne numberTwo',
   *   'boolean': 'hasStuff'
   * });
   */
  EasyTest.testController = function testScope(controller, spec) {
    if (typeof controller === 'string') {
      controller = EasyTest.getController(controller);
    }
    return EasyTest.looksLike(controller, spec);
  };

  /**
  * @memberof EasyTest
  * @description
  *
  * Tests a service to see if it conforms to a JSON specification object.
  * See {@link EasyTest.looksLike} for more information.
  *
  * @param {string|object} service The name of the service to create or a
  * reference to a service.
  * @param {array} spec  The service's spec. An object with the names of the
  * expected types as its keys and a string for its value of all the properties,
  * separated by spaces.
  *
  * @example
  * // Will load the service and test it using the passed spec.
  * EasyTest.testService('MyServiceName', {
  *   'function': 'funcOne funcTwo funcThree',
  *   'number': 'numberOne numberTwo',
  *   'boolean': 'hasStuff'
  * });
  */
  EasyTest.testService = function testScope(service, spec) {
    if (typeof service === 'string') {
      service = EasyTest.getService(service);
    }
    return EasyTest.looksLike(service, spec);
  };

  window.EasyTest = EasyTest;

}());