diff --git a/documentation/assertions/object/to-have-properties.md b/documentation/assertions/object/to-have-properties.md index aa8c04301..5c89ccb03 100644 --- a/documentation/assertions/object/to-have-properties.md +++ b/documentation/assertions/object/to-have-properties.md @@ -12,6 +12,22 @@ expect(['a', { c: 'c' }, 'd'], 'to have properties', { }); ``` +When validating the object against an array of properties, the `only` flag can +be used to assert that only the specified properties are present: + +```js +expect({ foo: 123, bar: 456 }, 'to only have properties', ['foo']); +``` + +```output +expected { foo: 123, bar: 456 } to only have properties [ 'foo' ] + +{ + foo: 123, + bar: 456 // should be removed +} +``` + Using the `own` flag, you can assert presence of an own properties. ```js diff --git a/lib/assertions.js b/lib/assertions.js index c4bc2099f..4f4b973f9 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -319,7 +319,7 @@ module.exports = expect => { ); expect.addAssertion( - ' [not] to have [own] properties ', + ' [not] to [only] have [own] properties ', (expect, subject, propertyNames) => { const unsupportedPropertyNames = []; propertyNames.forEach(propertyName => { @@ -344,9 +344,109 @@ module.exports = expect => { this.outdentLines(); }); } - propertyNames.forEach(propertyName => { - expect(subject, '[not] to have [own] property', String(propertyName)); - }); + + if (expect.flags.only) { + if (expect.flags.not) { + expect.errorMode = 'bubble'; + expect.fail( + 'The "not" flag cannot be used together with "to only have properties".' + ); + } + if (expect.flags.own) { + expect.errorMode = 'bubble'; + expect.fail( + 'The "own" flag cannot be used together with "to only have properties".' + ); + } + const subjectType = expect.subjectType; + const subjectKeys = subjectType.getKeys(subject).filter( + key => + // include only those keys whose value is not undefined + typeof subjectType.valueForKey(subject, key) !== 'undefined' + ); + + expect.withError( + () => { + expect(subjectKeys.length === propertyNames.length, 'to be true'); + // now catch differing property names + const keyInValue = {}; + propertyNames.forEach(key => { + keyInValue[key] = true; + }); + subjectKeys.forEach(key => + expect( + Object.prototype.hasOwnProperty.call(keyInValue, key), + 'to be true' + ) + ); + }, + () => { + expect.fail({ + diff: (output, diff, inspect, equal) => { + output.inline = true; + + const keyInValue = {}; + propertyNames.forEach(key => { + keyInValue[key] = true; + }); + + subjectType.prefix(output, subject); + output.nl().indentLines(); + + subjectKeys.forEach((key, index) => { + const propertyOutput = subjectType.property( + output.clone(), + key, + inspect(subjectType.valueForKey(subject, key)) + ); + const delimiterOutput = subjectType.delimiter( + output.clone(), + index, + subjectKeys.length + ); + + output + .i() + .block(function() { + this.append(propertyOutput).amend(delimiterOutput); + if ( + !Object.prototype.hasOwnProperty.call(keyInValue, key) + ) { + this.sp().annotationBlock(function() { + this.error('should be removed'); + }); + } else { + delete keyInValue[key]; + } + }) + .nl(); + }); + + // list any remaining value properties as missing + Object.keys(keyInValue).forEach(valueKey => { + output + .i() + .annotationBlock(function() { + this.error('missing') + .sp() + .append(inspect(valueKey)); + }) + .nl(); + }); + + output.outdentLines(); + subjectType.suffix(output, subject); + + return output; + } + }); + } + ); + } else { + propertyNames.forEach(propertyName => { + expect(subject, '[not] to have [own] property', String(propertyName)); + }); + } } ); diff --git a/test/assertions/to-have-properties.spec.js b/test/assertions/to-have-properties.spec.js index 23fd89e7c..2ede68719 100644 --- a/test/assertions/to-have-properties.spec.js +++ b/test/assertions/to-have-properties.spec.js @@ -195,7 +195,7 @@ describe('to have properties assertion', () => { ' The assertion does not have a matching signature for:\n' + ' to have properties \n' + ' did you mean:\n' + - ' [not] to have [own] properties \n' + + ' [not] to [only] have [own] properties \n' + ' to have [own] properties ' ); @@ -211,7 +211,7 @@ describe('to have properties assertion', () => { ' The assertion does not have a matching signature for:\n' + ' not to have properties \n' + ' did you mean:\n' + - ' [not] to have [own] properties ' + ' [not] to [only] have [own] properties ' ); }); @@ -289,4 +289,107 @@ describe('to have properties assertion', () => { ); }); }); + + describe('with the "only" flag', () => { + it('should pass', () => { + expect(function() { + expect({ foo: 123, bar: 456 }, 'to only have properties', [ + 'foo', + 'bar' + ]); + }, 'not to throw'); + }); + + it('should pass regardless of ordering', () => { + expect(function() { + expect({ foo: 123, bar: 456, baz: 768 }, 'to only have properties', [ + 'baz', + 'foo', + 'bar' + ]); + }, 'not to throw'); + }); + + it('should fail with a diff and mark properties to be removed', () => { + expect( + function() { + expect({ foo: 123, bar: 456 }, 'to only have properties', ['foo']); + }, + 'to error', + "expected { foo: 123, bar: 456 } to only have properties [ 'foo' ]\n" + + '\n' + + '{\n' + + ' foo: 123,\n' + + ' bar: 456 // should be removed\n' + + '}' + ); + }); + + it('should fail with a diff and mark properties that are missing', () => { + expect( + function() { + expect({ foo: 123, bar: 456 }, 'to only have properties', [ + 'foo', + 'baz' + ]); + }, + 'to error', + "expected { foo: 123, bar: 456 } to only have properties [ 'foo', 'baz' ]\n" + + '\n' + + '{\n' + + ' foo: 123,\n' + + ' bar: 456 // should be removed\n' + + " // missing 'baz'\n" + + '}' + ); + }); + + it('should fail with a diff while avoiding the prototype chain', () => { + expect( + function() { + expect({ toString: 'foobar' }, 'to only have properties', []); + }, + 'to error', + "expected { toString: 'foobar' } to only have properties []\n" + + '\n' + + '{\n' + + " toString: 'foobar' // should be removed\n" + + '}' + ); + }); + + it('should ignore undefined properties (pass)', () => { + expect(function() { + expect({ foo: 123, bar: undefined }, 'to only have properties', [ + 'foo' + ]); + }, 'not to throw'); + }); + + it('should ignore undefined properties (fail)', () => { + expect(function() { + expect({ 123: undefined }, 'to only have properties', ['123']); + }, 'to throw'); + }); + + it('should error if used with the not flag', () => { + expect( + function() { + expect({ foo: 123 }, 'not to only have properties', ['foo']); + }, + 'to throw', + 'The "not" flag cannot be used together with "to only have properties".' + ); + }); + + it('should error if used with the own flag', () => { + expect( + function() { + expect({ foo: 123 }, 'to only have own properties', ['foo']); + }, + 'to throw', + 'The "own" flag cannot be used together with "to only have properties".' + ); + }); + }); });