diff --git a/documentation/assertions/object/to-have-properties-satisfying.md b/documentation/assertions/object/to-have-properties-satisfying.md new file mode 100644 index 000000000..e4107047d --- /dev/null +++ b/documentation/assertions/object/to-have-properties-satisfying.md @@ -0,0 +1,42 @@ +Asserts that all properties with defined values satisfy a given assertion. + +```js +expect( + { foo: 0, bar: 1, baz: 2, qux: 3 }, + 'to have properties satisfying', + expect.it(function(key) { + expect(key, 'to match', /^[a-z]{3}$/); + }) +); + +expect( + { foo: 0, bar: 1, baz: 2, qux: 3 }, + 'to have properties satisfying', + 'to match', + /^[a-z]{3}$/ +); +``` + +In case of a failing expectation you get the following output: + +```js +expect( + { foo: 0, bar: 1, baz: 2, qux: 3, quux: 4 }, + 'to have properties satisfying', + 'to match', + /^[a-z]{3}$/ +); +``` + +```output +expected { foo: 0, bar: 1, baz: 2, qux: 3, quux: 4 } +to have properties satisfying to match /^[a-z]{3}$/ + +[ + 'foo', + 'bar', + 'baz', + 'qux', + 'quux' // should match /^[a-z]{3}$/ +] +``` diff --git a/lib/assertions.js b/lib/assertions.js index 1234351bb..76727613f 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -300,10 +300,20 @@ module.exports = expect => { } ); + const toHavePropertyValueDeprecation = utils.createDeprecationWarning( + "unexpected: The value argument of 'to have property' assertion is deprecated.\n" + + "Please use 'to have properties' with object argument instead:\n" + + 'http://unexpected.js.org/assertions/object/to-have-properties/' + ); + expect.addAssertion( ' to have [own] property ', - (expect, subject, key, expectedPropertyValue) => - expect(subject, 'to have [own] property', key).then( + (expect, subject, key, expectedPropertyValue) => { + if (expectedPropertyValue) { + toHavePropertyValueDeprecation(); + } + + return expect(subject, 'to have [own] property', key).then( actualPropertyValue => { expect.argsOutput = function() { this.appendInspected(key) @@ -315,7 +325,8 @@ module.exports = expect => { expect(actualPropertyValue, 'to equal', expectedPropertyValue); return actualPropertyValue; } - ) + ); + } ); expect.addAssertion( @@ -512,6 +523,26 @@ module.exports = expect => { } ); + expect.addAssertion( + [ + ' to have properties satisfying ', + ' to have properties satisfying ' + ], + (expect, subject, ...rest) => { + expect.errorMode = 'nested'; + expect(subject, 'not to be empty'); + expect.errorMode = 'default'; + + const subjectType = expect.subjectType; + const keys = subjectType.getKeys(subject).filter( + key => + // include only those keys whose value is not undefined + typeof subjectType.valueForKey(subject, key) !== 'undefined' + ); + return expect(keys, 'to have items satisfying', ...rest); + } + ); + expect.addAssertion( ' [not] to have length ', (expect, subject, length) => { diff --git a/lib/utils.js b/lib/utils.js index cf4ec5760..efe5aea7a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,6 +4,7 @@ const canSetPrototype = Object.setPrototypeOf || { __proto__: [] } instanceof Array; const greedyIntervalPacker = require('greedy-interval-packer'); +const magicpen = require('magicpen'); const setPrototypeOf = Object.setPrototypeOf || @@ -375,5 +376,28 @@ const utils = (module.exports = { } else if (typeof process === 'object' && process.env) { return process.env[varName]; } + }, + + createDeprecationWarning(message) { + let deprecationWarningDisplayed = false; + + return () => { + if (deprecationWarningDisplayed) { + return; + } + + deprecationWarningDisplayed = true; + + let format = magicpen.defaultFormat; + if (format === 'html') { + // override given this will be output in the browser console + format = 'text'; + } + console.warn( + magicpen() + .text(message, 'bgYellow', 'black') + .toString(format) + ); + }; } }); diff --git a/test/assertions/to-have-properties-satisfying.spec.js b/test/assertions/to-have-properties-satisfying.spec.js new file mode 100644 index 000000000..bcdeccc04 --- /dev/null +++ b/test/assertions/to-have-properties-satisfying.spec.js @@ -0,0 +1,258 @@ +/* global expect */ +describe('to have properties satisfying assertion', () => { + it('requires a third argument', () => { + expect( + function() { + expect([1, 2, 3], 'to have properties satisfying'); + }, + 'to throw', + 'expected [ 1, 2, 3 ] to have properties satisfying\n' + + ' The assertion does not have a matching signature for:\n' + + ' to have properties satisfying\n' + + ' did you mean:\n' + + ' to have properties satisfying \n' + + ' to have properties satisfying ' + ); + }); + + it('does not accept a fourth argument', () => { + expect( + function() { + expect([1], 'to have properties satisfying', 0, 1); + }, + 'to throw', + 'expected [ 1 ] to have properties satisfying 0, 1\n' + + ' The assertion does not have a matching signature for:\n' + + ' to have properties satisfying \n' + + ' did you mean:\n' + + ' to have properties satisfying \n' + + ' to have properties satisfying ' + ); + }); + + it('only accepts objects as the target', () => { + expect( + function() { + expect(42, 'to have properties satisfying', true); + }, + 'to throw', + 'expected 42 to have properties satisfying true\n' + + ' The assertion does not have a matching signature for:\n' + + ' to have properties satisfying \n' + + ' did you mean:\n' + + ' to have properties satisfying \n' + + ' to have properties satisfying ' + ); + }); + + it('allows assertions as the argument', () => { + expect( + { foo: 0, bar: 1, baz: 2, qux: 3 }, + 'to have properties satisfying', + 'not to be empty' + ); + + expect( + { foo: 0, bar: 1, baz: 2, qux: 3 }, + 'to have properties satisfying', + 'not to be empty' + ); + + expect( + { foo: 0, bar: 1, baz: 2, qux: 3 }, + 'to have properties satisfying', + 'to match', + /^[a-z]{3}$/ + ); + }); + + it('allows expect.it as an argument (the wrapped function receives the property)', () => { + expect( + { ff: 0, bbbb: 1, cc: 2 }, + 'to have properties satisfying', + expect.it(function(property) { + // properties must be of even length + expect(property.length % 2 === 0, 'to be true'); + }) + ); + }); + + it('ignores properties with undefined values', () => { + expect( + { foo: undefined, bar: '123' }, + 'to have properties satisfying', + 'to be a string' + ); + }); + + it('uses value semantics for functions', () => { + expect( + () => { + expect( + { + foo: true + }, + 'to have properties satisfying', + function(property) { + throw new Error('should not be called'); + } + ); + }, + 'to throw', + 'expected { foo: true } to have properties satisfying\n' + + 'function (property) {\n' + + " throw new Error('should not be called');\n" + + '}\n' + + '\n' + + '[\n' + + " 'foo' // should equal function (property) {\n" + + " // throw new Error('should not be called');\n" + + ' // }\n' + + ']' + ); + }); + + it('fails for an empty array', () => { + expect( + function() { + expect([], 'to have properties satisfying', 123); + }, + 'to throw', + 'expected [] to have properties satisfying 123\n' + + ' expected [] not to be empty' + ); + }); + + it('should work with non-enumerable keys returned by the getKeys function of the subject type', () => { + expect( + function() { + expect(new Error('foo'), 'to have properties satisfying', /bar/); + }, + 'to throw', + "expected Error('foo') to have properties satisfying /bar/\n" + + '\n' + + '[\n' + + " 'message' // should match /bar/\n" + + ']' + ); + }); + + it('fails when the assertion argument fails', () => { + expect( + function() { + expect( + { foo: 0, bar: 1, Baz: 2, qux: 3 }, + 'to have properties satisfying', + 'to match', + /^[a-z]{3}$/ + ); + }, + 'to throw', + /'Baz', \/\/ should match/ + ); + }); + + it('fails when the expect.it argument fails', () => { + expect( + () => { + expect( + { ff: 0, bbb: 1, cc: 2 }, + 'to have properties satisfying', + expect.it(function(property) { + // properties must be of even length + expect(property.length % 2 === 0, 'to be true'); + }) + ); + }, + 'to throw', + 'expected { ff: 0, bbb: 1, cc: 2 } to have properties satisfying\n' + + 'expect.it(function (property) {\n' + + ' // properties must be of even length\n' + + " expect(property.length % 2 === 0, 'to be true');\n" + + '})\n' + + '\n' + + '[\n' + + " 'ff',\n" + + " 'bbb', // expected false to be true\n" + + " 'cc'\n" + + ']' + ); + }); + + it('provides a detailed report of where failures occur', () => { + expect( + function() { + expect( + { foo: 0, bar: 1, baz: 2, qux: 3, quux: 4 }, + 'to have properties satisfying', + expect.it(function(key) { + expect(key, 'to have length', 3); + }) + ); + }, + 'to throw', + 'expected { foo: 0, bar: 1, baz: 2, qux: 3, quux: 4 } to have properties satisfying\n' + + 'expect.it(function (key) {\n' + + " expect(key, 'to have length', 3);\n" + + '})\n' + + '\n' + + '[\n' + + " 'foo',\n" + + " 'bar',\n" + + " 'baz',\n" + + " 'qux',\n" + + " 'quux' // should have length 3\n" + + ' // expected 4 to be 3\n' + + ']' + ); + }); + + describe('delegating to an async assertion', () => { + var clonedExpect = expect + .clone() + .addAssertion( + ' to be a sequence of as after a short delay', + function(expect, subject) { + expect.errorMode = 'nested'; + + return expect.promise(function(run) { + setTimeout( + run(function() { + expect(subject, 'to match', /^a+$/); + }), + 1 + ); + }); + } + ); + + it('should succeed', () => { + return clonedExpect( + { a: 1, aa: 2 }, + 'to have properties satisfying', + 'to be a sequence of as after a short delay' + ); + }); + + it('should fail with a diff', () => { + return expect( + clonedExpect( + { a: 1, foo: 2, bar: 3 }, + 'to have properties satisfying', + 'to be a sequence of as after a short delay' + ), + 'to be rejected with', + 'expected { a: 1, foo: 2, bar: 3 }\n' + + 'to have properties satisfying to be a sequence of as after a short delay\n' + + '\n' + + '[\n' + + " 'a',\n" + + " 'foo', // should be a sequence of as after a short delay\n" + + ' // should match /^a+$/\n' + + " 'bar' // should be a sequence of as after a short delay\n" + + ' // should match /^a+$/\n' + + ']' + ); + }); + }); +});