/* (c) Ferron Hanse 2012 https://github.com/ferronrsmith/anuglarjs-jasmine-matchers Released under the MIT license */ /*jslint nomen : true*/ /*jslint devel : true*/ /*jslint unparam : true */ /*jslint browser : true */ /*jslint bitwise : true*/ /*global describe, beforeEach, inject, module, angular, document, it, expect, $, jasmine, toJson */ /** Provides a comprehensive set of custom matchers for the Jasmine testing framework @class matchers @main matchers **/ beforeEach(function () { "use strict"; var matchers = {}, hlp = {}, bjQuery = false; hlp.cssMatcher = function (presentClasses, absentClasses) { var self = this; return function () { var element = angular.element(self.actual), present = true, absent = false; angular.forEach(presentClasses.split(' '), function (className) { present = present && element.hasClass(className); }); angular.forEach(absentClasses.split(' '), function (className) { absent = absent || element.hasClass(className); }); self.message = function () { return "Expected to {0} have ".t(this.isNot ? "not" : "") + presentClasses + (absentClasses ? (" and not have " + absentClasses + " ") : "") + " but had " + element[0].className + "."; }; return present && !absent; }; }; /** * Returns the index of an object in a given array * @method hpl.indexOf * @param array :- array object to be checked * @param obj :- object (value) to be checked for in the array * @return {number} index of the obj in the array */ hlp.indexOf = function (array, obj) { var i; for (i = 0; i < array.length; i += 1) { if (obj === array[i]) { return i; } } return -1; }; /** * Check if an object has a particular property matches the expected value * @method hpl.hasProperty * @param actualValue property value * @param expectedValue expected value * @return {boolean} boolean indicating if the values match */ hlp.hasProperty = function (actualValue, expectedValue) { if (expectedValue === undefined) { return actualValue !== undefined; } return actualValue === expectedValue; }; /** * Checks if a given element/JavaScript object matches the type * @method hpl.typeOf * @param actual Object to be checked for type comparison * @param type type to be matched * @return {boolean} boolean indicating if the type matches the object type */ hlp.typeOf = function (actual, type) { return Object.prototype.toString.call(actual) === "[object " + type + "]"; }; /** * Checks if the a given word/phrase/substring is at the end of a string * @method hpl.endsWith * @param {String} haystack string to be search * @param needle {String} word/phrase/substring * @return {boolean} boolean indicating if the word/phrase/substring was found at the end of the string */ hlp.endsWith = function (haystack, needle) { return haystack.substr(-needle.length) === needle; }; /** * Checks if the a given word/phrase/substring is at the beginning of a string * @method hpl.endsWith * @param {String} haystack string to be search * @param needle {String} word/phrase/substring * @return {boolean} boolean indicating if the word/phrase/substring was found at the beginning of the string */ hlp.startsWith = function (haystack, needle) { return haystack.substr(0, needle.length) === needle; }; /** * Coverts a given object literal to an array * @method hlp.objToArray * @param obj - object literal * @return {Array} array representation of the object * @since 0.2 :- Removed $$hashKey check */ hlp.objToArray = function (obj) { var arr = [], aDup = {}; angular.copy(obj, aDup); angular.forEach(aDup, function (value, key) { arr.push(value); }); return arr; }; /** * Coverts a given a list of object literals to a flatten array * @method hlp.objListToArray * @param obj - object literals * @return {Array} flatten array representation of the objects */ hlp.objListToArray = function (obj) { var res = []; angular.forEach(obj, function (value, key) { res = res.concat(hlp.objToArray(value)); }); return res; }; hlp.isNumber = function (val) { return !isNaN(parseFloat(val)) && !hlp.typeOf(val, 'String'); }; /** * Message constant for jQuery * @type {string} */ hlp.msg = { jQuery : "Error: jQuery not found. this matcher has a dependency on jQuery", date : { invalidType : 'Expected {0} & {1} to be a Date', nomatch : { Date : 'Expected {0} & {1} to match', part : "Invalid part : {0} entered" } } }; hlp.dp = function (x) { return angular.mock.dump(arguments.length > 1 ? arguments : x); }; /** * Returns isNot String * @param context * @param altText */ hlp.isNot = function (context, altText) { altText = altText || ""; return context.isNot ? "not " : altText; }; String.prototype.t = function () { var args = arguments; return this.replace(/\{(\d+)\}/g, function (match, number) { return args[number] !== 'undefined' ? args[number] : match; }); }; /** * Check if jQuery is present * @return {boolean} boolean indicating if jQuery is present */ bjQuery = (function () { return (window.$ !== undefined || window.jQuery !== undefined); }()); // a check that allows the matchers to work with angular-scenario // NB: Not all matchers work with angualar-scenario and i have not done extensive testing on this if (this.addMatchers === undefined) { this.addMatchers = function (properties) { if (angular.scenario !== undefined && angular.isObject(properties)) { angular.forEach(properties, function (value, key) { angular.scenario.matcher(key, value); }); } }; } matchers.toBeInvalid = hlp.cssMatcher('ng-invalid', 'ng-valid'); matchers.toBeValid = hlp.cssMatcher('ng-valid', 'ng-invalid'); matchers.toBeDirty = hlp.cssMatcher('ng-dirty', 'ng-pristine'); matchers.toBePristine = hlp.cssMatcher('ng-pristine', 'ng-dirty'); matchers.toEqual = function (expected) { if (this.actual && this.actual.$$log) { if (typeof expected === 'string') { this.actual = this.actual.toString(); } else { this.actual = this.actual.toArray(); } } return jasmine.Matchers.prototype.toEqual.call(this, expected); }; matchers.toEqualData = function (expected) { this.message = function () { return "Expected " + hlp.dp(this.actual) + " data {0} to Equal ".t(this.isNot ? "not" : "") + expected; }; return angular.equals(this.actual, expected); }; matchers.toEqualError = function (message) { this.message = function () { var expected; if (this.actual.message && this.actual.name === 'Error') { expected = angular.toJson(this.actual.message); } else { expected = angular.toJson(this.actual); } return "Expected " + expected + " to {0} be an Error with message ".t(this.isNot ? "not" : "") + angular.toJson(message); }; return this.actual.name === 'Error' && this.actual.message === message; }; matchers.toMatchError = function (messageRegexp) { this.message = function () { var expected; if (this.actual.message && this.actual.name === 'Error') { expected = angular.toJson(this.actual.message); } else { expected = angular.toJson(this.actual); } return "Expected " + expected + " to {0} match an Error with message ".t(this.isNot ? "not" : "") + angular.toJson(messageRegexp); }; return this.actual.name === 'Error' && messageRegexp.test(this.actual.message); }; matchers.toHaveBeenCalledOnce = function () { if (arguments.length > 0) { throw new Error('toHaveBeenCalledOnce does not take arguments, use toHaveBeenCalledWith'); } if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function () { var msg = 'Expected spy ' + this.actual.identity + ' to have been called once, but was ', count = this.actual.callCount; return [ count === 0 ? msg + 'never called.' : msg + 'called ' + count + ' times.', msg.replace('to have', 'not to have') + 'called once.' ]; }; return this.actual.callCount === 1; }; matchers.toHaveBeenCalledOnceWith = function () { var expectedArgs = jasmine.util.argsToArray(arguments); if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function () { var result; if (this.actual.callCount !== 1) { if (this.actual.callCount === 0) { result = [ 'Expected spy ' + this.actual.identity + ' to have been called with ' + jasmine.pp(expectedArgs) + ' but it was never called.', 'Expected spy ' + this.actual.identity + ' not to have been called with ' + jasmine.pp(expectedArgs) + ' but it was.' ]; } else { result = [ 'Expected spy ' + this.actual.identity + ' to have been called with ' + jasmine.pp(expectedArgs) + ' but it was never called.', 'Expected spy ' + this.actual.identity + ' not to have been called with ' + jasmine.pp(expectedArgs) + ' but it was.' ]; } } else { result = [ 'Expected spy ' + this.actual.identity + ' to have been called with ' + jasmine.pp(expectedArgs) + ' but was called with ' + jasmine.pp(this.actual.argsForCall), 'Expected spy ' + this.actual.identity + ' not to have been called with ' + jasmine.pp(expectedArgs) + ' but was called with ' + jasmine.pp(this.actual.argsForCall) ]; } return result; }; return this.actual.callCount === 1 && this.env.contains_(this.actual.argsForCall, expectedArgs); }; matchers.toBeOneOf = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be one of '".t(this.isNot ? "not" : "") + hlp.dp(arguments) + "'."; }; return hlp.indexOf(arguments, this.actual) !== -1; }; matchers.toHaveClass = function (clazz) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} have class '".t(this.isNot ? "not" : "") + clazz + "'."; }; return this.actual.hasClass ? this.actual.hasClass(clazz) : angular.element(this.actual).hasClass(clazz); }; matchers.toHaveCss = function (css) { var prop; // css prop this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} have css '".t(this.isNot ? "not" : "") + hlp.dp(css) + "'."; }; for (prop in css) { if (css.hasOwnProperty(prop)) { if (this.actual.css(prop) !== css[prop]) { return false; } } } return true; }; matchers.toMatchRegex = function (regex) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} match '".t(this.isNot ? "not" : "") + regex; }; var reg; if (hlp.typeOf(regex, "String")) { reg = new RegExp(regex); } else if (hlp.typeOf(regex, "RegExp")) { reg = regex; } return reg.test(this.actual); }; matchers.toBeVisible = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be visible '".t(this.isNot ? "not" : ""); }; return this.actual.is(':visible'); }; matchers.toBeHidden = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be hidden '".t(this.isNot ? "not" : ""); }; return this.actual.is(':hidden'); }; matchers.toBeSelected = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be selected '".t(this.isNot ? "not" : ""); }; return this.actual.is(':selected'); }; matchers.toBeChecked = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be checked '".t(this.isNot ? "not" : ""); }; return this.actual.is(':checked'); }; matchers.toBeSameDate = function (date) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be equal to '".t(this.isNot ? "not" : "") + hlp.dp(date); }; var actualDate = this.actual; return actualDate.getDate() === date.getDate() && actualDate.getFullYear() === date.getFullYear() && actualDate.getMonth() === date.getMonth() && actualDate.getHours() === date.getHours() && actualDate.getMinutes() === date.getMinutes() && actualDate.getSeconds() === date.getSeconds(); }; matchers.toBeEmpty = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be empty '".t(this.isNot ? "not" : ""); }; return this.actual.is(':empty'); }; matchers.toBeEmptyString = function () { this.message = function () { return "Expected string '" + hlp.dp(this.actual) + "' to {0} be empty '".t(this.isNot ? "not" : ""); }; return hlp.typeOf(this.actual, 'String') && $.trim(this.actual).length === 0; }; matchers.toExist = function () { this.message = function () { var msg = ""; if (bjQuery) { msg = "Expected '" + hlp.dp(this.actual) + "' to {0} exists '".t(this.isNot ? "not" : ""); } else { msg = hlp.msg.jQuery; } return msg; }; return bjQuery ? $(document).find(this.actual).length : false; }; matchers.toHaveAttr = function (attributeName, expectedAttributeValue) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} have attribute '".t(this.isNot ? "not" : "") + attributeName + "' with value " + expectedAttributeValue + "."; }; return hlp.hasProperty(this.actual.attr(attributeName), expectedAttributeValue); }; matchers.toHaveProp = function (propertyName, expectedPropertyValue) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} have property '".t(this.isNot ? "not" : "") + expectedPropertyValue + "'."; }; return hlp.hasProperty(this.actual.prop(propertyName), expectedPropertyValue); }; matchers.toHaveId = function (id) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} have id '".t(this.isNot ? "not" : "") + id + "'."; }; return this.actual.attr('id') === id; }; matchers.toBeDisabled = function (selector) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be disabled '".t(this.isNot ? "not" : "") + hlp.dp(selector) + "'."; }; return this.actual.is(':disabled'); }; matchers.toBeFocused = function (selector) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be focused '".t(this.isNot ? "not" : "") + hlp.dp(selector) + "'."; }; return this.actual.is(':focus'); }; matchers.toHaveText = function (text) { if (!bjQuery) { return false; } this.message = function () { var msg = ""; if (bjQuery) { msg = "Expected '" + hlp.dp(this.actual) + "' to {0} have text '".t(this.isNot ? "not" : "") + text + "'."; } else { msg = hlp.msg.jQuery; } return msg; }; var trimmedText = $.trim(this.actual.text()), result; if (text && angular.isFunction(text.test)) { result = text.test(trimmedText); } else { result = trimmedText === text; } return result; }; matchers.toHaveValue = function (value) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} have value '".t(this.isNot ? "not" : "") + value + "'."; }; return this.actual.val() === value; }; matchers.toHaveData = function (key, expectedValue) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} have data '" + expectedValue + "'.".t(this.isNot ? "not" : ""); }; return hlp.hasProperty(this.actual.data(key), expectedValue); }; /** * Does not return true if subject is null * @return {Boolean} */ matchers.toBeObject = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be an [Object]".t(this.isNot ? "not" : ""); }; return hlp.typeOf(this.actual, 'Object'); }; /** * @return {Boolean} */ matchers.toBeArray = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be an [Array]".t(this.isNot ? "not" : ""); }; return hlp.typeOf(this.actual, 'Array'); }; /** * @return {Boolean} */ matchers.toBeDate = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be a [Date]".t(this.isNot ? "not" : ""); }; return hlp.typeOf(this.actual, 'Date'); }; /** * @return {Boolean} */ matchers.toBeBefore = function (date) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be before".t(this.isNot ? "not" : "") + hlp.dp(date); }; return hlp.typeOf(this.actual, 'Date') && this.actual.getTime() < date.getTime(); }; /** * @return {Boolean} */ matchers.toBeAfter = function (date) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be after".t(this.isNot ? "not" : "") + hlp.dp(date); }; return hlp.typeOf(this.actual, 'Date') && this.actual.getTime() > date.getTime(); }; matchers.toBeIso8601Date = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be ISO8601 Date Format".t(this.isNot ? "not" : ""); }; return hlp.typeOf(this.actual, 'String') && this.actual.length >= 10 && new Date(this.actual).toString() !== 'Invalid Date' && new Date(this.actual).toISOString().slice(0, this.actual.length) === this.actual; }; /** * Asserts subject is an Array with a defined number of members * @param {Number} size * @return {Boolean} */ matchers.toBeArrayOfSize = function (size) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be an [Array] of size {1}".t(this.isNot ? "not" : "", size); }; return hlp.typeOf(this.actual, 'Array') && this.actual.length === size; }; /** * @return {Boolean} */ matchers.toBeString = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be a [String]".t(this.isNot ? "not" : ""); }; return hlp.typeOf(this.actual, 'String'); }; /** * @return {Boolean} */ matchers.toBeBoolean = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to {0} be Boolean".t(this.isNot ? "not" : ""); }; return hlp.typeOf(this.actual, 'Boolean'); }; /** * @return {Boolean} */ matchers.toBeNonEmptyString = function () { if (!bjQuery) { return false; } this.message = function () { var msg = ""; if (bjQuery) { msg = "Expected '" + hlp.dp(this.actual) + "' to " + hlp.isNot(this, "") + "be a non empty string "; } else { msg = hlp.msg.jQuery; } return msg; }; return hlp.typeOf(this.actual, 'String') && $.trim(this.actual).length > 0; }; /** * @return {Boolean} */ matchers.toBeNumber = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to " + hlp.isNot(this, "") + "be a [Number]"; }; return hlp.isNumber(this.actual); }; matchers.toBeEvenNumber = function () { this.message = function () { return "Expected " + hlp.dp(this.actual) + " to " + hlp.isNot(this, "") + "be an even number"; }; return hlp.isNumber(this.actual) && this.actual % 2 === 0; }; matchers.toBeOddNumber = function () { this.message = function () { return "Expected " + hlp.dp(this.actual) + " to " + hlp.isNot(this, "") + "be an odd number"; }; return hlp.isNumber(this.actual) && this.actual % 2 !== 0; }; matchers.toBeNaN = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to " + hlp.isNot(this, "") + "be a [NaN]"; }; return isNaN(this.actual); }; /** * @return {Boolean} */ matchers.toBeFunction = function () { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to " + hlp.isNot(this, "") + "be a [Function]"; }; return hlp.typeOf(this.actual, 'Function'); }; matchers.toHaveLength = function (length) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to " + hlp.isNot(this, "") + "have a length of " + length; }; return this.actual.length === length; }; matchers.toStartWith = function (value) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + " " + hlp.isNot(this, "") + "to start with " + value; }; return hlp.startsWith(this.actual, value); }; matchers.toEndWith = function (value) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + " " + hlp.isNot(this, "") + "' to end with " + value; }; return hlp.endsWith(this.actual, value); }; matchers.toContainOnce = function (value) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to contain only one " + value; }; var actual = this.actual, containsOnce = false, firstFoundAt; if (actual) { firstFoundAt = actual.indexOf(value); containsOnce = firstFoundAt !== -1 && firstFoundAt === actual.lastIndexOf(value); } return containsOnce; }; matchers.toContainSelector = function (selector) { this.message = function () { return "Expected '" + hlp.dp(this.actual) + "' to have contain '" + hlp.dp(selector) + "'."; }; return this.actual.find(selector).length; }; /** * @return {boolean} */ matchers.toBeUniqueArray = function () { // iterate over the array, adding unique elements to o var arr = this.actual, i, len = this.actual.length, o = []; this.message = function () { return "Expected " + hlp.dp(this.actual) + " values {0} to be unique".t(this.isNot ? "not" : ""); }; for (i = 0; i < len; i += 1) { if (hlp.indexOf(o, arr[i]) === -1) { o.push(arr[i]); } else { return false; } } return true; }; matchers.toHaveMatchingAtrr = function (attr, obj) { var arr = hlp.objListToArray(obj), result = true, temp = this.actual, iter = 0, len = this.actual.length; // can't compare arrays of different lengths if (this.actual.length !== arr.length) { return false; } for (iter = 0; iter < len; iter += 1) { result &= temp.eq(iter).attr(attr) === arr[iter]; } this.message = function () { var message; if (this.actual.length === arr.length) { message = "Expected '" + hlp.dp(this.actual) + "' elements to have attributes " + hlp.dp(arr) + " " + hlp.dp(arr); } else { message = "Can't compare obj properties of length " + arr.length + " with element collection of length " + this.actual.length; } return message; }; return result; }; /** * * @method matchers.toMatchDatePart * @param oDate {Date} Date to be compared * @param {String} part specific part/property of the date you want to be compared * Currently supported parts are listed below : *