catarse/catarse.js

View on GitHub
legacy/spec/lib/jasmine-matchers.js

Summary

Maintainability
D
2 days
Test Coverage
/*
 * Copyright © 2013 Jamie Mason, @fold_left,
 * https://github.com/JamieMason
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

beforeEach(function() {

    var matchers = {};
    var priv = {};

    priv.each = function(array, fn) {
        var i;
        var len = array.length;
        if ('length' in array) {
            for (i = 0; i < len; i++) {
                fn.call(this, array[i], i, array);
            }
        } else {
            for (i in array) {
                fn.call(this, array[i], i, array);
            }
        }
    };

    priv.all = function(array, fn) {
        var i;
        var len = array.length;
        for (i = 0; i < len; i++) {
            if (fn.call(this, array[i], i, array) === false) {
                return false;
            }
        }
        return true;
    };

    priv.some = function(array, fn) {
        var i;
        var len = array.length;
        for (i = 0; i < len; i++) {
            if (fn.call(this, array[i], i, array) === true) {
                return true;
            }
        }
        return false;
    };

    priv.expectAllMembers = function(assertion) {
        return priv.all.call(this, this.actual, function(item) {
            return matchers[assertion].call({
                actual: item
            });
        });
    };

    /**
     * Assert subject is of type
     * @param  {Mixed} subject
     * @param  {String} type
     * @return {Boolean}
     */

    priv.is = function(subject, type) {
        return Object.prototype.toString.call(subject) === '[object ' + type + ']';
    };

    /**
     * Assert subject is an HTML Element with the given node type
     * @return {Boolean}
     */

    priv.isHtmlElementOfType = function(subject, type) {
        return subject && subject.nodeType === type;
    };

    /**
     * Convert Array-like Object to true Array
     * @param  {Mixed[]} list
     * @return {Array}
     */

    priv.toArray = function(list) {
        return [].slice.call(list);
    };

    // Arrays
    // ---------------------------------------------------------------------------

    priv.createToBeArrayOfXsMatcher = function(toBeX) {
        return function() {
            return matchers.toBeArray.call(this) && priv.expectAllMembers.call(this, toBeX);
        };
    };

    /**
     * Assert subject is an Array (from this document, eg Arrays from iframes
     * or popups won't match)
     * @return {Boolean}
     */
    matchers.toBeArray = function() {
        return this.actual instanceof Array;
    };

    /**
     * Assert subject is an Array with a defined number of members
     * @param  {Number} size
     * @return {Boolean}
     */
    matchers.toBeArrayOfSize = function(size) {
        return matchers.toBeArray.call(this) && this.actual.length === size;
    };

    /**
     * Assert subject is an Array, but with no members
     * @return {Boolean}
     */
    matchers.toBeEmptyArray = function() {
        return matchers.toBeArrayOfSize.call(this, 0);
    };

    /**
     * Assert subject is an Array with at least one member
     * @return {Boolean}
     */
    matchers.toBeNonEmptyArray = function() {
        return matchers.toBeArray.call(this) && this.actual.length > 0;
    };

    /**
     * Assert subject is an Array which is either empty or contains only Objects
     * @return {Boolean}
     */
    matchers.toBeArrayOfObjects = priv.createToBeArrayOfXsMatcher('toBeObject');

    /**
     * Assert subject is an Array which is either empty or contains only Strings
     * @return {Boolean}
     */
    matchers.toBeArrayOfStrings = priv.createToBeArrayOfXsMatcher('toBeString');

    /**
     * Assert subject is an Array which is either empty or contains only Numbers
     * @return {Boolean}
     */
    matchers.toBeArrayOfNumbers = priv.createToBeArrayOfXsMatcher('toBeNumber');

    /**
     * Assert subject is an Array which is either empty or contains only Booleans
     * @return {Boolean}
     */
    matchers.toBeArrayOfBooleans = priv.createToBeArrayOfXsMatcher('toBeBoolean');

    // Booleans
    // ---------------------------------------------------------------------------

    /**
     * Assert subject is not only truthy or falsy, but an actual Boolean
     * @return {Boolean}
     */
    matchers.toBeBoolean = function() {
        return matchers.toBeTrue.call(this) || matchers.toBeFalse.call(this);
    };

    /**
     * Assert subject is not only truthy, but an actual Boolean
     * @return {Boolean}
     */
    matchers.toBeTrue = function() {
        return this.actual === true || this.actual instanceof Boolean && this.actual.valueOf() === true;
    };

    /**
     * Assert subject is not only falsy, but an actual Boolean
     * @return {Boolean}
     */
    matchers.toBeFalse = function() {
        return this.actual === false || this.actual instanceof Boolean && this.actual.valueOf() === false;
    };

    // Browser
    // ---------------------------------------------------------------------------

    /**
     * Assert subject is the window global
     * @return {Boolean}
     */
    matchers.toBeWindow = function() {
        return typeof window !== 'undefined' && this.actual === window;
    };

    /**
     * Assert subject is the document global
     * @return {Boolean}
     */
    matchers.toBeDocument = function() {
        return typeof document !== 'undefined' && this.actual === document;
    };

    /**
     * Assert subject is an HTML Element
     * @return {Boolean}
     */
    matchers.toBeHtmlNode = function() {
        return priv.isHtmlElementOfType(this.actual, 1);
    };

    /**
     * Assert subject is an HTML Text Element
     * @return {Boolean}
     */
    matchers.toBeHtmlTextNode = function() {
        return priv.isHtmlElementOfType(this.actual, 3);
    };

    /**
     * Assert subject is an HTML Text Element
     * @return {Boolean}
     */
    matchers.toBeHtmlCommentNode = function() {
        return priv.isHtmlElementOfType(this.actual, 8);
    };

    /**
     * Assert subject is a Date
     * @return {Boolean}
     */
    matchers.toBeDate = function() {
        return this.actual instanceof Date;
    };

    /**
     * Assert subject is a Date String conforming to the ISO 8601 standard
     * @return {Boolean}
     */
    matchers.toBeIso8601 = function() {
        return matchers.toBeString.call(this) && this.actual.length >= 10 && new Date(this.actual).toString() !== 'Invalid Date' && new Date(this.actual).toISOString().slice(0, this.actual.length) === this.actual;
    };

    /**
     * Assert subject is a Date occurring before another Date
     * @param {Date} date
     * @return {Boolean}
     */
    matchers.toBeBefore = function(date) {
        return matchers.toBeDate.call(this) && matchers.toBeDate.call({
            actual: date
        }) && this.actual.getTime() < date.getTime();
    };

    /**
     * Assert subject is a Date occurring after another Date
     * @param {Date} date
     * @return {Boolean}
     */
    matchers.toBeAfter = function(date) {
        return matchers.toBeBefore.call({
            actual: date
        }, this.actual);
    };

    // Errors
    // ---------------------------------------------------------------------------

    /**
     * Asserts subject throws an Error of any type
     * @return {Boolean}
     */
    matchers.toThrowAnyError = function() {
        var threwError = false;
        try {
            this.actual();
        } catch (e) {
            threwError = true;
        }
        return threwError;
    };

    /**
     * Asserts subject throws an Error of a specific type, such as 'TypeError'
     * @param  {String} type
     * @return {Boolean}
     */
    matchers.toThrowErrorOfType = function(type) {
        var threwErrorOfType = false;
        try {
            this.actual();
        } catch (e) {
            threwErrorOfType = (e.name === type);
        }
        return threwErrorOfType;
    };

    // Members
    // ---------------------------------------------------------------------------

    /**
     * Assert subject is an Object containing an Array at memberName
     * @name toHaveArray
     * @param {String} memberName
     * @return {Boolean}
     */

    /**
     * Assert subject is an Object containing an Array of size at memberName
     * @name toHaveArrayOfSize
     * @param {String} memberName
     * @param {Number} size
     * @return {Boolean}
     */

    /**
     * Assert subject is an Object containing an Array at memberName with no members
     * @name toHaveEmptyArray
     * @param {String} memberName
     * @return {Boolean}
     */

    /**
     * Assert subject is an Object containing an Array at memberName with at least one member
     * @name toHaveNonEmptyArray
     * @param {String} memberName
     * @return {Boolean}
     */

    /**
     * Assert subject is an Object containing an Array at memberName where no member is not an Object
     * @name toHaveArrayOfObjects
     * @param {String} memberName
     * @return {Boolean}
     */

    /**
     * Assert subject is an Object containing an Array at memberName where no member is not a String
     * @name toHaveArrayOfStrings
     * @param {String} memberName
     * @return {Boolean}
     */

    /**
     * Assert subject is an Object containing an Array at memberName where no member is not a Number
     * @name toHaveArrayOfNumbers
     * @param {Number} memberName
     * @return {Boolean}
     */

    /**
     * Assert subject is an Object containing an Array at memberName where no member is not a Boolean
     * @name toHaveArrayOfBooleans
     * @param {Boolean} memberName
     * @return {Boolean}
     */

    /**
     * @param  {String} matcherName
     * @return {Function}
     */

    function assertMember(matcherName) {
        return function() {
            var args = priv.toArray(arguments);
            var memberName = args.shift();
            return matchers.toBeObject.call(this) && matchers[matcherName].apply({
                actual: this.actual[memberName]
            }, args);
        };
    }

    priv.each([
        'Array',
        'ArrayOfSize',
        'EmptyArray',
        'NonEmptyArray',
        'ArrayOfObjects',
        'ArrayOfStrings',
        'ArrayOfNumbers',
        'ArrayOfBooleans'
    ], function(matcherName) {
        matchers['toHave' + matcherName] = assertMember('toBe' + matcherName);
    });

    // Numbers
    // ---------------------------------------------------------------------------

    /**
     * Assert subject is not only calculable, but an actual Number
     * @return {Boolean}
     */
    matchers.toBeNumber = function() {
        return !isNaN(parseFloat(this.actual)) && !priv.is(this.actual, 'String');
    };

    /**
     * Assert subject is an even Number
     * @return {Boolean}
     */
    matchers.toBeEvenNumber = function() {
        return matchers.toBeNumber.call(this) && this.actual % 2 === 0;
    };

    /**
     * Assert subject is an odd Number
     * @return {Boolean}
     */
    matchers.toBeOddNumber = function() {
        return matchers.toBeNumber.call(this) && this.actual % 2 !== 0;
    };

    /**
     * Assert subject can be used in Mathemetic calculations, despite not being an actual Number.
     * @example "1" * "2" === 2 (pass)
     * @example "wut?" * "2" === NaN (fail)
     * @return {Boolean}
     */
    matchers.toBeCalculable = function() {
        return !isNaN(this.actual * 2);
    };

    /**
     * Assert value is >= floor or <= ceiling
     * @param {Number} floor
     * @param {Number} ceiling
     * @return {Boolean}
     */
    matchers.toBeWithinRange = function(floor, ceiling) {
        return matchers.toBeNumber.call(this) && this.actual >= floor && this.actual <= ceiling;
    };

    /**
     * Assert value is a number with no decimal places
     * @return {Boolean}
     */
    matchers.toBeWholeNumber = function() {
        return matchers.toBeNumber.call(this) && (this.actual === 0 || this.actual % 1 === 0);
    };

    // Objects
    // ---------------------------------------------------------------------------

    /**
     * Assert subject is an Object, and not null
     * @return {Boolean}
     */
    matchers.toBeObject = function() {
        return this.actual instanceof Object;
    };

    /**
     * Assert subject features the same public members as api.
     * @param  {Object|Array} api
     * @return {Boolean}
     */
    matchers.toImplement = function(api) {
        var required;
        if (!this.actual || !api) {
            return false;
        }
        for (required in api) {
            if ((required in this.actual) === false) {
                return false;
            }
        }
        return true;
    };

    /**
     * Assert subject is a function
     * @return {Boolean}
     */
    matchers.toBeFunction = function() {
        return this.actual instanceof Function;
    };

    // Strings
    // ---------------------------------------------------------------------------

    /**
     * Assert subject is a String
     * @return {Boolean}
     */
    matchers.toBeString = function() {
        return priv.is(this.actual, 'String');
    };

    /**
     * @return {Boolean}
     */
    matchers.toBeEmptyString = function() {
        return this.actual === '';
    };

    /**
     * @return {Boolean}
     */
    matchers.toBeNonEmptyString = function() {
        return matchers.toBeString.call(this) && this.actual.length > 0;
    };

    /**
     * Assert subject is string containing HTML Markup
     * @return {Boolean}
     */
    matchers.toBeHtmlString = function() {
        return matchers.toBeString.call(this) && this.actual.search(/<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)/) !== -1;
    };

    /**
     * Assert subject is string containing parseable JSON
     * @return {Boolean}
     */
    matchers.toBeJsonString = function() {
        var isParseable;
        var json;
        try {
            json = JSON.parse(this.actual);
        } catch (e) {
            isParseable = false;
        }
        return isParseable !== false && json !== null;
    };

    /**
     * Assert subject is a String containing nothing but whitespace
     * @return {Boolean}
     */
    matchers.toBeWhitespace = function() {
        return matchers.toBeString.call(this) && this.actual.search(/\S/) === -1;
    };

    /**
     * Assert subject is a String whose first characters match our expected string
     * @param  {String} expected
     * @return {Boolean}
     */
    matchers.toStartWith = function(expected) {
        if (!matchers.toBeNonEmptyString.call(this) || !matchers.toBeNonEmptyString.call({
                actual: expected
            })) {
            return false;
        }
        return this.actual.slice(0, expected.length) === expected;
    };

    /**
     * Assert subject is a String whose last characters match our expected string
     * @param  {String} expected
     * @return {Boolean}
     */
    matchers.toEndWith = function(expected) {
        if (!matchers.toBeNonEmptyString.call(this) || !matchers.toBeNonEmptyString.call({
                actual: expected
            })) {
            return false;
        }
        return this.actual.slice(this.actual.length - expected.length, this.actual.length) === expected;
    };

    /**
     * Assert subject is a String whose length is greater than our other string
     * @param  {String} other
     * @return {Boolean}
     */
    matchers.toBeLongerThan = function(other) {
        return matchers.toBeString.call(this) && matchers.toBeString.call({
            actual: other
        }) && this.actual.length > other.length;
    };

    /**
     * Assert subject is a String whose length is less than our other string
     * @param  {String} other
     * @return {Boolean}
     */
    matchers.toBeShorterThan = function(other) {
        return matchers.toBeString.call(this) && matchers.toBeString.call({
            actual: other
        }) && this.actual.length < other.length;
    };

    /**
     * Assert subject is a String whose length is equal to our other string
     * @param  {String} other
     * @return {Boolean}
     */
    matchers.toBeSameLengthAs = function(other) {
        return matchers.toBeString.call(this) && matchers.toBeString.call({
            actual: other
        }) && this.actual.length === other.length;
    };


    // Create adapters for the original matchers so they can be compatible with Jasmine 2.0.

    var isJasmineV1 = typeof this.addMatchers === 'function';
    var isJasmineV2 = typeof jasmine.addMatchers === 'function';
    var v2Matchers = {};

    if (isJasmineV1) {
        this.addMatchers(matchers);
    } else if (isJasmineV2) {
        priv.each(matchers, function(fn, name) {
            v2Matchers[name] = function() {
                return {
                    compare: function(actual, expected) {
                        var args = priv.toArray(arguments);
                        var scope = {
                            actual: actual
                        };
                        args.shift();
                        return {
                            pass: matchers[name].apply(scope, args)
                        };
                    }
                };
            };
        });
        jasmine.addMatchers(v2Matchers);
    }

});