fvanwijk/mox

View on GitHub
src/helpers.js

Summary

Maintainability
C
1 day
Test Coverage
/*******************************
 * Helper functions *
 *******************************/

// Save the current spec for later use (Jasmine 2 compatibility)
var currentSpec;
beforeEach(function () {
  this.isJasmine2 = /^2/.test(jasmine.version);
  if (this.isJasmine2) {
    jasmine.addMatchers(jasmineMoxMatchers.v2);
  } else {
    this.addMatchers(jasmineMoxMatchers.v1);
  }
  currentSpec = this;
});

/*
 * angular-mocks v1.2.x clears the cache after every spec. We do not want this when we compile a template once in a beforeAll
 */
if (typeof beforeAll !== 'undefined')  {
  angular.mock.clearDataCache = angular.noop;
}

/**
 * Copies input. When input seems to be JSON data, it is fastcopied.
 *
 * @param {*} input
 * @returns {*}
 */
function copy(input) {
  if (angular.isObject(input) && !angular.isFunction(input)) {
    return JSON.parse(JSON.stringify(input));
  }
  return angular.copy(input);
}

/*******************************
 * Generic shortcuts for specs *
 *******************************/

/**
 * Create a new scope that is a child of $rootScope.
 *
 * @param {Object} [params] optional variables to bind to the $scope
 * @returns {Object}
 */
function createScope(params) {
  var $scope = mox.inject('$rootScope').$new();
  if (params) {
    angular.extend($scope, params);
  }
  currentSpec.$scope = $scope;
  return $scope;
}

/**
 * Creates a controller and runs the controller function.
 *
 * @param {string} ctrlName controller to create
 * @param {Object} $scope to inject into the created controller. If not given, look if there is a scope created with createScope().
 * @param {Object} [locals] optional local injections
 * @param {Object} [bindings] optional bindings parameter, for testing controllers of directives that use `bindToController`
 * @returns {*}
 */
function createController(ctrlName, $scope, locals, bindings) {
  var scope = $scope || currentSpec.$scope;
  var combinedLocals = angular.extend({ $scope: scope }, locals || {});
  return mox.inject('$controller')(ctrlName, combinedLocals, bindings);
}

/*********************
 * Compile shortcuts *
 *********************/

/**
 * Compile HTML and digest the scope (for example a directive).
 *
 * Example:
 * compileHtml('<p>This is a test</p>', $scope, true);
 *
 * Html added to body:
 *
 * ```
 * <div id="jasmine-fixtures"><p>This is a test</p></div>
 * ```
 *
 * Html added to body when mox.testTemplateAppendSelector = '#container' and mox.testTemplatePath = 'container.html'.
 * Contents of container.html: <div id="#container"><h1>This is a container</h1></div>
 *
 * ```
 * <div id="jasmine-fixtures"><div id="#container"><h1>This is a container</h1><p>This is a test</p></div></div>
 * ```
 *
 * @param {string} html
 * @param {Object} $scope to bind to the element. If not given, look if there is a scope created with createScope().
 * @param {boolean} [appendToBody] is true when the compiled html should be added to the DOM.
 *        Set mox.testTemplatePath to add a template to the body and append the html to the element with selector mox.testTemplateAppendSelector
 * @returns the created element
 */
function compileHtml(html, $scope, appendToBody) {
  $scope = $scope || currentSpec.$scope;
  if (appendToBody === undefined) { appendToBody = true; }

  var element = mox.inject('$compile')(html)($scope);
  var body = angular.element(document.body);
  body.find(mox.testTemplateAppendSelector).remove();
  if (appendToBody) {
    var testTemplate = mox.testTemplatePath ? jasmine.getFixtures().read(mox.testTemplatePath) : angular.element('<div id="mox-container">');
    body.append(testTemplate).find(mox.testTemplateAppendSelector).append(element);
  }

  currentSpec.element = element;
  $scope.$digest();
  return currentSpec.element;
}

/**
 * Compile a template and digest the $scope.
 * When the html will not be added to the body, the html is wrapped in a div.
 *
 * @param {string} template name
 * @param {Object} $scope to bind to the template
 * @returns the created element
 */
function compileTemplate(template, $scope, appendToBody) {
  var html = mox.inject('$templateCache').get(template);
  return compileHtml('<div>' + html + '</div>', $scope, appendToBody);
}

/*********************
 * Promise shortcuts *
 *********************/

function unresolvedPromise() {
  return mox.inject('$q').defer().promise;
}

function promise(result, dontCopy) {
  return mox.inject('$q').when(dontCopy ? result : copy(result));
}

/**
 * A resolved $resource promise must contain $-methods, so JSON-copy is not possible
 */
function resourcePromise(result) {
  return mox.inject('$q').when(angular.copy(result));
}

function reject(error) {
  return mox.inject('$q').reject(error);
}

/**
 * Create a $resource instance mock that is a result from a $resource method
 * The second argument must be the mock that has the $-methods to set on the $resource result
 */
function resourceResult(result, mock) {
  angular.forEach(mock, function (fn, fnName) {
    if (fnName[0] === '$') {
      result[fnName] = fn;
    }
  });

  return {
    $promise: mock ? resourcePromise(result) : promise(result)
  };
}

function nonResolvingResourceResult() {
  return {
    $promise: unresolvedPromise()
  };
}

function rejectingResourceResult(errorMessage) {
  return {
    $promise: reject(errorMessage)
  };
}

/********************************
 * Util functions for viewspecs *
 ********************************/

/**
 * Adds helper functions to an element that simplify element selections.
 * The selection is only performed when the generated helper functions are called, so
 * these work properly with changing DOM elements.
 *
 * Template example:
 *
 * <div>
 *   <div id="header"></div>
 *   <div id="body">
 *     <div class="foo">Foo</div>
 *     <div data-test="bar">
 *       Bar <span class="hl">something</span>
 *     </div>
 *     <div id="num-1-1">Test 1</div>
 *     <div id="num-2-1">Test 2</div>
 *     <div id="num-2-2">Test 3</div>
 *   </div>
 *   <div id="footer">
 *     <h3>Footer <span>title</span></h3>
 *     <div>Footer <span>content</span></div>
 *   </div>
 * </div>
 *
 *
 * Initialisation example:
 *
 * var element = compileHtml(template);
 * addSelectors(element, {
 *   header: '[id="header"]',               // shorthand string notation
 *   body: {                                // full object notation
 *     selector: '#body',                   // element selector
 *     sub: {                               // descendant selectors
 *       foo: '.foo',
 *       bar: {
 *         selector: '[data-test="bar"]',
 *         sub: {
 *           highlight: '.hl'
 *         }
 *       },
 *       num: '[id="num-{0}-{1}"]'          // parameter placeholders can be used
 *     }
 *   },
 *   footer: {
 *     selector: '#footer',
 *     children: [                          // shorthand for child nodes, starting from first node
 *       'heading',                         // shorthand string notation
 *       {                                  // full object notation
 *         name: 'content',
 *         sub: {
 *           innerSpan: 'span'
 *         }
 *       }
 *     ],
 *     sub: {                               // sub and children can be mixed
 *       spans: 'span'                      // (as long as they don't overlap)
 *     }
 *   }
 * });
 *
 *
 * Test examples:
 *
 * expect(element.header()).toExist();
 *
 * expect(element.body()).toExist();
 * expect(element.body().foo()).toExist();
 * expect(element.body().bar()).toExist();
 * expect(element.body().bar().highlight()).toExist();
 * expect(element.body().num(1, 1)).toExist();
 * expect(element.body().num(2, 1)).toExist();
 * expect(element.body().num(2, 2)).toExist();
 *
 * expect(element.footer()).toExist();
 * expect(element.footer().heading()).toExist();
 * expect(element.footer().content()).toExist();
 * expect(element.footer().content().innerSpan()).toExist();
 * expect(element.footer().spans()).toHaveLength(2);
 *
 *
 * @param {Object} element
 * @param {Object} selectors
 * @returns {Object} element
 */
function addSelectors(element, selectors) {

  function checkAndSetFn(obj, prop, fn) {
    var property = obj[prop];
    if (angular.isUndefined(property)) {
      obj[prop] = fn;
    } else if (!(angular.isFunction(property) && property.name === 'moxExtendElement')) {
      throw Error('Property ' + prop + ' already defined on element');
    }
  }

  function addChildFn(element, children) {
    angular.forEach(children, function (child, idx) {
      var name = angular.isObject(child) ? child.name : child;

      checkAndSetFn(element, name, function moxExtendElement() {
        var childElement = element.children().eq(idx);
        addSelectors(childElement, child.sub);
        addChildFn(childElement, child.children);
        return childElement;
      });
    });
  }

  function findElement(element, value, args) {
    if (value) {
      if (angular.isString(value) || value.selector) {
        var replacedSelector = (value.selector || value).replace(/{(\d+)}/g, function (match, group) {
          return args[group];
        });
        return element.find(replacedSelector);
      } else if (value.repeater) {
        var elements = element.find(value.repeater);
        return angular.isDefined(args[0]) ? elements.eq(args[0]) : elements;
      }
    }
    return angular.element(element);
  }

  angular.forEach(selectors, function (value, key) {
    var
      children = value.children,
      sub = value.sub;

    checkAndSetFn(element, key, function moxExtendElement() {
      var foundElement = findElement(element, value, arguments);
      if (!value.repeater || angular.isDefined(arguments[0])) {
        addSelectors(foundElement, sub);
        addChildFn(foundElement, children);
      }
      return foundElement;
    });
  });

  return element;
}

/**
 * Constructor function for testing $resource or an restangularized object
 *
 * Usage:
 * requestTest()
 *   .whenMethod(fooResource.query)
 *   .expectGet('api/foo')
 *   .andRespond([])
 *   .run();
 */
function requestTest() {
  var test = {
    _httpMethod: 'GET',
    _data: null
  };

  test._expectedResult = test._response;

  test.whenPath = function whenPath(path) {
    test._path = path;
    return test;
  };

  test.whenMethod = function whenMethod(method) {
    test._method = method;
    test._methodArguments = Array.prototype.slice.call(arguments, 1);
    return test;
  };

  test.whenCall = test.whenMethod;

  test.whenHttpMethod = function whenHttpMethod(httpMethod) {
    test._httpMethod = httpMethod;
    return test;
  };

  test.whenData = function whenData(data) {
    test._data = data;
    return test;
  };

  test.expectRequest = function expectRequest(httpMethod, path, data) {
    test.whenHttpMethod(httpMethod);
    test.whenPath(path);
    test.whenData(data);
    return test;
  };

  test.andRespond = function andRespond(response) {
    test._response = response;
    test._expectedResult = test._response;
    return test;
  };

  test.andExpect = function andExpect(expectedResult) {
    test._expectedResult = expectedResult;
    return test;
  };

  test.expectGet = function expectGet(path) {
    return test.expectRequest('GET', path);
  };

  test.expectPost = function expectPost(path, data) {
    return test.expectRequest('POST', path, data);
  };

  test.expectPut = function expectPut(path, data) {
    return test.expectRequest('PUT', path, data);
  };

  test.expectDelete = function expectDelete(path, data) {
    return test.expectRequest('DELETE', path, data);
  };

  test.expectQueryParams = function expectQueryParams(expectedQueryParams) {
    test._expectedQueryParams = expectedQueryParams;
    return test;
  };

  function validateUrl(url) {
    if (test._expectedQueryParams) {
      expect(url).toHaveQueryParams(test._expectedQueryParams);
      return test._path ? url.indexOf(test._path) >= 0 : true;
    }
    return test._path === url;
  }

  test.fail = function fail() {
    test._expectFail = true;
    test.run();
  };

  test.run = function run() {
    test._response = test._response || {};
    mox.inject('$httpBackend').expect(test._httpMethod, { test: validateUrl }, test._data).respond(test._response);

    var response = test._method.apply(this, test._methodArguments);
    var promise = response.$promise || response;

    if (angular.isFunction(test._expectedResult)) {
      var successCallback = jasmine.createSpy('success callback');
      var failureCallback = jasmine.createSpy('failure callback');

      promise
        .then(successCallback)
        .catch(failureCallback);

      mox.inject('$httpBackend').flush();

      var cb = test._expectFail ? failureCallback : successCallback;
      if (currentSpec.isJasmine2) {
        test._expectedResult(cb.calls.mostRecent().args[0]);
      } else {
        test._expectedResult(cb.mostRecentCall.args[0]);
      }
    } else {
      // Avoid `Possibly Unhandled Rejection` error but still fulfill the returned promise with a rejection, see angular/angular.js#15624
      promise.catch(angular.noop);
      mox.inject('$httpBackend').flush();

      if (test._expectFail) {
        expect(promise).toReject();
      } else {
        expect(promise).toResolve();
      }
    }

  };

  return test;
}