"use strict"; var arrayProto = require("@sinonjs/commons").prototypes.array; var deepEqual = require("./deep-equal").use(createMatcher); // eslint-disable-line no-use-before-define var every = require("@sinonjs/commons").every; var functionName = require("@sinonjs/commons").functionName; var get = require("lodash.get"); var iterableToString = require("./iterable-to-string"); var objectProto = require("@sinonjs/commons").prototypes.object; var typeOf = require("@sinonjs/commons").typeOf; var valueToString = require("@sinonjs/commons").valueToString; var assertMatcher = require("./create-matcher/assert-matcher"); var assertMethodExists = require("./create-matcher/assert-method-exists"); var assertType = require("./create-matcher/assert-type"); var isIterable = require("./create-matcher/is-iterable"); var isMatcher = require("./create-matcher/is-matcher"); var matcherPrototype = require("./create-matcher/matcher-prototype"); var arrayIndexOf = arrayProto.indexOf; var some = arrayProto.some; var hasOwnProperty = objectProto.hasOwnProperty; var objectToString = objectProto.toString; var TYPE_MAP = require("./create-matcher/type-map")(createMatcher); // eslint-disable-line no-use-before-define /** * Creates a matcher object for the passed expectation * * @alias module:samsam.createMatcher * @param {*} expectation An expecttation * @param {string} message A message for the expectation * @returns {object} A matcher object */ function createMatcher(expectation, message) { var m = Object.create(matcherPrototype); var type = typeOf(expectation); if (message !== undefined && typeof message !== "string") { throw new TypeError("Message should be a string"); } if (arguments.length > 2) { throw new TypeError( "Expected 1 or 2 arguments, received " + arguments.length ); } if (type in TYPE_MAP) { TYPE_MAP[type](m, expectation, message); } else { m.test = function(actual) { return deepEqual(actual, expectation); }; } if (!m.message) { m.message = "match(" + valueToString(expectation) + ")"; } return m; } createMatcher.isMatcher = isMatcher; createMatcher.any = createMatcher(function() { return true; }, "any"); createMatcher.defined = createMatcher(function(actual) { return actual !== null && actual !== undefined; }, "defined"); createMatcher.truthy = createMatcher(function(actual) { return Boolean(actual); }, "truthy"); createMatcher.falsy = createMatcher(function(actual) { return !actual; }, "falsy"); createMatcher.same = function(expectation) { return createMatcher(function(actual) { return expectation === actual; }, "same(" + valueToString(expectation) + ")"); }; createMatcher.in = function(arrayOfExpectations) { if (typeOf(arrayOfExpectations) !== "array") { throw new TypeError("array expected"); } return createMatcher(function(actual) { return some(arrayOfExpectations, function(expectation) { return expectation === actual; }); }, "in(" + valueToString(arrayOfExpectations) + ")"); }; createMatcher.typeOf = function(type) { assertType(type, "string", "type"); return createMatcher(function(actual) { return typeOf(actual) === type; }, 'typeOf("' + type + '")'); }; createMatcher.instanceOf = function(type) { /* istanbul ignore if */ if ( typeof Symbol === "undefined" || typeof Symbol.hasInstance === "undefined" ) { assertType(type, "function", "type"); } else { assertMethodExists( type, Symbol.hasInstance, "type", "[Symbol.hasInstance]" ); } return createMatcher(function(actual) { return actual instanceof type; }, "instanceOf(" + (functionName(type) || objectToString(type)) + ")"); }; /** * Creates a property matcher * * @private * @param {Function} propertyTest A function to test the property against a value * @param {string} messagePrefix A prefix to use for messages generated by the matcher * @returns {object} A matcher */ function createPropertyMatcher(propertyTest, messagePrefix) { return function(property, value) { assertType(property, "string", "property"); var onlyProperty = arguments.length === 1; var message = messagePrefix + '("' + property + '"'; if (!onlyProperty) { message += ", " + valueToString(value); } message += ")"; return createMatcher(function(actual) { if ( actual === undefined || actual === null || !propertyTest(actual, property) ) { return false; } return onlyProperty || deepEqual(actual[property], value); }, message); }; } createMatcher.has = createPropertyMatcher(function(actual, property) { if (typeof actual === "object") { return property in actual; } return actual[property] !== undefined; }, "has"); createMatcher.hasOwn = createPropertyMatcher(function(actual, property) { return hasOwnProperty(actual, property); }, "hasOwn"); createMatcher.hasNested = function(property, value) { assertType(property, "string", "property"); var onlyProperty = arguments.length === 1; var message = 'hasNested("' + property + '"'; if (!onlyProperty) { message += ", " + valueToString(value); } message += ")"; return createMatcher(function(actual) { if ( actual === undefined || actual === null || get(actual, property) === undefined ) { return false; } return onlyProperty || deepEqual(get(actual, property), value); }, message); }; var jsonParseResultTypes = { null: true, boolean: true, number: true, string: true, object: true, array: true }; createMatcher.json = function(value) { if (!jsonParseResultTypes[typeOf(value)]) { throw new TypeError("Value cannot be the result of JSON.parse"); } var message = "json(" + JSON.stringify(value, null, " ") + ")"; return createMatcher(function(actual) { var parsed; try { parsed = JSON.parse(actual); } catch (e) { return false; } return deepEqual(parsed, value); }, message); }; createMatcher.every = function(predicate) { assertMatcher(predicate); return createMatcher(function(actual) { if (typeOf(actual) === "object") { return every(Object.keys(actual), function(key) { return predicate.test(actual[key]); }); } return ( isIterable(actual) && every(actual, function(element) { return predicate.test(element); }) ); }, "every(" + predicate.message + ")"); }; createMatcher.some = function(predicate) { assertMatcher(predicate); return createMatcher(function(actual) { if (typeOf(actual) === "object") { return !every(Object.keys(actual), function(key) { return !predicate.test(actual[key]); }); } return ( isIterable(actual) && !every(actual, function(element) { return !predicate.test(element); }) ); }, "some(" + predicate.message + ")"); }; createMatcher.array = createMatcher.typeOf("array"); createMatcher.array.deepEquals = function(expectation) { return createMatcher(function(actual) { // Comparing lengths is the fastest way to spot a difference before iterating through every item var sameLength = actual.length === expectation.length; return ( typeOf(actual) === "array" && sameLength && every(actual, function(element, index) { var expected = expectation[index]; return typeOf(expected) === "array" && typeOf(element) === "array" ? createMatcher.array.deepEquals(expected).test(element) : deepEqual(expected, element); }) ); }, "deepEquals([" + iterableToString(expectation) + "])"); }; createMatcher.array.startsWith = function(expectation) { return createMatcher(function(actual) { return ( typeOf(actual) === "array" && every(expectation, function(expectedElement, index) { return actual[index] === expectedElement; }) ); }, "startsWith([" + iterableToString(expectation) + "])"); }; createMatcher.array.endsWith = function(expectation) { return createMatcher(function(actual) { // This indicates the index in which we should start matching var offset = actual.length - expectation.length; return ( typeOf(actual) === "array" && every(expectation, function(expectedElement, index) { return actual[offset + index] === expectedElement; }) ); }, "endsWith([" + iterableToString(expectation) + "])"); }; createMatcher.array.contains = function(expectation) { return createMatcher(function(actual) { return ( typeOf(actual) === "array" && every(expectation, function(expectedElement) { return arrayIndexOf(actual, expectedElement) !== -1; }) ); }, "contains([" + iterableToString(expectation) + "])"); }; createMatcher.map = createMatcher.typeOf("map"); createMatcher.map.deepEquals = function mapDeepEquals(expectation) { return createMatcher(function(actual) { // Comparing lengths is the fastest way to spot a difference before iterating through every item var sameLength = actual.size === expectation.size; return ( typeOf(actual) === "map" && sameLength && every(actual, function(element, key) { return expectation.has(key) && expectation.get(key) === element; }) ); }, "deepEquals(Map[" + iterableToString(expectation) + "])"); }; createMatcher.map.contains = function mapContains(expectation) { return createMatcher(function(actual) { return ( typeOf(actual) === "map" && every(expectation, function(element, key) { return actual.has(key) && actual.get(key) === element; }) ); }, "contains(Map[" + iterableToString(expectation) + "])"); }; createMatcher.set = createMatcher.typeOf("set"); createMatcher.set.deepEquals = function setDeepEquals(expectation) { return createMatcher(function(actual) { // Comparing lengths is the fastest way to spot a difference before iterating through every item var sameLength = actual.size === expectation.size; return ( typeOf(actual) === "set" && sameLength && every(actual, function(element) { return expectation.has(element); }) ); }, "deepEquals(Set[" + iterableToString(expectation) + "])"); }; createMatcher.set.contains = function setContains(expectation) { return createMatcher(function(actual) { return ( typeOf(actual) === "set" && every(expectation, function(element) { return actual.has(element); }) ); }, "contains(Set[" + iterableToString(expectation) + "])"); }; createMatcher.bool = createMatcher.typeOf("boolean"); createMatcher.number = createMatcher.typeOf("number"); createMatcher.string = createMatcher.typeOf("string"); createMatcher.object = createMatcher.typeOf("object"); createMatcher.func = createMatcher.typeOf("function"); createMatcher.regexp = createMatcher.typeOf("regexp"); createMatcher.date = createMatcher.typeOf("date"); createMatcher.symbol = createMatcher.typeOf("symbol"); module.exports = createMatcher;