UNPKG

unexpected

Version:
1,772 lines (1,628 loc) 52.3 kB
const createStandardErrorMessage = require('./createStandardErrorMessage'); const utils = require('./utils'); const magicpen = require('magicpen'); const extend = utils.extend; const leven = require('leven'); const makePromise = require('./makePromise'); const addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods'); const wrapPromiseIfNecessary = require('./wrapPromiseIfNecessary'); const oathbreaker = require('./oathbreaker'); const UnexpectedError = require('./UnexpectedError'); const notifyPendingPromise = require('./notifyPendingPromise'); const defaultDepth = require('./defaultDepth'); const createWrappedExpectProto = require('./createWrappedExpectProto'); const AssertionString = require('./AssertionString'); const throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError'); const makeDiffResultBackwardsCompatible = require('./makeDiffResultBackwardsCompatible'); function isAssertionArg({ type }) { return type.is('assertion'); } function Context(unexpected) { this.expect = unexpected; this.level = 0; } Context.prototype.child = function() { const child = Object.create(this); child.level++; return child; }; const anyType = { _unexpectedType: true, name: 'any', level: 0, identify() { return true; }, equal: utils.objectIs, inspect(value, depth, output) { if (output && output.isMagicPen) { return output.text(value); } else { // Guard against node.js' require('util').inspect eagerly calling .inspect() on objects return `type: ${this.name}`; } }, diff(actual, expected, output, diff, inspect) { return null; }, typeEqualityCache: {}, is(typeOrTypeName) { let typeName; if (typeof typeOrTypeName === 'string') { typeName = typeOrTypeName; } else { typeName = typeOrTypeName.name; } const cachedValue = this.typeEqualityCache[typeName]; if (typeof cachedValue !== 'undefined') { return cachedValue; } let result = false; if (this.name === typeName) { result = true; } else if (this.baseType) { result = this.baseType.is(typeName); } this.typeEqualityCache[typeName] = result; return result; } }; function Unexpected(options = {}) { this.assertions = options.assertions || {}; this.typeByName = options.typeByName || { any: anyType }; this.types = options.types || [anyType]; if (options.output) { this.output = options.output; } else { this.output = magicpen(); this.output.inline = false; this.output.diff = false; } this._outputFormat = options.format || magicpen.defaultFormat; this.installedPlugins = options.installedPlugins || []; // Make bound versions of these two helpers up front to save a bit when creating wrapped expects: const that = this; this.getType = typeName => that.typeByName[typeName] || (that.parent && that.parent.getType(typeName)); this.findTypeOf = obj => utils.findFirst( that.types || [], type => type.identify && type.identify(obj) ) || (that.parent && that.parent.findTypeOf(obj)); this.findTypeOfWithParentType = (obj, requiredParentType) => utils.findFirst( that.types || [], type => type.identify && type.identify(obj) && (!requiredParentType || type.is(requiredParentType)) ) || (that.parent && that.parent.findTypeOfWithParentType(obj, requiredParentType)); this.findCommonType = function(a, b) { const aAncestorIndex = {}; let current = this.findTypeOf(a); while (current) { aAncestorIndex[current.name] = current; current = current.baseType; } current = this.findTypeOf(b); while (current) { if (aAncestorIndex[current.name]) { return current; } current = current.baseType; } }; this._wrappedExpectProto = createWrappedExpectProto(this); } const OR = {}; function getOrGroups(expectations) { const orGroups = [[]]; expectations.forEach(expectation => { if (expectation === OR) { orGroups.push([]); } else { orGroups[orGroups.length - 1].push(expectation); } }); return orGroups; } function evaluateGroup(unexpected, context, subject, orGroup) { return orGroup.map(expectation => { const args = Array.prototype.slice.call(expectation); args.unshift(subject); return { expectation: args, promise: makePromise(() => { if (typeof args[1] === 'function') { if (args.length > 2) { throw new Error( 'expect.it(<function>) does not accept additional arguments' ); } else { // expect.it(function (value) { ... }) return args[1](args[0]); } } else { return unexpected._expect(context.child(), args); } }) }; }); } function writeGroupEvaluationsToOutput(output, groupEvaluations) { const hasOrClauses = groupEvaluations.length > 1; const hasAndClauses = groupEvaluations.some(({ length }) => length > 1); groupEvaluations.forEach((groupEvaluation, i) => { if (i > 0) { if (hasAndClauses) { output.nl(); } else { output.sp(); } output.jsComment('or').nl(); } let groupFailed = false; groupEvaluation.forEach((evaluation, j) => { if (j > 0) { output.jsComment(' and').nl(); } const isRejected = evaluation.promise.isRejected(); if (isRejected && !groupFailed) { groupFailed = true; const err = evaluation.promise.reason(); if (hasAndClauses || hasOrClauses) { output.error('⨯ '); } output.block(output => { output.append(err.getErrorMessage(output)); }); } else { if (isRejected) { output.error('⨯ '); } else { output.success('✓ '); } const expectation = evaluation.expectation; output.block(output => { const subject = expectation[0]; const subjectOutput = output => { output.appendInspected(subject); }; const args = expectation.slice(2); const argsOutput = args.map(arg => output => { output.appendInspected(arg); }); const testDescription = expectation[1]; createStandardErrorMessage( output, subjectOutput, testDescription, argsOutput, { subject } ); }); } }); }); } function createExpectIt(unexpected, expectations) { const orGroups = getOrGroups(expectations); function expectIt(subject, context) { context = context && typeof context === 'object' && context instanceof Context ? context : new Context(unexpected); const groupEvaluations = []; const promises = []; orGroups.forEach(orGroup => { const evaluations = evaluateGroup(unexpected, context, subject, orGroup); evaluations.forEach(({ promise }) => { promises.push(promise); }); groupEvaluations.push(evaluations); }); return oathbreaker( makePromise.settle(promises).then(() => { groupEvaluations.forEach(groupEvaluation => { groupEvaluation.forEach(({ promise }) => { if ( promise.isRejected() && promise.reason().errorMode === 'bubbleThrough' ) { throw promise.reason(); } }); }); if ( !groupEvaluations.some(groupEvaluation => groupEvaluation.every(({ promise }) => promise.isFulfilled()) ) ) { unexpected.fail(output => { writeGroupEvaluationsToOutput(output, groupEvaluations); }); } }) ); } expectIt._expectIt = true; expectIt._expectations = expectations; expectIt._OR = OR; expectIt.and = function(...args) { const copiedExpectations = expectations.slice(); copiedExpectations.push(args); return createExpectIt(unexpected, copiedExpectations); }; expectIt.or = function(...args) { const copiedExpectations = expectations.slice(); copiedExpectations.push(OR, args); return createExpectIt(unexpected, copiedExpectations); }; return expectIt; } Unexpected.prototype.it = function(...args) { // ... return createExpectIt(this, [args]); }; Unexpected.prototype.equal = function(actual, expected, depth, seen) { const that = this; depth = typeof depth === 'number' ? depth : 100; if (depth <= 0) { // detect recursive loops in the structure seen = seen || []; if (seen.indexOf(actual) !== -1) { throw new Error('Cannot compare circular structures'); } seen.push(actual); } return this.findCommonType(actual, expected).equal(actual, expected, (a, b) => that.equal(a, b, depth - 1, seen) ); }; Unexpected.prototype.inspect = function(obj, depth, outputOrFormat) { let seen = []; const that = this; function printOutput(obj, currentDepth, output) { const objType = that.findTypeOf(obj); if (currentDepth <= 0 && objType.is('object') && !objType.is('expect.it')) { return output.text('...'); } seen = seen || []; if (seen.indexOf(obj) !== -1) { return output.text('[Circular]'); } return objType.inspect(obj, currentDepth, output, (v, childDepth) => { output = output.clone(); seen.push(obj); if (typeof childDepth === 'undefined') { childDepth = currentDepth - 1; } output = printOutput(v, childDepth, output) || output; seen.pop(); return output; }); } let output = typeof outputOrFormat === 'string' ? this.createOutput(outputOrFormat) : outputOrFormat; output = output || this.createOutput(); return ( printOutput( obj, typeof depth === 'number' ? depth : defaultDepth, output ) || output ); }; Unexpected.prototype.expandTypeAlternations = function(assertion) { const that = this; function createPermutations(args, i) { if (i === args.length) { return []; } const result = []; args[i].forEach(arg => { const tails = createPermutations(args, i + 1); if (tails.length) { tails.forEach(tail => { result.push([arg].concat(tail)); }); } else if (arg.type.is('assertion')) { result.push([ { type: arg.type, minimum: 1, maximum: 1 }, { type: that.getType('any'), minimum: 0, maximum: Infinity } ]); result.push([ { type: that.getType('expect.it'), minimum: 1, maximum: 1 } ]); if (arg.minimum === 0) { // <assertion?> result.push([]); } } else { result.push([arg]); } }); return result; } const result = []; assertion.subject.forEach(subjectRequirement => { if (assertion.args.length) { createPermutations(assertion.args, 0).forEach(args => { result.push( extend({}, assertion, { subject: subjectRequirement, args }) ); }); } else { result.push( extend({}, assertion, { subject: subjectRequirement, args: [] }) ); } }); return result; }; Unexpected.prototype.parseAssertion = function(assertionString) { const that = this; const tokens = []; let nextIndex = 0; function parseTypeToken(typeToken) { return typeToken.split('|').map(typeDeclaration => { const matchNameAndOperator = typeDeclaration.match( /^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i ); if (!matchNameAndOperator) { throw new SyntaxError( `Cannot parse type declaration:${typeDeclaration}` ); } const type = that.getType(matchNameAndOperator[1]); if (!type) { throw new Error( `Unknown type: ${matchNameAndOperator[1]} in ${assertionString}` ); } const operator = matchNameAndOperator[2]; return { minimum: !operator || operator === '+' ? 1 : 0, maximum: operator === '*' || operator === '+' ? Infinity : 1, type }; }); } function hasVarargs(types) { return types.some(({ minimum, maximum }) => minimum !== 1 || maximum !== 1); } assertionString.replace( /\s*<((?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?)(?:\|(?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?))*)>|\s*([^<]+)/gi, ({ length }, $1, $2, index) => { if (index !== nextIndex) { throw new SyntaxError( `Cannot parse token at index ${nextIndex} in ${assertionString}` ); } if ($1) { tokens.push(parseTypeToken($1)); } else { tokens.push($2.trim()); } nextIndex += length; } ); let assertion; if (tokens.length === 1 && typeof tokens[0] === 'string') { assertion = { subject: parseTypeToken('any'), assertion: tokens[0], args: [parseTypeToken('any*')] }; } else { assertion = { subject: tokens[0], assertion: tokens[1], args: tokens.slice(2) }; } if (!Array.isArray(assertion.subject)) { throw new SyntaxError(`Missing subject type in ${assertionString}`); } if (typeof assertion.assertion !== 'string') { throw new SyntaxError(`Missing assertion in ${assertionString}`); } if (hasVarargs(assertion.subject)) { throw new SyntaxError( `The subject type cannot have varargs: ${assertionString}` ); } if (assertion.args.some(arg => typeof arg === 'string')) { throw new SyntaxError('Only one assertion string is supported (see #225)'); } if (assertion.args.slice(0, -1).some(hasVarargs)) { throw new SyntaxError( `Only the last argument type can have varargs: ${assertionString}` ); } if ( [assertion.subject] .concat(assertion.args.slice(0, -1)) .some(argRequirements => argRequirements.some(({ type }) => type.is('assertion')) ) ) { throw new SyntaxError( `Only the last argument type can be <assertion>: ${assertionString}` ); } const lastArgRequirements = assertion.args[assertion.args.length - 1] || []; const assertionRequirements = lastArgRequirements.filter(({ type }) => type.is('assertion') ); if (assertionRequirements.length > 0 && lastArgRequirements.length > 1) { throw new SyntaxError( `<assertion> cannot be alternated with other types: ${assertionString}` ); } if (assertionRequirements.some(({ maximum }) => maximum !== 1)) { throw new SyntaxError( `<assertion+> and <assertion*> are not allowed: ${assertionString}` ); } return this.expandTypeAlternations(assertion); }; const placeholderSplitRegexp = /(\{(?:\d+)\})/g; const placeholderRegexp = /\{(\d+)\}/; Unexpected.prototype.fail = function(arg) { if (arg instanceof UnexpectedError) { arg._hasSerializedErrorMessage = false; throw arg; } if (utils.isError(arg)) { throw arg; } const error = new UnexpectedError(this.expect); if (typeof arg === 'function') { error.errorMode = 'bubble'; error.output = arg; } else if (arg && typeof arg === 'object') { if (typeof arg.message !== 'undefined') { error.errorMode = 'bubble'; } error.output = output => { if (typeof arg.message !== 'undefined') { if (arg.message.isMagicPen) { output.append(arg.message); } else if (typeof arg.message === 'function') { arg.message.call(output, output); } else { output.text(String(arg.message)); } } else { output.error('Explicit failure'); } }; const expect = this.expect; Object.keys(arg).forEach(function(key) { const value = arg[key]; if (key === 'diff') { if (typeof value === 'function' && this.parent) { error.createDiff = (output, diff, inspect, equal) => { const childOutput = expect.createOutput(output.format); childOutput.inline = output.inline; childOutput.output = output.output; return value( childOutput, function diff(actual, expected) { return expect.diff(actual, expected, childOutput.clone()); }, function inspect(v, depth) { return childOutput .clone() .appendInspected(v, (depth || defaultDepth) - 1); }, (actual, expected) => expect.equal(actual, expected) ); }; } else { error.createDiff = value; } } else if (key !== 'message') { error[key] = value; } }, this); } else { let placeholderArgs; if (arguments.length > 0) { placeholderArgs = new Array(arguments.length - 1); for (let i = 1; i < arguments.length; i += 1) { placeholderArgs[i - 1] = arguments[i]; } } error.errorMode = 'bubble'; error.output = output => { const message = arg ? String(arg) : 'Explicit failure'; const tokens = message.split(placeholderSplitRegexp); tokens.forEach(token => { const match = placeholderRegexp.exec(token); if (match) { const index = match[1]; if (index in placeholderArgs) { const placeholderArg = placeholderArgs[index]; if (placeholderArg && placeholderArg.isMagicPen) { output.append(placeholderArg); } else { output.appendInspected(placeholderArg); } } else { output.text(match[0]); } } else { output.error(token); } }); }; } throw error; }; function compareSpecificities(a, b) { for (let i = 0; i < Math.min(a.length, b.length); i += 1) { const c = b[i] - a[i]; if (c !== 0) { return c; } } return b.length - a.length; } function calculateAssertionSpecificity({ subject, args }) { return [subject.type.level].concat( args.map(({ minimum, maximum, type }) => { const bonus = minimum === 1 && maximum === 1 ? 0.5 : 0; return bonus + type.level; }) ); } Unexpected.prototype.addAssertion = function( patternOrPatterns, handler, childUnexpected ) { if (this._frozen) { throw new Error( 'Cannot add an assertion to a frozen instance, please run .clone() first' ); } let maxArguments; if (typeof childUnexpected === 'object') { maxArguments = 3; } else { maxArguments = 2; } if ( arguments.length > maxArguments || typeof handler !== 'function' || (typeof patternOrPatterns !== 'string' && !Array.isArray(patternOrPatterns)) ) { let errorMessage = 'Syntax: expect.addAssertion(<string|array[string]>, function (expect, subject, ...) { ... });'; if ( (typeof handler === 'string' || Array.isArray(handler)) && typeof arguments[2] === 'function' ) { errorMessage += '\nAs of Unexpected 10, the syntax for adding assertions that apply only to specific\n' + 'types has changed. See http://unexpected.js.org/api/addAssertion/'; } throw new Error(errorMessage); } const patterns = Array.isArray(patternOrPatterns) ? patternOrPatterns : [patternOrPatterns]; patterns.forEach(pattern => { if (typeof pattern !== 'string' || pattern === '') { throw new Error('Assertion patterns must be a non-empty string'); } else { if (pattern !== pattern.trim()) { throw new Error( `Assertion patterns can't start or end with whitespace:\n\n ${JSON.stringify( pattern )}` ); } } }); const that = this; const assertions = this.assertions; const defaultValueByFlag = {}; const assertionHandlers = []; let maxNumberOfArgs = 0; patterns.forEach(pattern => { const assertionDeclarations = that.parseAssertion(pattern); assertionDeclarations.forEach(({ assertion, args, subject }) => { ensureValidUseOfParenthesesOrBrackets(assertion); const expandedAssertions = expandAssertion(assertion); expandedAssertions.forEach(({ flags, alternations, text }) => { Object.keys(flags).forEach(flag => { defaultValueByFlag[flag] = false; }); maxNumberOfArgs = Math.max( maxNumberOfArgs, args.reduce( (previous, { maximum }) => previous + (maximum === null ? Infinity : maximum), 0 ) ); assertionHandlers.push({ handler, alternations: alternations, flags: flags, subject: subject, args: args, testDescriptionString: text, declaration: pattern, unexpected: childUnexpected }); }); }); }); if (handler.length - 2 > maxNumberOfArgs) { throw new Error( `The provided assertion handler takes ${handler.length - 2} parameters, but the type signature specifies a maximum of ${maxNumberOfArgs}:\n\n ${JSON.stringify( patterns )}` ); } assertionHandlers.forEach(handler => { // Make sure that all flags are defined. handler.flags = extend({}, defaultValueByFlag, handler.flags); const assertionHandlers = assertions[handler.testDescriptionString]; handler.specificity = calculateAssertionSpecificity(handler); if (!assertionHandlers) { assertions[handler.testDescriptionString] = [handler]; } else { let i = 0; while ( i < assertionHandlers.length && compareSpecificities( handler.specificity, assertionHandlers[i].specificity ) > 0 ) { i += 1; } assertionHandlers.splice(i, 0, handler); } }); return this.expect; // for chaining }; Unexpected.prototype.addType = function(type, childUnexpected) { if (this._frozen) { throw new Error( 'Cannot add a type to a frozen instance, please run .clone() first' ); } const that = this; let baseType; if ( typeof type.name !== 'string' || !/^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$/i.test(type.name) ) { throw new Error( 'A type must be given a non-empty name and must match ^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$' ); } if (typeof type.identify !== 'function' && type.identify !== false) { throw new Error( `Type ${ type.name } must specify an identify function or be declared abstract by setting identify to false` ); } if (this.typeByName[type.name]) { throw new Error(`The type with the name ${type.name} already exists`); } if (type.base) { baseType = this.getType(type.base); if (!baseType) { throw new Error(`Unknown base type: ${type.base}`); } } else { baseType = anyType; } const extendedBaseType = Object.create(baseType); extendedBaseType.inspect = (value, depth, output) => { if (!output || !output.isMagicPen) { throw new Error( 'You need to pass the output to baseType.inspect() as the third parameter' ); } return baseType.inspect(value, depth, output, (value, depth) => output.clone().appendInspected(value, depth) ); }; extendedBaseType.diff = (actual, expected, output) => { if (!output || !output.isMagicPen) { throw new Error( 'You need to pass the output to baseType.diff() as the third parameter' ); } return makeDiffResultBackwardsCompatible( baseType.diff( actual, expected, output.clone(), (actual, expected) => that.diff(actual, expected, output.clone()), (value, depth) => output.clone().appendInspected(value, depth), that.equal.bind(that) ) ); }; extendedBaseType.equal = (actual, expected) => baseType.equal(actual, expected, that.equal.bind(that)); const extendedType = extend({}, baseType, type, { baseType: extendedBaseType }); const originalInspect = extendedType.inspect; extendedType.inspect = function(obj, depth, output, inspect) { if (arguments.length < 2 || (!output || !output.isMagicPen)) { return `type: ${type.name}`; } else if (childUnexpected) { const childOutput = childUnexpected.createOutput(output.format); return ( originalInspect.call(this, obj, depth, childOutput, inspect) || childOutput ); } else { return originalInspect.call(this, obj, depth, output, inspect) || output; } }; if (childUnexpected) { extendedType.childUnexpected = childUnexpected; const originalDiff = extendedType.diff; extendedType.diff = function( actual, expected, output, inspect, diff, equal ) { const childOutput = childUnexpected.createOutput(output.format); // Make sure that already buffered up output is preserved: childOutput.output = output.output; return ( originalDiff.call( this, actual, expected, childOutput, inspect, diff, equal ) || output ); }; } if (extendedType.identify === false) { this.types.push(extendedType); } else { this.types.unshift(extendedType); } extendedType.level = baseType.level + 1; extendedType.typeEqualityCache = {}; this.typeByName[extendedType.name] = extendedType; return this.expect; }; Unexpected.prototype.addStyle = function(...args) { if (this._frozen) { throw new Error( 'Cannot add a style to a frozen instance, please run .clone() first' ); } this.output.addStyle(...args); return this.expect; }; Unexpected.prototype.installTheme = function(...args) { if (this._frozen) { throw new Error( 'Cannot install a theme into a frozen instance, please run .clone() first' ); } this.output.installTheme(...args); return this.expect; }; function getPluginName(plugin) { if (typeof plugin === 'function') { return utils.getFunctionName(plugin); } else { return plugin.name; } } Unexpected.prototype.use = function(plugin) { if (this._frozen) { throw new Error( 'Cannot install a plugin into a frozen instance, please run .clone() first' ); } if ( (typeof plugin !== 'function' && (typeof plugin !== 'object' || typeof plugin.installInto !== 'function')) || (typeof plugin.name !== 'undefined' && typeof plugin.name !== 'string') ) { throw new Error( 'Plugins must be functions or adhere to the following interface\n' + '{\n' + ' name: <an optional plugin name>,\n' + ' version: <an optional semver version string>,\n' + ' installInto: <a function that will update the given expect instance>\n' + '}' ); } const pluginName = getPluginName(plugin); const existingPlugin = utils.findFirst( this.installedPlugins, installedPlugin => { if (installedPlugin === plugin) { return true; } else { return pluginName && pluginName === getPluginName(installedPlugin); } } ); if (existingPlugin) { if ( existingPlugin === plugin || (typeof plugin.version !== 'undefined' && plugin.version === existingPlugin.version) ) { // No-op return this.expect; } else { throw new Error( `Another instance of the plugin '${pluginName}' is already installed${ typeof existingPlugin.version !== 'undefined' ? ` (version ${existingPlugin.version}${ typeof plugin.version !== 'undefined' ? `, trying to install ${plugin.version}` : '' })` : '' }. Please check your node_modules folder for unmet peerDependencies.` ); } } if (pluginName === 'unexpected-promise') { throw new Error( 'The unexpected-promise plugin was pulled into Unexpected as of 8.5.0. This means that the plugin is no longer supported.' ); } this.installedPlugins.push(plugin); if (typeof plugin === 'function') { plugin(this.expect); } else { plugin.installInto(this.expect); } return this.expect; // for chaining }; Unexpected.prototype.withError = (body, handler) => oathbreaker( makePromise(body).caught(e => { throwIfNonUnexpectedError(e); return handler(e); }) ); Unexpected.prototype.installPlugin = Unexpected.prototype.use; // Legacy alias function installExpectMethods(unexpected) { const expect = function(...args) { /// ... return unexpected._expect(new Context(unexpected), args); }; expect.it = unexpected.it.bind(unexpected); expect.equal = unexpected.equal.bind(unexpected); expect.inspect = unexpected.inspect.bind(unexpected); expect.findTypeOf = unexpected.findTypeOf; // Already bound expect.fail = (...args) => { try { unexpected.fail(...args); } catch (e) { if (e && e._isUnexpected) { unexpected.setErrorMessage(e); } throw e; } }; expect.createOutput = unexpected.createOutput.bind(unexpected); expect.diff = unexpected.diff.bind(unexpected); expect.async = unexpected.async.bind(unexpected); expect.promise = makePromise; expect.withError = unexpected.withError; expect.addAssertion = unexpected.addAssertion.bind(unexpected); expect.addStyle = unexpected.addStyle.bind(unexpected); expect.installTheme = unexpected.installTheme.bind(unexpected); expect.addType = unexpected.addType.bind(unexpected); expect.getType = unexpected.getType; expect.clone = unexpected.clone.bind(unexpected); expect.child = unexpected.child.bind(unexpected); expect.freeze = unexpected.freeze.bind(unexpected); expect.toString = unexpected.toString.bind(unexpected); expect.assertions = unexpected.assertions; expect.use = expect.installPlugin = unexpected.use.bind(unexpected); expect.output = unexpected.output; expect.outputFormat = unexpected.outputFormat.bind(unexpected); expect.notifyPendingPromise = notifyPendingPromise; expect.hook = fn => { unexpected._expect = fn(unexpected._expect.bind(unexpected)); }; // TODO For testing purpose while we don't have all the pieces yet expect.parseAssertion = unexpected.parseAssertion.bind(unexpected); return expect; } function calculateLimits(items) { return items.reduce( (result, { minimum, maximum }) => { result.minimum += minimum; result.maximum += maximum; return result; }, { minimum: 0, maximum: 0 } ); } Unexpected.prototype.throwAssertionNotFoundError = function( subject, testDescriptionString, args ) { let candidateHandlers = this.assertions[testDescriptionString]; const that = this; let instance = this; while (instance && !candidateHandlers) { candidateHandlers = instance.assertions[testDescriptionString]; instance = instance.parent; } if (candidateHandlers) { this.fail({ message(output) { const subjectOutput = output => { output.appendInspected(subject); }; const argsOutput = output => { output.appendItems(args, ', '); }; output .append( createStandardErrorMessage( output.clone(), subjectOutput, testDescriptionString, argsOutput ) ) .nl() .indentLines(); output .i() .error('The assertion does not have a matching signature for:') .nl() .indentLines() .i() .text('<') .text(that.findTypeOf(subject).name) .text('>') .sp() .text(testDescriptionString); args.forEach((arg, i) => { output .sp() .text('<') .text(that.findTypeOf(arg).name) .text('>'); }); output .outdentLines() .nl() .i() .text('did you mean:') .indentLines() .nl(); const assertionDeclarations = Object.keys( candidateHandlers.reduce((result, { declaration }) => { result[declaration] = true; return result; }, {}) ).sort(); assertionDeclarations.forEach((declaration, i) => { output .nl(i > 0 ? 1 : 0) .i() .text(declaration); }); output.outdentLines(); } }); } const assertionsWithScore = []; const assertionStrings = []; instance = this; while (instance) { assertionStrings.push(...Object.keys(instance.assertions)); instance = instance.parent; } function compareAssertions(a, b) { const aAssertion = that.lookupAssertionRule(subject, a, args); const bAssertion = that.lookupAssertionRule(subject, b, args); if (!aAssertion && !bAssertion) { return 0; } if (aAssertion && !bAssertion) { return -1; } if (!aAssertion && bAssertion) { return 1; } return compareSpecificities(aAssertion.specificity, bAssertion.specificity); } assertionStrings.forEach(assertionString => { const score = leven(testDescriptionString, assertionString); assertionsWithScore.push({ assertion: assertionString, score }); }, this); const bestMatch = assertionsWithScore .sort((a, b) => { const c = a.score - b.score; if (c !== 0) { return c; } if (a.assertion < b.assertion) { return -1; } else { return 1; } }) .slice(0, 10) .filter(({ score }, i, arr) => Math.abs(score - arr[0].score) <= 2) .sort((a, b) => { const c = compareAssertions(a.assertion, b.assertion); if (c !== 0) { return c; } return a.score - b.score; })[0]; this.fail({ errorMode: 'bubbleThrough', message(output) { output .error("Unknown assertion '") .jsString(testDescriptionString) .error("', did you mean: '") .jsString(bestMatch.assertion) .error("'"); } }); }; Unexpected.prototype.lookupAssertionRule = function( subject, testDescriptionString, args, requireAssertionSuffix ) { const that = this; if (typeof testDescriptionString !== 'string') { throw new Error( 'The expect function requires the second parameter to be a string or an expect.it.' ); } let handlers; let instance = this; while (instance) { const instanceHandlers = instance.assertions[testDescriptionString]; if (instanceHandlers) { handlers = handlers ? handlers.concat(instanceHandlers) : instanceHandlers; } instance = instance.parent; } if (!handlers) { return null; } const cachedTypes = {}; function findTypeOf(value, key) { let type = cachedTypes[key]; if (!type) { type = that.findTypeOf(value); cachedTypes[key] = type; } return type; } function matches(value, assertionType, key, relaxed) { if (assertionType.is('assertion') && typeof value === 'string') { return true; } if (relaxed) { if (assertionType.identify === false) { return that.types.some( type => type.identify && type.is(assertionType) && type.identify(value) ); } return assertionType.identify(value); } else { return findTypeOf(value, key).is(assertionType); } } function matchesHandler(handler, relaxed) { if (!matches(subject, handler.subject.type, 'subject', relaxed)) { return false; } if (requireAssertionSuffix && !handler.args.some(isAssertionArg)) { return false; } const requireArgumentsLength = calculateLimits(handler.args); if ( args.length < requireArgumentsLength.minimum || requireArgumentsLength.maximum < args.length ) { return false; } else if (args.length === 0 && requireArgumentsLength.maximum === 0) { return true; } const lastRequirement = handler.args[handler.args.length - 1]; return args.every((arg, i) => { if (i < handler.args.length - 1) { return matches(arg, handler.args[i].type, i, relaxed); } else { return matches(arg, lastRequirement.type, i, relaxed); } }); } let j, handler; for (j = 0; j < handlers.length; j += 1) { handler = handlers[j]; if (matchesHandler(handler)) { return handler; } } for (j = 0; j < handlers.length; j += 1) { handler = handlers[j]; if (matchesHandler(handler, true)) { return handler; } } return null; }; function makeExpectFunction(unexpected) { const expect = installExpectMethods(unexpected); unexpected.expect = expect; return expect; } Unexpected.prototype.setErrorMessage = function(err) { err.serializeMessage(this.outputFormat()); }; Unexpected.prototype._expect = function expect(context, args) { const that = this; const subject = args[0]; const testDescriptionString = args[1]; if (args.length < 2) { throw new Error('The expect function requires at least two parameters.'); } else if (testDescriptionString && testDescriptionString._expectIt) { return that.expect.withError( () => testDescriptionString(subject), err => { that.fail(err); } ); } function executeExpect(context, subject, testDescriptionString, args) { let assertionRule = that.lookupAssertionRule( subject, testDescriptionString, args ); if (!assertionRule) { const tokens = testDescriptionString.split(' '); // eslint-disable-next-line no-labels OUTER: for (let n = tokens.length - 1; n > 0; n -= 1) { const prefix = tokens.slice(0, n).join(' '); const remainingTokens = tokens.slice(n); const argsWithAssertionPrepended = [remainingTokens.join(' ')].concat( args ); assertionRule = that.lookupAssertionRule( subject, prefix, argsWithAssertionPrepended, true ); if (assertionRule) { // Found the longest prefix of the string that yielded a suitable assertion for the given subject and args // To avoid bogus error messages when shifting later (#394) we require some prefix of the remaining tokens // to be a valid assertion name: for (let i = 1; i < remainingTokens.length; i += 1) { if ( that.assertions.hasOwnProperty( remainingTokens.slice(0, i + 1).join(' ') ) ) { testDescriptionString = prefix; args = argsWithAssertionPrepended; // eslint-disable-next-line no-labels break OUTER; } } } } if (!assertionRule) { that.throwAssertionNotFoundError(subject, testDescriptionString, args); } } if ( assertionRule && assertionRule.unexpected && assertionRule.unexpected !== that ) { return assertionRule.unexpected.expect( subject, testDescriptionString, ...args ); } const flags = extend({}, assertionRule.flags); const wrappedExpect = function(subject, testDescriptionString) { if (arguments.length === 0) { throw new Error('The expect function requires at least one parameter.'); } else if (arguments.length === 1) { return addAdditionalPromiseMethods( makePromise.resolve(subject), wrappedExpect, subject ); } else if (testDescriptionString && testDescriptionString._expectIt) { wrappedExpect.errorMode = 'nested'; return wrappedExpect.withError( () => testDescriptionString(subject), err => { wrappedExpect.fail(err); } ); } testDescriptionString = utils.forwardFlags(testDescriptionString, flags); const args = new Array(arguments.length - 2); for (let i = 2; i < arguments.length; i += 1) { args[i - 2] = arguments[i]; } return wrappedExpect.callInNestedContext(() => executeExpect(context.child(), subject, testDescriptionString, args) ); }; utils.setPrototypeOfOrExtend(wrappedExpect, that._wrappedExpectProto); wrappedExpect.context = context; wrappedExpect.execute = wrappedExpect; wrappedExpect.alternations = assertionRule.alternations; wrappedExpect.flags = flags; wrappedExpect.subject = subject; wrappedExpect.testDescription = testDescriptionString; wrappedExpect.args = args; wrappedExpect.assertionRule = assertionRule; wrappedExpect.subjectOutput = output => { output.appendInspected(subject); }; wrappedExpect.argsOutput = args.map((arg, i) => { const argRule = wrappedExpect.assertionRule.args[i]; if ( typeof arg === 'string' && ((argRule && argRule.type.is('assertion')) || wrappedExpect._getAssertionIndices().indexOf(i) >= 0) ) { return new AssertionString(arg); } return output => { output.appendInspected(arg); }; }); // Eager-compute these properties in browsers that don't support getters // (Object.defineProperty might be polyfilled by es5-sham): if (!Object.__defineGetter__) { wrappedExpect.subjectType = wrappedExpect._getSubjectType(); wrappedExpect.argTypes = wrappedExpect._getArgTypes(); } return oathbreaker( assertionRule.handler.call(wrappedExpect, wrappedExpect, subject, ...args) ); } try { let result = executeExpect( context, subject, testDescriptionString, Array.prototype.slice.call(args, 2) ); if (utils.isPromise(result)) { result = wrapPromiseIfNecessary(result); if (result.isPending()) { that.expect.notifyPendingPromise(result); result = result.then(undefined, e => { if (e && e._isUnexpected && context.level === 0) { that.setErrorMessage(e); } throw e; }); } } else { result = makePromise.resolve(result); } return addAdditionalPromiseMethods(result, that.expect, subject); } catch (e) { if (e && e._isUnexpected) { let newError = e; if (typeof mochaPhantomJS !== 'undefined') { newError = e.clone(); } if (context.level === 0) { that.setErrorMessage(newError); } throw newError; } throw e; } }; Unexpected.prototype.async = function(cb) { const that = this; function asyncMisusage(message) { that._isAsync = false; that.expect.fail(output => { output .error(message) .nl() .text('Usage: ') .nl() .text("it('test description', expect.async(function () {") .nl() .indentLines() .i() .text( "return expect('test.txt', 'to have content', 'Content read asynchroniously');" ) .nl() .outdentLines() .text('});'); }); } if (typeof cb !== 'function' || cb.length !== 0) { asyncMisusage('expect.async requires a callback without arguments.'); } return done => { if (that._isAsync) { asyncMisusage("expect.async can't be within a expect.async context."); } that._isAsync = true; if (typeof done !== 'function') { asyncMisusage( 'expect.async should be called in the context of an it-block\n' + 'and the it-block should supply a done callback.' ); } let result; try { result = cb(); } finally { that._isAsync = false; } if (!result || typeof result.then !== 'function') { asyncMisusage( 'expect.async requires the block to return a promise or throw an exception.' ); } result.then( () => { that._isAsync = false; done(); }, err => { that._isAsync = false; done(err); } ); }; }; Unexpected.prototype.diff = function( a, b, output = this.createOutput(), recursions, seen ) { const that = this; const maxRecursions = 100; recursions = typeof recursions === 'number' ? recursions : maxRecursions; if (recursions <= 0) { // detect recursive loops in the structure seen = seen || []; if (seen.indexOf(a) !== -1) { throw new Error('Cannot compare circular structures'); } seen.push(a); } return makeDiffResultBackwardsCompatible( this.findCommonType(a, b).diff( a, b, output, (actual, expected) => that.diff(actual, expected, output.clone(), recursions - 1, seen), (v, depth) => output.clone().appendInspected(v, depth), (actual, expected) => that.equal(actual, expected) ) ); }; Unexpected.prototype.toString = function() { const assertions = this.assertions; const seen = {}; const declarations = []; const pen = magicpen(); Object.keys(assertions) .sort() .forEach(key => { assertions[key].forEach(({ declaration }) => { if (!seen[declaration]) { declarations.push(declaration); seen[declaration] = true; } }); }); declarations.forEach(declaration => { pen.text(declaration).nl(); }); return pen.toString(); }; Unexpected.prototype.clone = function() { const clonedAssertions = {}; Object.keys(this.assertions).forEach(function(assertion) { clonedAssertions[assertion] = [].concat(this.assertions[assertion]); }, this); const unexpected = new Unexpected({ assertions: clonedAssertions, types: [].concat(this.types), typeByName: extend({}, this.typeByName), output: this.output.clone(), format: this.outputFormat(), installedPlugins: [].concat(this.installedPlugins) }); // Install the hooks: unexpected._expect = this._expect; return makeExpectFunction(unexpected); }; Unexpected.prototype.child = function() { const childUnexpected = new Unexpected({ assertions: {}, types: [], typeByName: {}, output: this.output.clone(), format: this.outputFormat(), installedPlugins: [] }); const parent = (childUnexpected.parent = this); const childExpect = makeExpectFunction(childUnexpected); childExpect.exportAssertion = function(testDescription, handler) { parent.addAssertion(testDescription, handler, childUnexpected); return this; }; childExpect.exportType = function(type) { if (childExpect.getType(type.name) !== type) { childExpect.addType(type); } parent.addType(type, childUnexpected); return this; }; childExpect.exportStyle = function(name, handler) { parent.addStyle(name, function(...args) { const childOutput = childExpect.createOutput(this.format); this.append(handler.call(childOutput, ...args) || childOutput); }); return this; }; return childExpect; }; Unexpected.prototype.freeze = function() { this._frozen = true; return this.expect; }; Unexpected.prototype.outputFormat = function(format) { if (typeof format === 'undefined') { return this._outputFormat; } else { this._outputFormat = format; return this.expect; } }; Unexpected.prototype.createOutput = function(format) { const that = this; const output = this.output.clone(format || 'text'); output.addStyle('appendInspected', function(value, depth) { this.append(that.inspect(value, depth, this.clone())); }); return output; }; Unexpected.create = () => { const unexpected = new Unexpected(); return makeExpectFunction(unexpected); }; var expandAssertion = (() => { function isFlag(token) { return token.slice(0, 1) === '[' && token.slice(-1) === ']'; } function isAlternation(token) { return token.slice(0, 1) === '(' && token.slice(-1) === ')'; } function removeEmptyStrings(texts) { return texts.filter(text => text !== ''); } function createPermutations(tokens, index) { if (index === tokens.length) { return [{ text: '', flags: {}, alternations: [] }]; } const token = tokens[index]; const tail = createPermutations(tokens, index + 1); if (isFlag(token)) { const flag = token.slice(1, -1); return tail .map(pattern => { const flags = {}; flags[flag] = true; return { text: `${flag} ${pattern.text}`, flags: extend(flags, pattern.flags), alternations: pattern.alternations }; }) .concat( tail.map(pattern => { const flags = {}; flags[flag] = false; return { text: pattern.text, flags: extend(flags, pattern.flags), alternations: pattern.alternations }; }) ); } else if (isAlternation(token)) { return token .substr(1, token.length - 2) // Remove parentheses .split(/\|/) .reduce( (result, alternation) => result.concat( tail.map(({ text, flags, alternations }) => ({ // Make sure that an empty alternation doesn't produce two spaces: text: alternation ? alternation + text : text.replace(/^ /, ''), flags: flags, alternations: [alternation].concat(alternations) })) ), [] ); } else { return tail.map(({ text, flags, alternations }) => ({ text: token + text, flags: flags, alternations: alternations })); } } return pattern => { pattern = pattern.replace(/(\[[^\]]+\]) ?/g, '$1'); const splitRe