diff --git a/lib/addAdditionalPromiseMethods.js b/lib/addAdditionalPromiseMethods.js deleted file mode 100644 index dbb00bb8b..000000000 --- a/lib/addAdditionalPromiseMethods.js +++ /dev/null @@ -1,25 +0,0 @@ -function addAdditionalPromiseMethods(promise, expect, subject) { - promise.and = function (...args) { - function executeAnd() { - if (expect.findTypeOf(args[0]).is('expect.it')) { - return addAdditionalPromiseMethods(args[0](subject), expect, subject); - } else { - return expect(subject, ...args); - } - } - - if (this.isFulfilled()) { - return executeAnd(); - } else { - return addAdditionalPromiseMethods( - this.then(executeAnd), - expect, - subject - ); - } - }; - - return promise; -} - -module.exports = addAdditionalPromiseMethods; diff --git a/lib/createTopLevelExpect.js b/lib/createTopLevelExpect.js index f18287dd2..5becbe075 100644 --- a/lib/createTopLevelExpect.js +++ b/lib/createTopLevelExpect.js @@ -4,7 +4,6 @@ const magicpen = require('magicpen'); const extend = utils.extend; const ukkonen = require('ukkonen'); const makePromise = require('./makePromise'); -const addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods'); const wrapPromiseIfNecessary = require('./wrapPromiseIfNecessary'); const oathbreaker = require('./oathbreaker'); const UnexpectedError = require('./UnexpectedError'); @@ -244,6 +243,30 @@ function createExpectIt(expect, expectations, forwardedFlags) { copiedExpectations.push(OR, args); return createExpectIt(expect, copiedExpectations, forwardedFlags); }; + + const camelCaseMethods = expect._camelCaser(expect.context || new Context()); + Object.keys(camelCaseMethods).forEach((methodName) => { + expectIt[`and${methodName.replace(/^[a-z]/, ($0) => $0.toUpperCase())}`] = + function (...args) { + const copiedExpectations = expectations.slice(); + copiedExpectations.push([ + methodName.replace(/[A-Z]/g, ($0) => ` ${$0.toLowerCase()}`), // Eeeeek, fix this + ...args, + ]); + return createExpectIt(expect, copiedExpectations, forwardedFlags); + }; + + expectIt[`or${methodName.replace(/^[a-z]/, ($0) => $0.toUpperCase())}`] = + function (...args) { + const copiedExpectations = expectations.slice(); + copiedExpectations.push(OR, [ + methodName.replace(/[A-Z]/g, ($0) => ` ${$0.toLowerCase()}`), // Eeeeek, fix this + ...args, + ]); + return createExpectIt(expect, copiedExpectations, forwardedFlags); + }; + }); + return expectIt; } @@ -707,6 +730,17 @@ expectPrototype.addAssertion = function ( // Make sure that all flags are defined. handler.flags = extend({}, defaultValueByFlag, handler.flags); + this[ + handler.testDescriptionString.replace(/ [a-z]/g, ($0) => + $0.charAt(1).toUpperCase() + ) + ] = (...args) => + createExpectIt( + this._topLevelExpect, + [[handler.testDescriptionString, ...args]], + this.flags + ); + const assertionHandlers = assertions[handler.testDescriptionString]; handler.specificity = calculateAssertionSpecificity(handler); if (!assertionHandlers) { @@ -1286,6 +1320,90 @@ expectPrototype.setErrorMessage = function (err) { err.serializeMessage(this.outputFormat()); }; +expectPrototype.addAdditionalPromiseMethods = function (promise, subject) { + const wrappedExpect = this; + const expect = this._topLevelExpect; + promise.and = function (...args) { + function executeAnd() { + if (expect.findTypeOf(args[0]).is('expect.it')) { + return expect.addAdditionalPromiseMethods(args[0](subject), subject); + } else { + return expect(subject, ...args); + } + } + + if (this.isFulfilled()) { + return executeAnd(); + } else { + return expect.addAdditionalPromiseMethods(this.then(executeAnd), subject); + } + }; + + const camelCaseMethods = expect._camelCaser(expect.context || new Context()); + Object.keys(camelCaseMethods).forEach((methodName) => { + promise[methodName] = function (...args) { + function execute(shiftedSubject) { + if (expect.findTypeOf(args[0]).is('expect.it')) { + return expect.addAdditionalPromiseMethods( + args[0](shiftedSubject), + subject + ); + } else { + if (wrappedExpect !== expect) { + return wrappedExpect._shifty( + shiftedSubject, + wrappedExpect.args, + [ + methodName.replace(/[A-Z]/g, ($0) => ` ${$0.toLowerCase()}`), + ...args, + ], + true // setArgsOutput + ); + } else { + return expect._executeExpect( + new Context(), + shiftedSubject, + methodName.replace(/[A-Z]/g, ($0) => ` ${$0.toLowerCase()}`), + args + ); + } + } + } + + if (this.isFulfilled()) { + return execute(this.value()); + } else { + return expect.addAdditionalPromiseMethods(this.then(execute), subject); + } + }; + + promise[`and${methodName.replace(/^[a-z]/, ($0) => $0.toUpperCase())}`] = + function (...args) { + function execute() { + if (expect.findTypeOf(args[0]).is('expect.it')) { + return expect.addAdditionalPromiseMethods( + args[0](subject), + subject + ); + } else { + return camelCaseMethods[methodName](subject)(...args); + } + } + + if (this.isFulfilled()) { + return execute(this.value()); + } else { + return expect.addAdditionalPromiseMethods( + this.then(execute), + subject + ); + } + }; + }); + + return promise; +}; + expectPrototype._createWrappedExpect = function ( assertionRule, subject, @@ -1301,9 +1419,8 @@ expectPrototype._createWrappedExpect = function ( if (arguments.length === 0) { throw new Error('The expect function requires at least one parameter.'); } else if (arguments.length === 1) { - return addAdditionalPromiseMethods( + return parentExpect.addAdditionalPromiseMethods( makePromise.resolve(subject), - wrappedExpect, subject ); } else if (typeof testDescriptionString === 'function') { @@ -1422,6 +1539,15 @@ expectPrototype._executeExpect = function ( } } + const wrappedExpect = this._createWrappedExpect( + assertionRule, + subject, + args, + testDescriptionString, + context, + forwardedFlags + ); + if (assertionRule.expect && assertionRule.expect !== this._topLevelExpect) { return assertionRule.expect._expect(context, [ subject, @@ -1430,24 +1556,72 @@ expectPrototype._executeExpect = function ( ]); } - const wrappedExpect = this._createWrappedExpect( - assertionRule, - subject, - args, - testDescriptionString, - context, - forwardedFlags + let result = oathbreaker( + assertionRule.handler(wrappedExpect, subject, ...args) ); - return oathbreaker(assertionRule.handler(wrappedExpect, subject, ...args)); + if (utils.isPromise(result)) { + result = wrapPromiseIfNecessary(result); + if (result.isPending()) { + result = result.then(undefined, (e) => { + if (e && e._isUnexpected && context.level === 0) { + this.setErrorMessage(e); + } + throw e; + }); + this.notifyPendingPromise(result); + } + } else { + result = makePromise.resolve(result); + } + return wrappedExpect.addAdditionalPromiseMethods(result, subject); +}; + +expectPrototype._camelCaser = function (context, subjectType, subject) { + const methods = {}; + let instance = this; + while (instance) { + Object.keys(instance.assertions).forEach((testDescriptionString) => { + if ( + !subjectType || + instance.assertions[testDescriptionString].some((assertion) => + subjectType.is(assertion.subject.type) + ) + ) { + let method = + (subject) => + (...args) => { + return this._expect(context, [ + subject, + testDescriptionString, + ...args, + ]); + }; + + if (subjectType) { + method = method(subject); + } + + methods[ + testDescriptionString.replace(/ [a-z]/g, ($0) => + $0.charAt(1).toUpperCase() + ) + ] = method; + } + }); + instance = instance.parent; + } + return methods; }; expectPrototype._expect = function (context, args, forwardedFlags) { const subject = args[0]; const testDescriptionString = args[1]; - if (args.length < 2) { - throw new Error('The expect function requires at least two parameters.'); + if (args.length < 1) { + throw new Error('The expect function requires at least one parameter.'); + } else if (args.length === 1) { + return this._camelCaser(context, this.findTypeOf(subject), subject); } else if (typeof testDescriptionString === 'function') { return this.withError( () => testDescriptionString(subject), @@ -1458,28 +1632,13 @@ expectPrototype._expect = function (context, args, forwardedFlags) { } try { - let result = this._executeExpect( + return this._executeExpect( context, subject, testDescriptionString, Array.prototype.slice.call(args, 2), forwardedFlags ); - if (utils.isPromise(result)) { - result = wrapPromiseIfNecessary(result); - if (result.isPending()) { - result = result.then(undefined, (e) => { - if (e && e._isUnexpected && context.level === 0) { - this.setErrorMessage(e); - } - throw e; - }); - this.notifyPendingPromise(result); - } - } else { - result = makePromise.resolve(result); - } - return addAdditionalPromiseMethods(result, this, subject); } catch (e) { if (e && e._isUnexpected) { let newError = e; @@ -1552,6 +1711,11 @@ expectPrototype.clone = function () { const clonedAssertions = {}; Object.keys(this.assertions).forEach(function (assertion) { clonedAssertions[assertion] = [].concat(this.assertions[assertion]); + // FIXME: We would get this for free if the assertions were recreated in the clone with addAssertion: + this[assertion.replace(/ [a-z]/g, ($0) => $0.charAt(1).toUpperCase())] = ( + ...args + ) => + createExpectIt(this._topLevelExpect, [[assertion, ...args]], this.flags); }, this); const expect = createTopLevelExpect({ assertions: clonedAssertions, @@ -1742,7 +1906,7 @@ expectPrototype._callInNestedContext = function (callback) { } else { result = makePromise.resolve(result); } - return addAdditionalPromiseMethods(result, this.execute, this.subject); + return this.addAdditionalPromiseMethods(result, this.subject); } catch (e) { if (e && e._isUnexpected) { const wrappedError = new UnexpectedError(this, e); @@ -1753,6 +1917,50 @@ expectPrototype._callInNestedContext = function (callback) { } }; +expectPrototype._shifty = function (subject, args, rest, setArgsOutput) { + const nextArgumentType = this.findTypeOf(rest[0]); + if (setArgsOutput) { + this.argsOutput = (output) => { + args.forEach((arg, index) => { + if (index > 0) { + output.text(', '); + } + output.appendInspected(arg); + }); + + if (args.length > 0) { + output.sp(); + } + if (nextArgumentType.is('string')) { + output.error(rest[0]); + } else if (rest.length > 0) { + output.appendInspected(rest[0]); + } + if (rest.length > 1) { + output.sp(); + } + rest.slice(1).forEach((arg, index) => { + if (index > 0) { + output.text(', '); + } + output.appendInspected(arg); + }); + }; + } + if (nextArgumentType.is('expect.it')) { + return this.withError( + () => rest[0](subject), + (err) => { + this.fail(err); + } + ); + } else if (nextArgumentType.is('string')) { + return this(subject, ...rest); + } else { + return subject; + } +}; + expectPrototype.shift = function (subject, assertionIndex) { this._assertWrappedExpect(); if (arguments.length <= 1) { @@ -1776,48 +1984,8 @@ expectPrototype.shift = function (subject, assertionIndex) { if (assertionIndex !== -1) { const args = this.args.slice(0, assertionIndex); const rest = this.args.slice(assertionIndex); - const nextArgumentType = this.findTypeOf(rest[0]); - if (arguments.length > 1) { - // Legacy - this.argsOutput = (output) => { - args.forEach((arg, index) => { - if (index > 0) { - output.text(', '); - } - output.appendInspected(arg); - }); - - if (args.length > 0) { - output.sp(); - } - if (nextArgumentType.is('string')) { - output.error(rest[0]); - } else if (rest.length > 0) { - output.appendInspected(rest[0]); - } - if (rest.length > 1) { - output.sp(); - } - rest.slice(1).forEach((arg, index) => { - if (index > 0) { - output.text(', '); - } - output.appendInspected(arg); - }); - }; - } - if (nextArgumentType.is('expect.it')) { - return this.withError( - () => rest[0](subject), - (err) => { - this.fail(err); - } - ); - } else if (nextArgumentType.is('string')) { - return this.execute(subject, ...rest); - } else { - return subject; - } + const setArgsOutput = arguments.length > 1; + return this._shifty(subject, args, rest, setArgsOutput); } else { // No assertion to delegate to. Provide the new subject as the fulfillment value: return subject; diff --git a/test/camelCaseSyntax.spec.js b/test/camelCaseSyntax.spec.js new file mode 100644 index 000000000..29d971486 --- /dev/null +++ b/test/camelCaseSyntax.spec.js @@ -0,0 +1,230 @@ +/* global expect */ +describe('camel case syntax', () => { + describe('with an assertion that is defined for the given subject type', () => { + it('should succeed', () => { + expect(456).toBeGreaterThan(123); + }); + + it('should fail', () => { + expect(() => expect(123).toBeGreaterThan(456)).toThrow( + 'expected 123 to be greater than 456' + ); + }); + + describe('with a flag', () => { + it('should succeed', () => { + expect(123).notToBeGreaterThan(456); + }); + + it('should fail', () => { + expect(() => expect(456).notToBeGreaterThan(123)).toThrow( + 'expected 456 not to be greater than 123' + ); + }); + }); + }); + + describe('within an assertion', () => { + it('should succeed', () => { + const clonedExpect = expect.clone(); + clonedExpect.addAssertion(' to foo', (expect, subject) => + expect(subject).toEqual('foo') + ); + clonedExpect('foo').toFoo(); + }); + + it('should fail', () => { + const clonedExpect = expect.clone(); + clonedExpect.addAssertion(' to foo', (expect, subject) => + expect(subject).toEqual('foo') + ); + expect(() => clonedExpect('bar').toFoo()).toThrow( + "expected 'bar' to equal 'foo'\n" + '\n' + '-bar\n' + '+foo' + ); + }); + }); + + describe('in a clone', () => { + it('should succeed', () => { + expect.clone()(456).toBeGreaterThan(123); + }); + + it('should fail', () => { + expect(() => expect.clone()(123).toBeGreaterThan(456)).toThrow( + 'expected 123 to be greater than 456' + ); + }); + + it('should not support an assertion subsequently added to the original expect', () => { + const originalExpect = expect.clone(); + const clonedExpect = originalExpect.clone(); + originalExpect.addAssertion(' to foo', (expect, subject) => + expect(subject).toEqual('foo') + ); + expect(clonedExpect('foo').toFoo).notToBeAFunction(); + }); + }); + + describe('in a child', () => { + it('should succeed', () => { + expect.child()(456).toBeGreaterThan(123); + }); + + it('should fail', () => { + expect(() => expect.child()(123).toBeGreaterThan(456)).toThrow( + 'expected 123 to be greater than 456' + ); + }); + + it('should support an assertion subsequently added to the parent', () => { + const parentExpect = expect.clone(); + const childExpect = parentExpect.child(); + parentExpect.addAssertion(' to foo', (expect, subject) => + expect(subject).toEqual('foo') + ); + childExpect('foo').toFoo(); + }); + }); + + describe('with an assertion that is defined for the base of the given subject type', () => { + it('should succeed', () => { + function foo() {} + foo.bar = 123; + expect(foo).toHaveProperty('bar', 123); + }); + + it('should fail', () => { + function foo() {} + expect(() => expect(foo).toHaveProperty('bar', 123)).toThrow( + "expected function foo() {} to have property 'bar', 123" + ); + }); + }); + + describe('with an assertion that is not defined for the given subject type', () => { + it('should not expose a function', () => { + expect(expect(123).toContain).toBeUndefined(); + }); + }); + + describe('with a middle rocket assertion', () => { + it('should succeed', () => { + expect([1, 2, 3]).whenPassedAsParametersTo(Math.max).toEqual(3); + }); + + it('should fail', () => { + expect(() => + expect([1, 2, 3]).whenPassedAsParametersTo(Math.max).toEqual(2) + ).toThrow( + 'expected [ 1, 2, 3 ]\n' + + 'when passed as parameters to function max() { /* native code */ } to equal 2\n' + + ' expected 3 to equal 2' + ); + }); + }); + + describe('with the expect.it equivalent', () => { + it('should succeed', () => { + expect({ foo: 123 }).toSatisfy({ foo: expect.toBeANumber() }); + }); + + describe('and extra chaining with .and...', () => { + it('should succeed', () => { + expect({ foo: 123 }).toSatisfy({ + foo: expect.toBeANumber().andToBeGreaterThan(100), + }); + }); + + it('should fail', () => { + expect(() => + expect({ foo: 123 }).toSatisfy({ + foo: expect.toBeANumber().andToBeGreaterThan(200), + }) + ).toThrow( + 'expected { foo: 123 } to satisfy\n' + + '{\n' + + " foo: expect.it('to be a number')\n" + + " .and('to be greater than', 200)\n" + + '}\n' + + '\n' + + '{\n' + + ' foo: 123 // ✓ should be a number and\n' + + ' // ⨯ should be greater than 200\n' + + '}' + ); + }); + }); + + describe('and extra chaining with .or...', () => { + it('should succeed', () => { + expect({ foo: 123 }).toSatisfy({ + foo: expect.toBeAString().orToBeGreaterThan(100), + }); + }); + + it('should fail', () => { + expect(() => + expect({ foo: 123 }).toSatisfy({ + foo: expect.toBeAString().orToBeGreaterThan(200), + }) + ).toThrow( + 'expected { foo: 123 } to satisfy\n' + + '{\n' + + " foo: expect.it('to be a string')\n" + + " .or('to be greater than', 200)\n" + + '}\n' + + '\n' + + '{\n' + + ' foo: 123 // ⨯ should be a string or\n' + + ' // ⨯ should be greater than 200\n' + + '}' + ); + }); + }); + + it('should fail', () => { + expect(() => + expect({ foo: 123 }).toSatisfy({ foo: expect.notToBeANumber() }) + ).toThrow( + "expected { foo: 123 } to satisfy { foo: expect.it('not to be a number') }\n" + + '\n' + + '{\n' + + ' foo: 123 // should not be a number\n' + + '}' + ); + }); + + describe('in a child', () => { + describe('with an assertion subsequently added to the parent', () => { + it('should succeed', () => { + const parentExpect = expect.clone(); + const childExpect = parentExpect.child(); + parentExpect.addAssertion(' to foo', (expect, subject) => { + return expect(subject).toEqual('foo'); + }); + childExpect({ hey: 'foo' }).toSatisfy({ hey: childExpect.toFoo() }); + }); + + it('should fail', () => { + const parentExpect = expect.clone(); + const childExpect = parentExpect.child(); + parentExpect.addAssertion(' to foo', (expect, subject) => + expect(subject).toEqual('foo') + ); + expect(() => + childExpect({ hey: 'bar' }).toSatisfy({ hey: childExpect.toFoo() }) + ).toThrow( + "expected { hey: 'bar' } to satisfy { hey: expect.it('to foo') }\n" + + '\n' + + '{\n' + + " hey: 'bar' // should equal 'foo'\n" + + ' //\n' + + ' // -bar\n' + + ' // +foo\n' + + '}' + ); + }); + }); + }); + }); +}); diff --git a/test/unexpected.spec.js b/test/unexpected.spec.js index e9fcee6b8..152aecb0f 100644 --- a/test/unexpected.spec.js +++ b/test/unexpected.spec.js @@ -30,17 +30,7 @@ describe('unexpected', () => { expect(); }, 'to throw', - 'The expect function requires at least two parameters.' - ); - }); - - it('fails when given only one parameter', () => { - expect( - function () { - expect('foo'); - }, - 'to throw', - 'The expect function requires at least two parameters.' + 'The expect function requires at least one parameter.' ); });