diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 4da99927..0a55c58e 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -239,6 +239,13 @@ Assertion.addProperty('all', function () { flag(this, 'any', false); }); +const functionTypes = { + 'function': ['function', 'asyncfunction', 'generatorfunction', 'asyncgeneratorfunction'], + 'asyncfunction': ['asyncfunction', 'asyncgeneratorfunction'], + 'generatorfunction': ['generatorfunction', 'asyncgeneratorfunction'], + 'asyncgeneratorfunction': ['asyncgeneratorfunction'] +} + /** * ### .a(type[, msg]) * @@ -298,18 +305,27 @@ Assertion.addProperty('all', function () { * @namespace BDD * @api public */ - function an (type, msg) { if (msg) flag(this, 'message', msg); type = type.toLowerCase(); var obj = flag(this, 'object') , article = ~[ 'a', 'e', 'i', 'o', 'u' ].indexOf(type.charAt(0)) ? 'an ' : 'a '; - this.assert( - type === _.type(obj).toLowerCase() - , 'expected #{this} to be ' + article + type - , 'expected #{this} not to be ' + article + type - ); + const detectedType = _.type(obj).toLowerCase(); + + if (functionTypes['function'].includes(type)) { + this.assert( + functionTypes[type].includes(detectedType) + , 'expected #{this} to be ' + article + type + , 'expected #{this} not to be ' + article + type + ); + } else { + this.assert( + type === detectedType + , 'expected #{this} to be ' + article + type + , 'expected #{this} not to be ' + article + type + ); + } } Assertion.addChainableMethod('an', an); @@ -672,6 +688,43 @@ Assertion.addProperty('true', function () { ); }); +/** + * ### .callable + * + * Asserts that the target a callable function. + * + * expect(console.log).to.be.callable; + * + * A custom error message can be given as the second argument to `expect`. + * + * expect('not a function', 'nooo why fail??').to.be.callable; + * + * @name callable + * @namespace BDD + * @api public + */ +Assertion.addProperty('callable', function () { + const val = flag(this, 'object') + const ssfi = flag(this, 'ssfi') + const message = flag(this, 'message') + const msg = message ? `${message}: ` : '' + const negate = flag(this, 'negate'); + + const assertionMessage = negate ? + `${msg}expected ${_.inspect(val)} not to be a callable function` : + `${msg}expected ${_.inspect(val)} to be a callable function`; + + const isCallable = ['Function', 'AsyncFunction', 'GeneratorFunction', 'AsyncGeneratorFunction'].includes(_.type(val)); + + if ((isCallable && negate) || (!isCallable && !negate)) { + throw new AssertionError( + assertionMessage, + undefined, + ssfi + ); + } +}); + /** * ### .false * diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js index 77af8ead..24ceff36 100644 --- a/lib/chai/interface/assert.js +++ b/lib/chai/interface/assert.js @@ -8,6 +8,7 @@ import * as chai from '../../../index.js'; import {Assertion} from '../assertion.js'; import {flag, inspect} from '../utils/index.js'; import {AssertionError} from 'assertion-error'; +import {type} from '../utils/type-detect.js'; /** * ### assert(expression, message) @@ -553,41 +554,39 @@ assert.isDefined = function (val, msg) { }; /** - * ### .isFunction(value, [message]) + * ### .isCallable(value, [message]) * - * Asserts that `value` is a function. + * Asserts that `value` is a callable function. * * function serveTea() { return 'cup of tea'; }; - * assert.isFunction(serveTea, 'great, we can have tea now'); + * assert.isCallable(serveTea, 'great, we can have tea now'); * - * @name isFunction + * @name isCallable * @param {Mixed} value * @param {String} message * @namespace Assert * @api public */ - -assert.isFunction = function (val, msg) { - new Assertion(val, msg, assert.isFunction, true).to.be.a('function'); -}; +assert.isCallable = function (val, msg) { + new Assertion(val, msg, assert.isCallable, true).is.callable; +} /** - * ### .isNotFunction(value, [message]) + * ### .isNotCallable(value, [message]) * - * Asserts that `value` is _not_ a function. + * Asserts that `value` is _not_ a callable function. * * var serveTea = [ 'heat', 'pour', 'sip' ]; - * assert.isNotFunction(serveTea, 'great, we have listed the steps'); + * assert.isNotCallable(serveTea, 'great, we have listed the steps'); * - * @name isNotFunction + * @name isNotCallable * @param {Mixed} value * @param {String} message * @namespace Assert * @api public */ - -assert.isNotFunction = function (val, msg) { - new Assertion(val, msg, assert.isNotFunction, true).to.not.be.a('function'); +assert.isNotCallable = function (val, msg) { + new Assertion(val, msg, assert.isNotCallable, true).is.not.callable; }; /** @@ -3104,4 +3103,6 @@ assert.isNotEmpty = function(val, msg) { ('isFrozen', 'frozen') ('isNotFrozen', 'notFrozen') ('isEmpty', 'empty') -('isNotEmpty', 'notEmpty'); +('isNotEmpty', 'notEmpty') +('isCallable', 'isFunction') +('isNotCallable', 'isNotFunction') diff --git a/test/assert.js b/test/assert.js index 8462fd5e..22373a35 100644 --- a/test/assert.js +++ b/test/assert.js @@ -139,6 +139,20 @@ describe('assert', function () { assert.typeOf('test', 'string'); assert.typeOf(true, 'boolean'); assert.typeOf(5, 'number'); + + assert.typeOf(() => {}, 'function'); + assert.typeOf(function() {}, 'function'); + assert.typeOf(async function() {}, 'asyncfunction'); + assert.typeOf(function*() {}, 'generatorfunction'); + assert.typeOf(async function*() {}, 'asyncgeneratorfunction'); + + err(function () { + assert.typeOf(5, 'function', 'blah'); + }, "blah: expected 5 to be a function"); + + err(function () { + assert.typeOf(function() {}, 'asyncfunction', 'blah'); + }, "blah: expected [Function] to be an asyncfunction"); if (typeof Symbol === 'function') { assert.typeOf(Symbol(), 'symbol'); @@ -151,10 +165,20 @@ describe('assert', function () { it('notTypeOf', function () { assert.notTypeOf('test', 'number'); + + assert.notTypeOf(() => {}, 'string'); + assert.notTypeOf(function() {}, 'string'); + assert.notTypeOf(async function() {}, 'string'); + assert.notTypeOf(function*() {}, 'string'); + assert.notTypeOf(async function*() {}, 'string'); err(function () { assert.notTypeOf(5, 'number', 'blah'); }, "blah: expected 5 not to be a number"); + + err(function () { + assert.notTypeOf(() => {}, 'function', 'blah'); + }, "blah: expected [Function] not to be a function"); }); it('instanceOf', function() { @@ -521,13 +545,41 @@ describe('assert', function () { }, "blah: expected undefined to not equal undefined"); }); + it('isCallable', function() { + var func = function() {}; + assert.isCallable(func); + + var func = async function() {}; + assert.isCallable(func); + + var func = function* () {} + assert.isCallable(func); + + var func = async function* () {} + assert.isCallable(func); + + err(function () { + assert.isCallable({}, 'blah'); + }, "blah: expected {} to be a callable function"); + }); + + it('isNotCallable', function() { + assert.isNotCallable(false); + assert.isNotCallable(10); + assert.isNotCallable('string'); + + err(function () { + assert.isNotCallable(function() {}, 'blah'); + }, "blah: expected [Function] not to be a callable function"); + }); + it('isFunction', function() { var func = function() {}; assert.isFunction(func); err(function () { assert.isFunction({}, 'blah'); - }, "blah: expected {} to be a function"); + }, "blah: expected {} to be a callable function"); }); it('isNotFunction', function () { @@ -535,7 +587,7 @@ describe('assert', function () { err(function () { assert.isNotFunction(function () {}, 'blah'); - }, "blah: expected [Function] not to be a function"); + }, "blah: expected [Function] not to be a callable function"); }); it('isArray', function() { diff --git a/test/expect.js b/test/expect.js index 2dd08ef2..d5247655 100644 --- a/test/expect.js +++ b/test/expect.js @@ -385,6 +385,90 @@ describe('expect', function () { }, "blah: expected 5 not to be a number"); }); + it('callable', function() { + expect(function() {}).to.be.callable; + expect(async function() {}).to.be.callable; + expect(function*() {}).to.be.callable; + expect(async function*() {}).to.be.callable; + + expect('foobar').to.not.be.callable; + + err(function(){ + expect('foobar', 'blah').to.be.callable; + }, "blah: expected 'foobar' to be a callable function"); + + err(function(){ + expect(function() {}, 'blah').to.not.be.callable; + }, "blah: expected [Function] not to be a callable function"); + }); + + it('function', function() { + expect(function() {}).to.be.a('function'); + expect(async function() {}).to.be.a('function'); + expect(function*() {}).to.be.a('function'); + expect(async function*() {}).to.be.a('function'); + + expect('foobar').to.not.be.a('function'); + + err(function(){ + expect('foobar').to.be.a('function', 'blah'); + }, "blah: expected 'foobar' to be a function"); + + err(function(){ + expect(function() {}).to.not.be.a('function', 'blah'); + }, "blah: expected [Function] not to be a function"); + + err(function(){ + expect(function() {}, 'blah').to.not.be.a('function'); + }, "blah: expected [Function] not to be a function"); + }) + + it('asyncFunction', function() { + expect(async function() {}).to.be.a('AsyncFunction'); + expect(async function*() {}).to.be.a('AsyncFunction'); + + err(function(){ + expect('foobar').to.be.a('asyncfunction', 'blah'); + }, "blah: expected 'foobar' to be an asyncfunction"); + + err(function(){ + expect(async function() {}).to.not.be.a('asyncfunction', 'blah'); + }, "blah: expected [AsyncFunction] not to be an asyncfunction"); + + err(function(){ + expect(async function() {}, 'blah').to.not.be.a('asyncfunction'); + }, "blah: expected [AsyncFunction] not to be an asyncfunction"); + }) + + it('generatorFunction', function() { + expect(function*() {}).to.be.a('generatorFunction'); + expect(async function*() {}).to.be.a('generatorFunction'); + + err(function(){ + expect('foobar').to.be.a('generatorfunction', 'blah'); + }, "blah: expected 'foobar' to be a generatorfunction"); + + err(function(){ + expect(function*() {}).to.not.be.a('generatorfunction', 'blah'); + }, "blah: expected [GeneratorFunction] not to be a generatorfunction"); + + err(function(){ + expect(function*() {}, 'blah').to.not.be.a('generatorfunction'); + }, "blah: expected [GeneratorFunction] not to be a generatorfunction"); + }) + + it('asyncGeneratorFunction', function() { + expect(async function*() {}).to.be.a('asyncGeneratorFunction'); + + err(function(){ + expect(async function() {}, 'blah').to.be.a('asyncgeneratorfunction'); + }, "blah: expected [AsyncFunction] to be an asyncgeneratorfunction"); + + err(function(){ + expect(async function*() {}, 'blah').to.not.be.a('asyncgeneratorfunction'); + }, "blah: expected [AsyncGeneratorFunction] not to be an asyncgeneratorfunction"); + }) + it('instanceof', function(){ function Foo(){} expect(new Foo()).to.be.an.instanceof(Foo); diff --git a/test/should.js b/test/should.js index 0da30610..adb94d94 100644 --- a/test/should.js +++ b/test/should.js @@ -337,6 +337,18 @@ describe('should', function() { }, "expected '' to be false") }); + it("callable", function () { + (function () {}).should.be.callable; + (async function () {}).should.be.callable; + (function* () {}).should.be.callable; + (async function* () {}).should.be.callable; + true.should.not.be.callable; + + err(function () { + "".should.be.callable; + }, "expected '' to be a callable function"); + }); + it('null', function(){ (0).should.not.be.null;