From 8e2ae9d4702ed29740c682bd02924bbc99170495 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 18 Feb 2018 14:46:35 +0100 Subject: [PATCH 01/17] Add required object type hooks to support Map plugin. Provide support for overriding the how properties are drawn in the object methods - use the hook and override it to use "[...]" when outputting elements of the Map. * allow changing the method used to calculate the uniqueKeys * allow specifying the method used to retrieve the valueForKey * attach the keyComparator to the object type to allow reuse --- lib/types.js | 108 ++++++++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 48 deletions(-) diff --git a/lib/types.js b/lib/types.js index 298d959f9..cf547af17 100644 --- a/lib/types.js +++ b/lib/types.js @@ -71,38 +71,6 @@ module.exports = function(expect) { }); } - // If Symbol support is not detected, default to passing undefined to - // Array.prototype.sort, which means "natural" (asciibetical) sort. - let keyComparator; - if (typeof Symbol === 'function') { - // Comparator that puts symbols last: - keyComparator = (a, b) => { - let aIsSymbol, bIsSymbol; - let aString = a; - let bString = b; - aIsSymbol = typeof a === 'symbol'; - bIsSymbol = typeof b === 'symbol'; - if (aIsSymbol) { - if (bIsSymbol) { - aString = a.toString(); - bString = b.toString(); - } else { - return 1; - } - } else if (bIsSymbol) { - return -1; - } - - if (aString < bString) { - return -1; - } else if (aString > bString) { - return 1; - } - - return 0; - }; - } - expect.addType({ name: 'object', indent: true, @@ -110,6 +78,7 @@ module.exports = function(expect) { identify(obj) { return obj && typeof obj === 'object'; }, + propertyStyle: 'property', prefix(output, obj) { const constructor = obj.constructor; const constructorName = @@ -152,6 +121,36 @@ module.exports = function(expect) { } } : Object.keys, + // If Symbol support is not detected default to undefined which, when + // passed to Array.prototype.sort, means "natural" (asciibetical) sort. + keyComparator: + typeof Symbol === 'function' + ? (a, b) => { + let aIsSymbol, bIsSymbol; + let aString = a; + let bString = b; + aIsSymbol = typeof a === 'symbol'; + bIsSymbol = typeof b === 'symbol'; + if (aIsSymbol) { + if (bIsSymbol) { + aString = a.toString(); + bString = b.toString(); + } else { + return 1; + } + } else if (bIsSymbol) { + return -1; + } + + if (aString < bString) { + return -1; + } else if (aString > bString) { + return 1; + } + + return 0; + } + : null, equal(a, b, equal) { if (a === b) { return true; @@ -173,8 +172,8 @@ module.exports = function(expect) { return false; } //the same set of keys (although not necessarily the same order), - actualKeys.sort(keyComparator); - expectedKeys.sort(keyComparator); + actualKeys.sort(this.keyComparator); + expectedKeys.sort(this.keyComparator); // cheap key test for (let i = 0; i < actualKeys.length; i += 1) { if (actualKeys[i] !== expectedKeys[i]) { @@ -211,13 +210,20 @@ module.exports = function(expect) { propertyOutput.text('set').sp(); } // Inspect the setter function if there's no getter: - const value = hasSetter && !hasGetter ? hasSetter : obj[key]; + let value; + if (hasSetter && !hasGetter) { + value = hasSetter; + } else if (typeof type.valueForKey === 'function') { + value = type.valueForKey(obj, key); + } else { + value = obj[key]; + } let inspectedValue = inspect(value); if (value && value._expectIt) { inspectedValue = output.clone().block(inspectedValue); } - propertyOutput.property(key, inspectedValue); + propertyOutput[type.propertyStyle](key, inspectedValue); propertyOutput.amend( type.delimiter(output.clone(), index, keys.length) @@ -318,10 +324,8 @@ module.exports = function(expect) { output.inline = true; const actualKeys = this.getKeys(actual); - const keys = utils.uniqueStringsAndSymbols( - actualKeys, - this.getKeys(expected) - ); + const expectedKeys = this.getKeys(expected); + const keys = this.uniqueKeys(actualKeys, expectedKeys); const prefixOutput = this.prefix(output.clone(), actual); output.append(prefixOutput).nl(prefixOutput.isEmpty() ? 0 : 1); @@ -329,14 +333,21 @@ module.exports = function(expect) { output.indentLines(); } const type = this; + const hasValueForKey = typeof type.valueForKey === 'function'; keys.forEach((key, index) => { output .nl(index > 0 ? 1 : 0) .i() .block(function() { + var valueActual = hasValueForKey + ? type.valueForKey(actual, key) + : actual[key]; + var valueExpected = hasValueForKey + ? type.valueForKey(expected, key) + : expected[key]; let valueOutput; const annotation = output.clone(); - const conflicting = !equal(actual[key], expected[key]); + const conflicting = !equal(valueActual, valueExpected); let isInlineDiff = false; if (conflicting) { if (!(key in expected)) { @@ -344,12 +355,12 @@ module.exports = function(expect) { isInlineDiff = true; } else if (!(key in actual)) { this.error('// missing').sp(); - valueOutput = output.clone().appendInspected(expected[key]); + valueOutput = output.clone().appendInspected(valueExpected); isInlineDiff = true; } else { - const keyDiff = diff(actual[key], expected[key]); + const keyDiff = diff(valueActual, valueExpected); if (!keyDiff || (keyDiff && !keyDiff.inline)) { - annotation.shouldEqualError(expected[key]); + annotation.shouldEqualError(valueExpected); if (keyDiff) { annotation.nl(2).append(keyDiff); } @@ -363,7 +374,7 @@ module.exports = function(expect) { } if (!valueOutput) { - valueOutput = inspect(actual[key], conflicting ? Infinity : null); + valueOutput = inspect(valueActual, conflicting ? Infinity : null); } valueOutput.amend( @@ -372,7 +383,7 @@ module.exports = function(expect) { if (!isInlineDiff) { valueOutput = output.clone().block(valueOutput); } - this.property(key, valueOutput); + this[type.propertyStyle](key, valueOutput); if (!annotation.isEmpty()) { this.sp().annotationBlock(annotation); } @@ -385,7 +396,6 @@ module.exports = function(expect) { const suffixOutput = this.suffix(output.clone(), actual); return output.nl(suffixOutput.isEmpty() ? 0 : 1).append(suffixOutput); }, - similar(a, b) { if (a === null || b === null) { return false; @@ -422,7 +432,9 @@ module.exports = function(expect) { } return numberOfSimilarKeys >= requiredSimilarKeys; }); - } + }, + uniqueKeys: utils.uniqueStringsAndSymbols, + valueForKey: null }); expect.addType({ From ae23451888b89e0ddc4d4a209883963ce0cf91db Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 18 Feb 2018 15:07:43 +0100 Subject: [PATCH 02/17] Add a property() method to types and use it to replace propertyStyle. --- lib/assertions.js | 67 ++++++++++++++++++++++------------ lib/styles.js | 6 ++- lib/types.js | 55 ++++++++++++++++------------ test/types/object-type.spec.js | 45 +++++++++++++++++++++++ 4 files changed, 126 insertions(+), 47 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index 21e50d79b..103626d15 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -458,7 +458,8 @@ module.exports = expect => { output .i() .block(function() { - this.property( + subjectType.property( + this, key, inspect(subject[key]), subjectIsArrayLike @@ -1619,11 +1620,13 @@ module.exports = expect => { } else { return output.clone().block(function() { if (type === 'moveSource') { - this.property( - diffItem.actualIndex, - inspect(diffItem.value), - true - ) + subjectType + .property( + this, + diffItem.actualIndex, + inspect(diffItem.value), + true + ) .amend(delimiterOutput.sp()) .error('// should be moved'); } else if (type === 'insert') { @@ -1635,30 +1638,43 @@ module.exports = expect => { if ( expect.findTypeOf(diffItem.value).is('function') ) { - this.error('missing: ').property( + subjectType.property( + this, index, - output.clone().block(function() { - this.omitSubject = undefined; - const promise = - keyPromises[diffItem.expectedIndex]; - if (promise.isRejected()) { - this.appendErrorMessage(promise.reason()); - } else { - this.appendInspected(diffItem.value); - } - }), + output + .clone() + .error('missing: ') + .block(function() { + this.omitSubject = undefined; + const promise = + keyPromises[diffItem.expectedIndex]; + if (promise.isRejected()) { + this.appendErrorMessage( + promise.reason() + ); + } else { + this.appendInspected(diffItem.value); + } + }), true ); } else { - this.error('missing ').property( - index, - inspect(diffItem.value), - true + var propertyOutput = output + .clone() + .error('missing '); + this.append( + subjectType.property( + propertyOutput, + index, + inspect(diffItem.value), + true + ) ); } }); } else { - this.property( + subjectType.property( + this, diffItem.actualIndex, output.clone().block(function() { if (type === 'remove') { @@ -1963,7 +1979,12 @@ module.exports = expect => { valueOutput = output.clone().block(valueOutput); } - this.property(key, valueOutput, subjectIsArrayLike); + subjectType.property( + this, + key, + valueOutput, + subjectIsArrayLike + ); }); }); diff --git a/lib/styles.js b/lib/styles.js index 8ba347f09..32efe916d 100644 --- a/lib/styles.js +++ b/lib/styles.js @@ -122,7 +122,11 @@ module.exports = expect => { .jsString("'"); }); - expect.addStyle('property', function(key, inspectedValue, isArrayLike) { + expect.addStyle('propertyForObject', function( + key, + inspectedValue, + isArrayLike + ) { let keyOmitted = false; let isSymbol; isSymbol = typeof key === 'symbol'; diff --git a/lib/types.js b/lib/types.js index cf547af17..0bfabbf6c 100644 --- a/lib/types.js +++ b/lib/types.js @@ -78,7 +78,6 @@ module.exports = function(expect) { identify(obj) { return obj && typeof obj === 'object'; }, - propertyStyle: 'property', prefix(output, obj) { const constructor = obj.constructor; const constructorName = @@ -91,6 +90,9 @@ module.exports = function(expect) { } return output.text('{'); }, + property(output, key, inspectedValue, isArrayLike) { + return output.propertyForObject(key, inspectedValue, isArrayLike); + }, suffix(output, obj) { output.text('}'); const constructor = obj.constructor; @@ -223,7 +225,7 @@ module.exports = function(expect) { if (value && value._expectIt) { inspectedValue = output.clone().block(inspectedValue); } - propertyOutput[type.propertyStyle](key, inspectedValue); + type.property(propertyOutput, key, inspectedValue); propertyOutput.amend( type.delimiter(output.clone(), index, keys.length) @@ -383,7 +385,7 @@ module.exports = function(expect) { if (!isInlineDiff) { valueOutput = output.clone().block(valueOutput); } - this[type.propertyStyle](key, valueOutput); + type.property(this, key, valueOutput); if (!annotation.isEmpty()) { this.sp().annotationBlock(annotation); } @@ -560,7 +562,7 @@ module.exports = function(expect) { // Not present non-numerical property returned by getKeys inspectedValue = inspect(undefined); } - return output.clone().property(key, inspectedValue, true); + return this.property(output.clone(), key, inspectedValue, true); }); const currentDepth = defaultDepth - Math.min(defaultDepth, depth); @@ -578,9 +580,8 @@ module.exports = function(expect) { width += size.width; return width > maxLineLength; }); - const type = this; inspectedItems.forEach((inspectedItem, index) => { - inspectedItem.amend(type.delimiter(output.clone(), index, keys.length)); + inspectedItem.amend(this.delimiter(output.clone(), index, keys.length)); }); if (multipleLines) { output.append(prefixOutput); @@ -674,11 +675,13 @@ module.exports = function(expect) { } else { return output.clone().block(function() { if (diffItem.type === 'moveSource') { - this.property( - diffItem.actualIndex, - inspect(diffItem.value), - true - ) + type + .property( + this, + diffItem.actualIndex, + inspect(diffItem.value), + true + ) .amend(delimiterOutput.sp()) .error('// should be moved'); } else if (diffItem.type === 'insert') { @@ -688,31 +691,37 @@ module.exports = function(expect) { typeof diffItem.actualIndex !== 'undefined' ? diffItem.actualIndex : diffItem.expectedIndex; - this.property(index, inspect(diffItem.value), true); + type.property(this, index, inspect(diffItem.value), true); }); }); } else if (diffItem.type === 'remove') { this.block(function() { - this.property( - diffItem.actualIndex, - inspect(diffItem.value), - true - ) + type + .property( + this, + diffItem.actualIndex, + inspect(diffItem.value), + true + ) .amend(delimiterOutput.sp()) .error('// should be removed'); }); } else if (diffItem.type === 'equal') { this.block(function() { - this.property( - diffItem.actualIndex, - inspect(diffItem.value), - true - ).amend(delimiterOutput); + type + .property( + this, + diffItem.actualIndex, + inspect(diffItem.value), + true + ) + .amend(delimiterOutput); }); } else { this.block(function() { const valueDiff = diff(diffItem.value, diffItem.expected); - this.property( + type.property( + this, diffItem.actualIndex, output.clone().block(function() { if (valueDiff && valueDiff.inline) { diff --git a/test/types/object-type.spec.js b/test/types/object-type.spec.js index 31a91af40..01ebc25c6 100644 --- a/test/types/object-type.spec.js +++ b/test/types/object-type.spec.js @@ -195,4 +195,49 @@ describe('object type', function() { ); }); }); + + describe('with a custom subtype that overrides property()', function() { + it('should bar', function() { + var clonedExpect = expect.clone(); + var customObject = { quux: 'xuuq', foobar: 'faz' }; + + clonedExpect.addStyle('xuuqProperty', function(key, inspectedValue) { + this.text('<') + .appendInspected(key) + .text('> --> ') + .append(inspectedValue); + }); + + clonedExpect.addType({ + name: 'xuuq', + base: 'object', + identify: function(obj) { + return obj && typeof 'object' && obj.quux === 'xuuq'; + }, + property: function(output, key, inspectedValue) { + return output.xuuqProperty(key, inspectedValue); + } + }); + + expect( + function() { + clonedExpect(customObject, 'to equal', { + quux: 'xuuq', + foobar: 'baz' + }); + }, + 'to throw', + "expected { <'quux'> --> 'xuuq', <'foobar'> --> 'faz' }\n" + + "to equal { <'quux'> --> 'xuuq', <'foobar'> --> 'baz' }\n" + + '\n' + + '{\n' + + " <'quux'> --> 'xuuq',\n" + + " <'foobar'> --> 'faz' // should equal 'baz'\n" + + ' //\n' + + ' // -faz\n' + + ' // +baz\n' + + '}' + ); + }); + }); }); From 6d4854a76a0d41956802d9fb080939dab1ae3401 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 25 Feb 2018 23:37:23 +0100 Subject: [PATCH 03/17] Add default valueForKey() impl and test it within the object type. --- lib/types.js | 21 +++++++---------- test/types/object-type.spec.js | 42 ++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/lib/types.js b/lib/types.js index 0bfabbf6c..067465682 100644 --- a/lib/types.js +++ b/lib/types.js @@ -187,7 +187,7 @@ module.exports = function(expect) { // possibly expensive deep test for (let j = 0; j < actualKeys.length; j += 1) { const key = actualKeys[j]; - if (!equal(a[key], b[key])) { + if (!equal(this.valueForKey(a, key), this.valueForKey(b, key))) { return false; } } @@ -215,10 +215,8 @@ module.exports = function(expect) { let value; if (hasSetter && !hasGetter) { value = hasSetter; - } else if (typeof type.valueForKey === 'function') { - value = type.valueForKey(obj, key); } else { - value = obj[key]; + value = type.valueForKey(obj, key); } let inspectedValue = inspect(value); @@ -335,22 +333,17 @@ module.exports = function(expect) { output.indentLines(); } const type = this; - const hasValueForKey = typeof type.valueForKey === 'function'; keys.forEach((key, index) => { output .nl(index > 0 ? 1 : 0) .i() .block(function() { - var valueActual = hasValueForKey - ? type.valueForKey(actual, key) - : actual[key]; - var valueExpected = hasValueForKey - ? type.valueForKey(expected, key) - : expected[key]; - let valueOutput; + const valueActual = type.valueForKey(actual, key); + const valueExpected = type.valueForKey(expected, key); const annotation = output.clone(); const conflicting = !equal(valueActual, valueExpected); let isInlineDiff = false; + let valueOutput; if (conflicting) { if (!(key in expected)) { annotation.error('should be removed'); @@ -436,7 +429,9 @@ module.exports = function(expect) { }); }, uniqueKeys: utils.uniqueStringsAndSymbols, - valueForKey: null + valueForKey(obj, key) { + return obj[key]; + } }); expect.addType({ diff --git a/test/types/object-type.spec.js b/test/types/object-type.spec.js index 01ebc25c6..a12d6cfd0 100644 --- a/test/types/object-type.spec.js +++ b/test/types/object-type.spec.js @@ -196,8 +196,8 @@ describe('object type', function() { }); }); - describe('with a custom subtype that overrides property()', function() { - it('should bar', function() { + describe('with a subtype that overrides property()', function() { + it('should render correctly in both inspection and diff', function() { var clonedExpect = expect.clone(); var customObject = { quux: 'xuuq', foobar: 'faz' }; @@ -240,4 +240,42 @@ describe('object type', function() { ); }); }); + + describe('with a subtype that overrides valueForKey()', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'nineObject', + base: 'object', + identify: function(obj) { + return obj && typeof 'object' && obj.nine === 9; + }, + valueForKey: function(obj, key) { + if (typeof obj[key] === 'string') { + return obj[key].toUpperCase(); + } + return obj[key]; + } + }); + + it('should process propeties in both inspection and diff', function() { + expect( + function() { + clonedExpect({ nine: 9, zero: 1, foo: 'bAr' }, 'to equal', { + nine: 9, + zero: 0, + foo: 'BaR' + }); + }, + 'to throw', + "expected { nine: 9, zero: 1, foo: 'BAR' } to equal { nine: 9, zero: 0, foo: 'BAR' }\n" + + '\n' + + '{\n' + + ' nine: 9,\n' + + ' zero: 1, // should equal 0\n' + + " foo: 'BAR'\n" + + '}' + ); + }); + }); }); From 9e9380f921023ce47deed9446b8f2d08882730fb Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 25 Feb 2018 23:42:28 +0100 Subject: [PATCH 04/17] Include additional tests for equal() and getKeys() type methods. --- test/types/object-type.spec.js | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/types/object-type.spec.js b/test/types/object-type.spec.js index a12d6cfd0..d19e95a2a 100644 --- a/test/types/object-type.spec.js +++ b/test/types/object-type.spec.js @@ -42,6 +42,46 @@ describe('object type', function() { }); }); + describe('#equal', function() { + it('should ignore undefined properties on the LHS', function() { + expect(function() { + expect({ lhs: undefined }, 'to equal', {}); + }, 'not to throw'); + }); + + it('should ignore undefined properties on the RHS', function() { + expect(function() { + expect({}, 'to equal', { rhs: undefined }); + }, 'not to throw'); + }); + }); + + describe('#getKeys', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'fooObject', + base: 'object', + identify: function(obj) { + return obj && typeof 'object' && obj.foo; + }, + getKeys: function(obj) { + return Object.keys(obj).filter(function(key) { + return key[0] !== '_'; + }); + } + }); + + it('should restrict the compared properties', function() { + expect(function() { + clonedExpect({ foo: true, _bar: true }, 'to equal', { + foo: true, + _bar: false + }); + }, 'not to throw'); + }); + }); + describe('with a subtype that disables indentation', function() { var clonedExpect = expect.clone(); From d1ea1e3ccbefb439b9e149c8c099a5b68159b452 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 25 Feb 2018 23:44:57 +0100 Subject: [PATCH 05/17] Check undefined values in equal() with valueForKey. --- lib/types.js | 4 ++-- test/types/object-type.spec.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/types.js b/lib/types.js index 067465682..42c4e4510 100644 --- a/lib/types.js +++ b/lib/types.js @@ -163,10 +163,10 @@ module.exports = function(expect) { } const actualKeys = this.getKeys(a).filter( - key => typeof a[key] !== 'undefined' + key => typeof this.valueForKey(a, key) !== 'undefined' ); const expectedKeys = this.getKeys(b).filter( - key => typeof b[key] !== 'undefined' + key => typeof this.valueForKey(b, key) !== 'undefined' ); // having the same number of owned properties (keys incorporates hasOwnProperty) diff --git a/test/types/object-type.spec.js b/test/types/object-type.spec.js index d19e95a2a..1cdabd4c9 100644 --- a/test/types/object-type.spec.js +++ b/test/types/object-type.spec.js @@ -54,6 +54,36 @@ describe('object type', function() { expect({}, 'to equal', { rhs: undefined }); }, 'not to throw'); }); + + describe('with a subtype that overrides valueForKey()', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'undefinerObject', + base: 'object', + identify: function(obj) { + return obj && typeof 'object' && obj.xuuq; + }, + valueForKey: function(obj, key) { + if (key !== 'xuuq') { + return undefined; + } + return obj[key]; + } + }); + + it('should ignore undefined properties on the LHS', function() { + expect(function() { + expect({ xuuq: true, lhs: undefined }, 'to equal', { xuuq: true }); + }, 'not to throw'); + }); + + it('should ignore undefined properties on the RHS', function() { + expect(function() { + expect({ xuuq: true }, 'to equal', { xuuq: true, rhs: undefined }); + }, 'not to throw'); + }); + }); }); describe('#getKeys', function() { From 224ad6bcd93298b816f9223c218f503f5fdfd221 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Mon, 26 Feb 2018 00:06:09 +0100 Subject: [PATCH 06/17] Add a hasKey() type method. --- lib/types.js | 5 ++- test/types/object-type.spec.js | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/types.js b/lib/types.js index 42c4e4510..7371de6d1 100644 --- a/lib/types.js +++ b/lib/types.js @@ -193,6 +193,9 @@ module.exports = function(expect) { } return true; }, + hasKey(obj, key) { + return key in obj; + }, inspect(obj, depth, output, inspect) { const keys = this.getKeys(obj); if (keys.length === 0) { @@ -422,7 +425,7 @@ module.exports = function(expect) { Math.max(aKeys.length, bKeys.length) / 2 ); return aKeys.concat(bKeys).some(key => { - if (key in a && key in b) { + if (this.hasKey(a, key) && this.hasKey(b, key)) { numberOfSimilarKeys += 1; } return numberOfSimilarKeys >= requiredSimilarKeys; diff --git a/test/types/object-type.spec.js b/test/types/object-type.spec.js index 1cdabd4c9..0ae7d529c 100644 --- a/test/types/object-type.spec.js +++ b/test/types/object-type.spec.js @@ -112,6 +112,62 @@ describe('object type', function() { }); }); + describe('#similar', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'ignoreUnderscoresObject', + base: 'object', + identify(obj) { + return obj && typeof 'object' && obj.xuuq; + }, + valueForKey(obj, key) { + if (key[0] === '_') { + return undefined; + } + return obj[key]; + } + }); + + it('should pass with values overriding valueForKey()', function() { + expect(function() { + clonedExpect( + [{ xuuq: true, quux: 'foo', _bob: true }, 'foobar'], + 'to equal', + [{ xuuq: true, quux: 'foo', _bob: false }, 'foobar'] + ); + }, 'not to throw'); + }); + + it('should fail with values overriding valueForKey()', function() { + expect( + function() { + clonedExpect( + [{ xuuq: true, quux: 'bar', _bob: true }, 'foobar'], + 'to equal', + ['foobar', { xuuq: true, quux: 'baz', _bob: false }] + ); + }, + 'to throw', + "expected [ { xuuq: true, quux: 'bar', _bob: undefined }, 'foobar' ]\n" + + "to equal [ 'foobar', { xuuq: true, quux: 'baz', _bob: undefined } ]\n" + + '\n' + + '[\n' + + '┌─▷\n' + + '│ {\n' + + '│ xuuq: true,\n' + + "│ quux: 'bar', // should equal 'baz'\n" + + '│ //\n' + + '│ // -bar\n' + + '│ // +baz\n' + + '│ _bob: undefined\n' + + '│ },\n' + + "└── 'foobar' // should be moved\n" + + ']' + ); + }); + }); + describe('with a subtype that disables indentation', function() { var clonedExpect = expect.clone(); From 14a5e79ee8d5811a58ce9b7e8aa64caa2ad812ec Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Mon, 26 Feb 2018 00:27:05 +0100 Subject: [PATCH 07/17] Adjust "to satisfy" assertion to make use of type hasKey and valueForKey. --- lib/assertions.js | 65 ++++++++++++++++++---------------- test/types/object-type.spec.js | 24 ++++++++++++- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index 103626d15..152df6b6f 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1788,18 +1788,16 @@ module.exports = expect => { keys.forEach((key, index) => { promiseByKey[key] = expect.promise(() => { - const valueKeyType = expect.findTypeOf(value[key]); + const subjectKey = subjectType.valueForKey(subject, key); + const valueKey = valueType.valueForKey(value, key); + const valueKeyType = expect.findTypeOf(valueKey); if (valueKeyType.is('expect.it')) { expect.context.thisObject = subject; - return value[key](subject[key], expect.context); + return valueKey(subjectKey, expect.context); } else if (valueKeyType.is('function')) { - return value[key](subject[key]); + return valueKey(subjectKey); } else { - return expect( - subject[key], - 'to [exhaustively] satisfy', - value[key] - ); + return expect(subjectKey, 'to [exhaustively] satisfy', valueKey); } }); }); @@ -1814,10 +1812,11 @@ module.exports = expect => { typeof subject[key] !== 'undefined' ); const valueKeysWithDefinedValues = keys.filter( - key => typeof value[key] !== 'undefined' + key => typeof valueType.valueForKey(value, key) !== 'undefined' ); const subjectKeysWithDefinedValues = subjectKeys.filter( - key => typeof subject[key] !== 'undefined' + key => + typeof subjectType.valueForKey(subject, key) !== 'undefined' ); expect( valueKeysWithDefinedValues.length - @@ -1835,16 +1834,16 @@ module.exports = expect => { diff(output, diff, inspect, equal) { output.inline = true; const subjectIsArrayLike = subjectType.is('array-like'); - const keys = utils - .uniqueStringsAndSymbols( - subjectKeys, - valueType.getKeys(value) - ) - .filter( - ( - key // Skip missing keys expected to be missing so they don't get rendered in the diff - ) => key in subject || typeof value[key] !== 'undefined' - ); + const valueKeys = valueType.getKeys(value); + const keys = subjectType + .uniqueKeys(subjectKeys, valueKeys) + .filter(key => { + // Skip missing keys expected to be missing so they don't get rendered in the diff + return ( + subjectType.hasKey(subject, key) || + typeof valueType.valueForKey(value, key) !== 'undefined' + ); + }); const prefixOutput = subjectType.prefix( output.clone(), subject @@ -1855,6 +1854,8 @@ module.exports = expect => { output.indentLines(); } keys.forEach((key, index) => { + const subjectKey = subjectType.valueForKey(subject, key); + const valueKey = valueType.valueForKey(value, key); output .nl(index > 0 ? 1 : 0) .i() @@ -1874,19 +1875,20 @@ module.exports = expect => { } const missingArrayIndex = - subjectType.is('array-like') && !(key in subject); + subjectType.is('array-like') && + !subjectType.hasKey(subject, key); let isInlineDiff = true; - output.omitSubject = subject[key]; - if (!(key in value)) { + output.omitSubject = subjectKey; + if (!valueType.hasKey(value, key)) { if (expect.flags.exhaustively) { annotation.error('should be removed'); } else { conflicting = null; } - } else if (!(key in subject)) { - if (expect.findTypeOf(value[key]).is('function')) { + } else if (!subjectType.hasKey(subject, key)) { + if (expect.findTypeOf(valueKey).is('function')) { if (promiseByKey[key].isRejected()) { output.error('// missing:').sp(); valueOutput = output @@ -1902,7 +1904,7 @@ module.exports = expect => { } } else { output.error('// missing').sp(); - valueOutput = inspect(value[key]); + valueOutput = inspect(valueKey); } } else if (conflicting || missingArrayIndex) { const keyDiff = @@ -1913,7 +1915,7 @@ module.exports = expect => { } if (keyDiff && keyDiff.inline) { valueOutput = keyDiff; - } else if (typeof value[key] === 'function') { + } else if (typeof valueKey === 'function') { isInlineDiff = false; annotation.appendErrorMessage(conflicting); } else if (!keyDiff || (keyDiff && !keyDiff.inline)) { @@ -1923,7 +1925,7 @@ module.exports = expect => { 'should satisfy' ) .sp() - .block(inspect(value[key])); + .block(inspect(valueKey)); if (keyDiff) { annotation.nl(2).append(keyDiff); @@ -1934,10 +1936,13 @@ module.exports = expect => { } if (!valueOutput) { - if (missingArrayIndex || !(key in subject)) { + if ( + missingArrayIndex || + !subjectType.hasKey(subject, key) + ) { valueOutput = output.clone(); } else { - valueOutput = inspect(subject[key]); + valueOutput = inspect(subjectKey); } } diff --git a/test/types/object-type.spec.js b/test/types/object-type.spec.js index 0ae7d529c..c438079bb 100644 --- a/test/types/object-type.spec.js +++ b/test/types/object-type.spec.js @@ -384,7 +384,7 @@ describe('object type', function() { } }); - it('should process propeties in both inspection and diff', function() { + it('should process propeties in both inspection and diff in "to equal"', function() { expect( function() { clonedExpect({ nine: 9, zero: 1, foo: 'bAr' }, 'to equal', { @@ -403,5 +403,27 @@ describe('object type', function() { '}' ); }); + + it('should process propeties in both inspection and diff in "to satsify"', function() { + expect( + function() { + clonedExpect( + { nine: 9, zero: 1, foo: 'bAr', baz: undefined }, + 'to satisfy', + { nine: 9, zero: 0, foo: 'BaR', baz: expect.it('to be undefined') } + ); + }, + 'to throw', + "expected { nine: 9, zero: 1, foo: 'BAR', baz: undefined }\n" + + "to satisfy { nine: 9, zero: 0, foo: 'BAR', baz: expect.it('to be undefined') }\n" + + '\n' + + '{\n' + + ' nine: 9,\n' + + ' zero: 1, // should equal 0\n' + + " foo: 'BAR',\n" + + ' baz: undefined\n' + + '}' + ); + }); }); }); From 85736e69fab9e2883bf04ff315ec1621f9034fa7 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Mon, 26 Feb 2018 00:34:51 +0100 Subject: [PATCH 08/17] Convert array-like inspect and diff to respect valueForKey(). --- lib/types.js | 49 +++++++++++++++++------------- lib/utils.js | 18 +++++++++++ test/types/array-like-type.spec.js | 32 +++++++++++++++++++ 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/lib/types.js b/lib/types.js index 7371de6d1..00f3a08bc 100644 --- a/lib/types.js +++ b/lib/types.js @@ -494,30 +494,36 @@ module.exports = function(expect) { // compare numerically indexed elements for (i = 0; i < a.length; i += 1) { - if (!equal(a[i], b[i])) { + if (!equal(this.valueForKey(a, i), this.valueForKey(b, i))) { return false; } } // compare non-numerical keys if enabled for the type if (!this.numericalPropertiesOnly) { - const aKeys = this.getKeysNonNumerical(a).filter( - ( - key // include keys whose value is not undefined - ) => typeof a[key] !== 'undefined' - ); - const bKeys = this.getKeysNonNumerical(b).filter( - ( - key // include keys whose value is not undefined on either LHS or RHS - ) => typeof b[key] !== 'undefined' || typeof a[key] !== 'undefined' - ); + const aKeys = this.getKeysNonNumerical(a).filter(key => { + // include keys whose value is not undefined + return typeof this.valueForKey(a, key) !== 'undefined'; + }); + const bKeys = this.getKeysNonNumerical(b).filter(key => { + // include keys whose value is not undefined on either LHS or RHS + return ( + typeof this.valueForKey(b, key) !== 'undefined' || + typeof this.valueForKey(a, key) !== 'undefined' + ); + }); if (aKeys.length !== bKeys.length) { return false; } for (i = 0; i < aKeys.length; i += 1) { - if (!equal(a[aKeys[i]], b[aKeys[i]])) { + if ( + !equal( + this.valueForKey(a, aKeys[i]), + this.valueForKey(b, bKeys[i]) + ) + ) { return false; } } @@ -551,8 +557,8 @@ module.exports = function(expect) { const inspectedItems = keys.map(key => { let inspectedValue; - if (key in arr) { - inspectedValue = inspect(arr[key]); + if (this.hasKey(arr, key)) { + inspectedValue = inspect(this.valueForKey(arr, key)); } else if (utils.numericalRegExp.test(key)) { // Sparse array entry inspectedValue = output.clone(); @@ -637,17 +643,18 @@ module.exports = function(expect) { output.indentLines(); } - const nonNumericalKeysAndSymbols = + var actualElements = utils.duplicateArrayLikeUsingType(actual, this); + var actualKeys = this.getKeys(actual); + var expectedElements = utils.duplicateArrayLikeUsingType(expected, this); + var expectedKeys = this.getKeys(expected); + var nonNumericalKeysAndSymbols = !this.numericalPropertiesOnly && - utils.uniqueNonNumericalStringsAndSymbols( - this.getKeys(actual), - this.getKeys(expected) - ); + utils.uniqueNonNumericalStringsAndSymbols(actualKeys, expectedKeys); const type = this; const changes = arrayChanges( - actual, - expected, + actualElements, + expectedElements, equal, (a, b) => type.similar(a, b), { diff --git a/lib/utils.js b/lib/utils.js index de8b85420..5975ffb78 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -27,6 +27,24 @@ const utils = (module.exports = { return a === b; }), + duplicateArrayLikeUsingType(obj, type) { + var arr = new Array(obj.length); + for (var i = 0; i < obj.length; i += 1) { + arr[i] = type.valueForKey(obj, i); + } + Object.keys(obj).forEach(function(key) { + if (!utils.numericalRegExp.test(key)) { + arr[key] = type.valueForKey(obj, key); + } + }); + if (Object.getOwnPropertySymbols) { + Object.getOwnPropertySymbols(obj).forEach(function(key) { + arr[key] = type.valueForKey(obj, key); + }); + } + return arr; + }, + isArray(ar) { return Object.prototype.toString.call(ar) === '[object Array]'; }, diff --git a/test/types/array-like-type.spec.js b/test/types/array-like-type.spec.js index 653657e85..498706ed3 100644 --- a/test/types/array-like-type.spec.js +++ b/test/types/array-like-type.spec.js @@ -374,6 +374,38 @@ describe('array-like type', function() { }); }); + describe('with a custom subtype that comes with its own valueForKeys', function() { + it('should process the elements in both inspection and diff in "to equal"', function() { + var clonedExpect = expect.clone().addType({ + name: 'firstElemUpper', + base: 'array-like', + identify: Array.isArray, + valueForKey: function(arr, key) { + var value = arr[key]; + if (key === 0) { + return value.toUpperCase(); + } + return value; + } + }); + expect( + function() { + clonedExpect(['foobar', 'barbar'], 'to equal', ['foobar', 'barbaz']); + }, + 'to throw', + "expected [ 'FOOBAR', 'barbar' ] to equal [ 'FOOBAR', 'barbaz' ]\n" + + '\n' + + '[\n' + + " 'FOOBAR',\n" + + " 'barbar' // should equal 'barbaz'\n" + + ' //\n' + + ' // -barbar\n' + + ' // +barbaz\n' + + ']' + ); + }); + }); + it('should inspect as [...] at depth 2+', function() { expect( [[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]]], From 0dc72a9a9c9d9d9b9a25d8e4c93e9a781dd29c5e Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 4 Mar 2018 17:49:42 +0100 Subject: [PATCH 09/17] Rework array-like "to satisfy" assertion with hooks support. --- lib/assertions.js | 51 ++++++++++++++++-------------- lib/utils.js | 6 ++-- test/types/array-like-type.spec.js | 22 +++++++++++++ 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index 152df6b6f..3d135059b 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1410,29 +1410,28 @@ module.exports = expect => { ' to [exhaustively] satisfy ', (expect, subject, value) => { expect.errorMode = 'bubble'; - let i; const subjectType = expect.subjectType; + const subjectKeys = subjectType.getKeys(subject); const valueType = expect.argTypes[0]; const valueKeys = valueType.getKeys(value).filter( key => utils.numericalRegExp.test(key) || typeof key === 'symbol' || // include keys whose value is not undefined on either LHS or RHS - typeof value[key] !== 'undefined' || - typeof subject[key] !== 'undefined' + typeof valueType.valueForKey(value, key) !== 'undefined' || + typeof subjectType.valueForKey(subject, key) !== 'undefined' ); const keyPromises = {}; - valueKeys.forEach(valueKey => { - keyPromises[valueKey] = expect.promise(() => { - const valueKeyType = expect.findTypeOf(value[valueKey]); + valueKeys.forEach(function(keyInValue) { + keyPromises[keyInValue] = expect.promise(function() { + const subjectKey = subjectType.valueForKey(subject, keyInValue); + const valueKey = valueType.valueForKey(value, keyInValue); + const valueKeyType = expect.findTypeOf(valueKey); + if (valueKeyType.is('function')) { - return value[valueKey](subject[valueKey]); + return valueKey(subjectKey); } else { - return expect( - subject[valueKey], - 'to [exhaustively] satisfy', - value[valueKey] - ); + return expect(subjectKey, 'to [exhaustively] satisfy', valueKey); } }); }); @@ -1457,7 +1456,7 @@ module.exports = expect => { key => utils.numericalRegExp.test(key) || typeof key === 'symbol' || - typeof subject[key] !== 'undefined' || + typeof subjectType.valueForKey(subject, key) !== 'undefined' || remainingKeysInSubject[key] === 2 ); // key checking succeeds with no outstanding keys @@ -1465,8 +1464,9 @@ module.exports = expect => { }), expect.promise.all(keyPromises) ]) - .caught(() => - expect.promise.settle(keyPromises).then(() => { + .caught(() => { + let i = 0; + return expect.promise.settle(keyPromises).then(() => { const toSatisfyMatrix = new Array(subject.length); for (i = 0; i < subject.length; i += 1) { toSatisfyMatrix[i] = new Array(value.length); @@ -1508,16 +1508,21 @@ module.exports = expect => { } let isAsync = false; + const subjectElements = utils.duplicateArrayLikeUsingType( + subject, + subjectType + ); + const valueElements = utils.duplicateArrayLikeUsingType( + value, + valueType + ); const nonNumericalKeysAndSymbols = !subjectType.numericalPropertiesOnly && - utils.uniqueNonNumericalStringsAndSymbols( - subjectType.getKeys(subject), - valueType.getKeys(value) - ); + utils.uniqueNonNumericalStringsAndSymbols(subjectKeys, valueKeys); const changes = arrayChanges( - subject, - value, + subjectElements, + valueElements, function equal(a, b, aIndex, bIndex) { toSatisfyMatrix[aIndex] = toSatisfyMatrix[aIndex] || []; const existingResult = toSatisfyMatrix[aIndex][bIndex]; @@ -1753,8 +1758,8 @@ module.exports = expect => { } }); } - }) - ); + }); + }); } ); diff --git a/lib/utils.js b/lib/utils.js index 5975ffb78..e41722543 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -30,11 +30,13 @@ const utils = (module.exports = { duplicateArrayLikeUsingType(obj, type) { var arr = new Array(obj.length); for (var i = 0; i < obj.length; i += 1) { - arr[i] = type.valueForKey(obj, i); + arr[i] = type.hasKey(obj, i) ? type.valueForKey(obj, i) : undefined; } Object.keys(obj).forEach(function(key) { if (!utils.numericalRegExp.test(key)) { - arr[key] = type.valueForKey(obj, key); + arr[key] = type.hasKey(obj, key) + ? type.valueForKey(obj, key) + : undefined; } }); if (Object.getOwnPropertySymbols) { diff --git a/test/types/array-like-type.spec.js b/test/types/array-like-type.spec.js index 498706ed3..2493ecb50 100644 --- a/test/types/array-like-type.spec.js +++ b/test/types/array-like-type.spec.js @@ -374,6 +374,28 @@ describe('array-like type', function() { }); }); + describe('with a custom subtype that comes with its own hasKey', function() { + it('should honour the presence of a key within inspection', function() { + var clonedExpect = expect.clone().addType({ + name: 'allExceptFoo', + base: 'array-like', + identify: Array.isArray, + numericalPropertiesOnly: false, + hasKey: function(obj, key) { + if (String(key).indexOf('foo') === 0) { + return false; + } + return obj[key]; + } + }); + + var arr = ['a']; + arr.fooAndBar = true; + + clonedExpect(arr, 'to inspect as', "[ 'a', fooAndBar: undefined ]"); + }); + }); + describe('with a custom subtype that comes with its own valueForKeys', function() { it('should process the elements in both inspection and diff in "to equal"', function() { var clonedExpect = expect.clone().addType({ From cf5de2177fc130ae2bafef3f92c329ae8a7bba59 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Wed, 7 Mar 2018 00:07:09 +0100 Subject: [PATCH 10/17] Ensure the keyComparator defaults to undefined. This fixes Unexpected execution when built with es5-shim and thus execution in the browser since null is disallowed. --- lib/types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types.js b/lib/types.js index 00f3a08bc..9fca3c67c 100644 --- a/lib/types.js +++ b/lib/types.js @@ -152,7 +152,7 @@ module.exports = function(expect) { return 0; } - : null, + : undefined, equal(a, b, equal) { if (a === b) { return true; From a0d26f3b1d442a5f2a315962d79490d68dc5594d Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 25 Mar 2018 13:57:20 +0200 Subject: [PATCH 11/17] Pass a cloned output to subjectType.property() calls. --- lib/assertions.js | 191 ++++++++++++++--------------- test/assertions/to-satisfy.spec.js | 17 ++- 2 files changed, 106 insertions(+), 102 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index 3d135059b..5d409dafe 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -455,16 +455,22 @@ module.exports = expect => { output.nl().indentLines(); subjectKeys.forEach((key, index) => { + const propertyOutput = subjectType.property( + output.clone(), + key, + inspect(subject[key]), + subjectIsArrayLike + ); + const delimiterOutput = subjectType.delimiter( + output.clone(), + index, + subjectKeys.length + ); + output .i() .block(function() { - subjectType.property( - this, - key, - inspect(subject[key]), - subjectIsArrayLike - ); - subjectType.delimiter(this, index, subjectKeys.length); + this.amend(propertyOutput).amend(delimiterOutput); if (!keyInValue[key]) { this.sp().annotationBlock(function() { this.error('should be removed'); @@ -1625,118 +1631,101 @@ module.exports = expect => { } else { return output.clone().block(function() { if (type === 'moveSource') { - subjectType - .property( - this, - diffItem.actualIndex, - inspect(diffItem.value), - true - ) + const propertyOutput = subjectType.property( + output.clone(), + diffItem.actualIndex, + inspect(diffItem.value), + true + ); + + this.amend(propertyOutput) .amend(delimiterOutput.sp()) .error('// should be moved'); } else if (type === 'insert') { this.annotationBlock(function() { - const index = - typeof diffItem.actualIndex !== 'undefined' - ? diffItem.actualIndex - : diffItem.expectedIndex; if ( expect.findTypeOf(diffItem.value).is('function') ) { - subjectType.property( - this, + this.error('missing: ').block(function() { + this.omitSubject = undefined; + const promise = + keyPromises[diffItem.expectedIndex]; + if (promise.isRejected()) { + this.appendErrorMessage(promise.reason()); + } else { + this.appendInspected(diffItem.value); + } + }); + } else { + const index = + typeof diffItem.actualIndex !== 'undefined' + ? diffItem.actualIndex + : diffItem.expectedIndex; + const propertyOutput = subjectType.property( + output.clone(), index, - output - .clone() - .error('missing: ') - .block(function() { - this.omitSubject = undefined; - const promise = - keyPromises[diffItem.expectedIndex]; - if (promise.isRejected()) { - this.appendErrorMessage( - promise.reason() - ); - } else { - this.appendInspected(diffItem.value); - } - }), + inspect(diffItem.value), true ); - } else { - var propertyOutput = output - .clone() - .error('missing '); - this.append( - subjectType.property( - propertyOutput, - index, - inspect(diffItem.value), - true - ) - ); + this.error('missing ').append(propertyOutput); } }); } else { - subjectType.property( - this, + const propertyOutput = subjectType.property( + output.clone(), diffItem.actualIndex, - output.clone().block(function() { - if (type === 'remove') { - this.append( - inspect(diffItem.value) - .amend(delimiterOutput.sp()) - .error('// should be removed') - ); - } else if (type === 'equal') { - this.append( - inspect(diffItem.value).amend( - delimiterOutput - ) + inspect(diffItem.value), + true + ); + + this.block(function() { + if (type === 'remove') { + this.append(propertyOutput) + .append(delimiterOutput) + .sp() + .error('// should be removed'); + } else if (type === 'equal') { + this.append(propertyOutput).append( + delimiterOutput + ); + } else { + const toSatisfyResult = + toSatisfyMatrix[diffItem.actualIndex][ + diffItem.expectedIndex + ]; + const valueDiff = + toSatisfyResult && + toSatisfyResult !== true && + toSatisfyResult.getDiff({ + output: output.clone() + }); + if (valueDiff && valueDiff.inline) { + this.append(valueDiff).append( + delimiterOutput ); } else { - const toSatisfyResult = - toSatisfyMatrix[diffItem.actualIndex][ - diffItem.expectedIndex - ]; - const valueDiff = - toSatisfyResult && - toSatisfyResult !== true && - toSatisfyResult.getDiff({ - output: output.clone() - }); - if (valueDiff && valueDiff.inline) { - this.append( - valueDiff.amend(delimiterOutput) - ); - } else { - this.append( - inspect(diffItem.value).amend( - delimiterOutput - ) - ) - .sp() - .annotationBlock(function() { - this.omitSubject = diffItem.value; - const label = toSatisfyResult.getLabel(); - if (label) { - this.error(label) - .sp() - .block(inspect(diffItem.expected)); - if (valueDiff) { - this.nl(2).append(valueDiff); - } - } else { - this.appendErrorMessage( - toSatisfyResult - ); + this.append(propertyOutput) + .append(delimiterOutput) + .sp() + .annotationBlock(function() { + this.omitSubject = diffItem.value; + const label = toSatisfyResult.getLabel(); + if (label) { + this.error(label) + .sp() + .block(inspect(diffItem.expected)); + if (valueDiff) { + this.nl(2).append(valueDiff); } - }); - } + } else { + this.appendErrorMessage( + toSatisfyResult + ); + } + }); } - }), - true - ); + } + }); } }); } diff --git a/test/assertions/to-satisfy.spec.js b/test/assertions/to-satisfy.spec.js index bb613708e..57f3681f8 100644 --- a/test/assertions/to-satisfy.spec.js +++ b/test/assertions/to-satisfy.spec.js @@ -93,7 +93,7 @@ describe('to satisfy assertion', function() { }); describe('with an array satisfied against an array', function() { - it('should render missing items nicely', function() { + it('should render missing number items nicely', function() { expect( function() { expect([], 'to satisfy', [1, 2]); @@ -108,6 +108,21 @@ describe('to satisfy assertion', function() { ); }); + it('should render missing object items nicely', function() { + expect( + function() { + expect([], 'to satisfy', [{ foo: true }, { baz: false }]); + }, + 'to throw', + 'expected [] to satisfy [ { foo: true }, { baz: false } ]\n' + + '\n' + + '[\n' + + ' // missing { foo: true }\n' + + ' // missing { baz: false }\n' + + ']' + ); + }); + it('should fall back to comparing index-by-index if one of the arrays has more than 10 entries', function() { expect( function() { From c1bdb3eef1907dec7fb0387f3fc27d4a893c2883 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 25 Mar 2018 15:16:03 +0200 Subject: [PATCH 12/17] Add additional test for array-like custom inspection. --- test/assertions/to-satisfy.spec.js | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/assertions/to-satisfy.spec.js b/test/assertions/to-satisfy.spec.js index 57f3681f8..458b29d18 100644 --- a/test/assertions/to-satisfy.spec.js +++ b/test/assertions/to-satisfy.spec.js @@ -123,6 +123,48 @@ describe('to satisfy assertion', function() { ); }); + it('should render missing custom items nicely', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addStyle('xuuqProperty', function(key, inspectedValue) { + this.text('<') + .appendInspected(key) + .text('> --> ') + .append(inspectedValue); + }); + + clonedExpect.addType({ + name: 'xuuq', + base: 'object', + identify: function(obj) { + return obj && typeof 'object' && obj.quux === 'xuuq'; + }, + property: function(output, key, inspectedValue) { + return output.xuuqProperty(key, inspectedValue); + } + }); + + expect( + function() { + clonedExpect([], 'to satisfy', [ + { quux: 'xuuq', foo: true }, + { quux: 'xuuq', baz: false } + ]); + }, + 'to throw', + 'expected [] to satisfy\n' + + '[\n' + + " { <'quux'> --> 'xuuq', <'foo'> --> true },\n" + + " { <'quux'> --> 'xuuq', <'baz'> --> false }\n" + + ']\n' + + '\n' + + '[\n' + + " // missing { <'quux'> --> 'xuuq', <'foo'> --> true }\n" + + " // missing { <'quux'> --> 'xuuq', <'baz'> --> false }\n" + + ']' + ); + }); + it('should fall back to comparing index-by-index if one of the arrays has more than 10 entries', function() { expect( function() { From 3766b9b2d3a4d3dd108e3eeaa115bc3b544992a0 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Sun, 25 Mar 2018 16:37:29 +0200 Subject: [PATCH 13/17] Update assertions case missed in a0d26f3 & tweak a couple of blocks. --- lib/assertions.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index 5d409dafe..33e74040c 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1638,8 +1638,9 @@ module.exports = expect => { true ); - this.amend(propertyOutput) - .amend(delimiterOutput.sp()) + this.append(propertyOutput) + .append(delimiterOutput) + .sp() .error('// should be moved'); } else if (type === 'insert') { this.annotationBlock(function() { @@ -1944,13 +1945,12 @@ module.exports = expect => { missingArrayIndex || index >= subjectKeys.length - 1; if (!omitDelimiter) { - valueOutput.amend( - subjectType.delimiter( - output.clone(), - index, - keys.length - ) + const delimiterOutput = subjectType.delimiter( + output.clone(), + index, + keys.length ); + valueOutput.amend(delimiterOutput); } const annotationOnNextLine = @@ -1978,12 +1978,14 @@ module.exports = expect => { valueOutput = output.clone().block(valueOutput); } - subjectType.property( - this, + const propertyOutput = subjectType.property( + output.clone(), key, valueOutput, subjectIsArrayLike ); + + this.append(propertyOutput); }); }); From a60873fdca7b5ee62b4047ea9bda835f6072f021 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Mon, 26 Mar 2018 10:06:29 +0200 Subject: [PATCH 14/17] Pass over types for cloned output to type.property() calls. This commit also aligns the array-like diff drawing code much more closely with its counterpart in the "to satisfy" assertion. --- lib/types.js | 100 +++++++++++++++-------------- test/types/array-like-type.spec.js | 70 ++++++++++++++++++++ 2 files changed, 121 insertions(+), 49 deletions(-) diff --git a/lib/types.js b/lib/types.js index 9fca3c67c..c4ce3c8e0 100644 --- a/lib/types.js +++ b/lib/types.js @@ -680,14 +680,15 @@ module.exports = function(expect) { } else { return output.clone().block(function() { if (diffItem.type === 'moveSource') { - type - .property( - this, - diffItem.actualIndex, - inspect(diffItem.value), - true - ) - .amend(delimiterOutput.sp()) + const propertyOutput = type.property( + output.clone(), + diffItem.actualIndex, + inspect(diffItem.value), + true + ); + this.amend(propertyOutput) + .amend(delimiterOutput) + .sp() .error('// should be moved'); } else if (diffItem.type === 'insert') { this.annotationBlock(function() { @@ -696,59 +697,60 @@ module.exports = function(expect) { typeof diffItem.actualIndex !== 'undefined' ? diffItem.actualIndex : diffItem.expectedIndex; - type.property(this, index, inspect(diffItem.value), true); + const propertyOutput = type.property( + output.clone(), + index, + inspect(diffItem.value), + true + ); + this.amend(propertyOutput); }); }); } else if (diffItem.type === 'remove') { this.block(function() { - type - .property( - this, - diffItem.actualIndex, - inspect(diffItem.value), - true - ) - .amend(delimiterOutput.sp()) + const propertyOutput = type.property( + output.clone(), + diffItem.actualIndex, + inspect(diffItem.value), + true + ); + this.amend(propertyOutput) + .amend(delimiterOutput) + .sp() .error('// should be removed'); }); } else if (diffItem.type === 'equal') { this.block(function() { - type - .property( - this, - diffItem.actualIndex, - inspect(diffItem.value), - true - ) - .amend(delimiterOutput); + const propertyOutput = type.property( + output.clone(), + diffItem.actualIndex, + inspect(diffItem.value), + true + ); + this.amend(propertyOutput).amend(delimiterOutput); }); } else { this.block(function() { const valueDiff = diff(diffItem.value, diffItem.expected); - type.property( - this, - diffItem.actualIndex, - output.clone().block(function() { - if (valueDiff && valueDiff.inline) { - this.append(valueDiff.amend(delimiterOutput)); - } else if (valueDiff) { - this.append( - inspect(diffItem.value).amend(delimiterOutput.sp()) - ).annotationBlock(function() { - this.shouldEqualError(diffItem.expected, inspect) - .nl(2) - .append(valueDiff); - }); - } else { - this.append( - inspect(diffItem.value).amend(delimiterOutput.sp()) - ).annotationBlock(function() { - this.shouldEqualError(diffItem.expected, inspect); - }); - } - }), - true - ); + if (valueDiff && valueDiff.inline) { + this.append(valueDiff).append(delimiterOutput); + } else { + const propertyOutput = type.property( + output.clone(), + diffItem.actualIndex, + inspect(diffItem.value), + true + ); + this.append(propertyOutput) + .append(delimiterOutput) + .sp() + .annotationBlock(function() { + this.shouldEqualError(diffItem.expected, inspect); + if (valueDiff) { + this.nl(2).append(valueDiff); + } + }); + } }); } }); diff --git a/test/types/array-like-type.spec.js b/test/types/array-like-type.spec.js index 2493ecb50..febb69dcd 100644 --- a/test/types/array-like-type.spec.js +++ b/test/types/array-like-type.spec.js @@ -396,6 +396,76 @@ describe('array-like type', function() { }); }); + describe('with a subtype that overrides property()', function() { + it('should render correctly in both inspection and diff in "to equal"', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addStyle('xuuqProperty', function(key, inspectedValue) { + this.text('<') + .appendInspected(key) + .text('> --> ') + .append(inspectedValue); + }); + + clonedExpect.addType({ + name: 'xuuq', + base: 'array-like', + numericalPropertiesOnly: false, + identify: function(obj) { + return obj && typeof 'object' && obj.quux === 'xuuq'; + }, + property: function(output, key, inspectedValue, isSubjectArrayLike) { + if (isSubjectArrayLike && !isNaN(Number(key))) { + return this.baseType.property( + output, + key, + inspectedValue, + isSubjectArrayLike + ); + } + return output.xuuqProperty(key, inspectedValue); + } + }); + + const lhs = [1, 2, 3]; + lhs.quux = 'xuuq'; + lhs.foobar = 'faz'; + lhs.missing = true; + const rhs = [1, 2, 4]; + rhs.quux = 'xuuq'; + rhs.foobar = 'baz'; + + expect( + function() { + clonedExpect(lhs, 'to equal', rhs); + }, + 'to throw', + 'expected\n' + + '[\n' + + ' 1,\n' + + ' 2,\n' + + ' 3,\n' + + " <'quux'> --> 'xuuq',\n" + + " <'foobar'> --> 'faz',\n" + + " <'missing'> --> true\n" + + ']\n' + + "to equal [ 1, 2, 4, <'quux'> --> 'xuuq', <'foobar'> --> 'baz' ]\n" + + '\n' + + '[\n' + + ' 1,\n' + + ' 2,\n' + + ' 3, // should equal 4\n' + + " <'quux'> --> 'xuuq',\n" + + " <'foobar'> --> 'faz', // should equal 'baz'\n" + + ' //\n' + + ' // -faz\n' + + ' // +baz\n' + + " <'missing'> --> true // should be removed\n" + + ']' + ); + }); + }); + describe('with a custom subtype that comes with its own valueForKeys', function() { it('should process the elements in both inspection and diff in "to equal"', function() { var clonedExpect = expect.clone().addType({ From 009a2524e3e858f3aac8f223ee190c7586464c3f Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Tue, 27 Mar 2018 10:53:32 +0200 Subject: [PATCH 15/17] Fix missed valueForKey conversion in array-like satisfy and add test. --- lib/assertions.js | 2 +- test/types/object-type.spec.js | 40 ++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index 33e74040c..b62d058c9 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1804,7 +1804,7 @@ module.exports = expect => { const nonOwnKeysWithDefinedValues = keys.filter( key => !Object.prototype.hasOwnProperty.call(subject, key) && - typeof subject[key] !== 'undefined' + typeof subjectType.valueForKey(subject, key) !== 'undefined' ); const valueKeysWithDefinedValues = keys.filter( key => typeof valueType.valueForKey(value, key) !== 'undefined' diff --git a/test/types/object-type.spec.js b/test/types/object-type.spec.js index c438079bb..06f9d9c28 100644 --- a/test/types/object-type.spec.js +++ b/test/types/object-type.spec.js @@ -325,7 +325,6 @@ describe('object type', function() { describe('with a subtype that overrides property()', function() { it('should render correctly in both inspection and diff', function() { var clonedExpect = expect.clone(); - var customObject = { quux: 'xuuq', foobar: 'faz' }; clonedExpect.addStyle('xuuqProperty', function(key, inspectedValue) { this.text('<') @@ -347,7 +346,7 @@ describe('object type', function() { expect( function() { - clonedExpect(customObject, 'to equal', { + clonedExpect({ quux: 'xuuq', foobar: 'faz' }, 'to equal', { quux: 'xuuq', foobar: 'baz' }); @@ -377,6 +376,9 @@ describe('object type', function() { return obj && typeof 'object' && obj.nine === 9; }, valueForKey: function(obj, key) { + if (key === 'oof') { + return; + } if (typeof obj[key] === 'string') { return obj[key].toUpperCase(); } @@ -425,5 +427,39 @@ describe('object type', function() { '}' ); }); + + (Object.setPrototypeOf ? it : it.skip)( + 'should process keys from the prototype chain in "to exhaustively satisfy"', + function() { + var fooObject = { + foo: 'bAr', + oof: 'should not appear' + }; + var chainedObject = { nine: 9, baz: undefined }; + Object.setPrototypeOf(chainedObject, fooObject); + + expect( + function() { + clonedExpect(chainedObject, 'to exhaustively satisfy', { + nine: 9, + foo: 'BaZ', + baz: expect.it('to be undefined') + }); + }, + 'to throw', + 'expected { nine: 9, baz: undefined }\n' + + "to exhaustively satisfy { nine: 9, foo: 'BAZ', baz: expect.it('to be undefined') }\n" + + '\n' + + '{\n' + + ' nine: 9,\n' + + ' baz: undefined\n' + + " foo: 'BAR' // should equal 'BAZ'\n" + + ' //\n' + + ' // -BAR\n' + + ' // +BAZ\n' + + '}' + ); + } + ); }); }); From d1f01141bbfc60242dc0260abc97abb097af9b69 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Tue, 27 Mar 2018 10:54:01 +0200 Subject: [PATCH 16/17] Convert a number of additional object assertions to valueForKey. * to have a value satisfying * to have keys * to have property --- lib/assertions.js | 23 ++++++++---- .../to-have-a-value-satisfying.spec.js | 29 +++++++++++++++ test/assertions/to-have-keys.spec.js | 35 +++++++++++++++++++ test/assertions/to-have-property.spec.js | 26 ++++++++++++++ 4 files changed, 106 insertions(+), 7 deletions(-) diff --git a/lib/assertions.js b/lib/assertions.js index b62d058c9..f5992088a 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -286,8 +286,12 @@ module.exports = expect => { expect.addAssertion( ' [not] to have property ', (expect, subject, key) => { - expect(subject[key], '[!not] to be undefined'); - return subject[key]; + const subjectType = expect.findTypeOf(subject); + const subjectKey = subjectType.is('function') + ? subject[key] + : subjectType.valueForKey(subject, key); + expect(subjectKey, '[!not] to be undefined'); + return subjectKey; } ); @@ -421,7 +425,8 @@ module.exports = expect => { ' to [not] [only] have keys ', (expect, subject, keys) => { const keysInSubject = {}; - const subjectKeys = expect.findTypeOf(subject).getKeys(subject); + const subjectType = expect.findTypeOf(subject); + const subjectKeys = subjectType.getKeys(subject); subjectKeys.forEach(key => { keysInSubject[key] = true; }); @@ -448,7 +453,6 @@ module.exports = expect => { keys.forEach(key => { keyInValue[key] = true; }); - const subjectType = expect.findTypeOf(subject); const subjectIsArrayLike = subjectType.is('array-like'); subjectType.prefix(output, subject); @@ -458,7 +462,7 @@ module.exports = expect => { const propertyOutput = subjectType.property( output.clone(), key, - inspect(subject[key]), + inspect(subjectType.valueForKey(subject, key)), subjectIsArrayLike ); const delimiterOutput = subjectType.delimiter( @@ -1140,7 +1144,8 @@ module.exports = expect => { expect(subject, 'not to be empty'); expect.errorMode = 'bubble'; - const keys = expect.subjectType.getKeys(subject); + const subjectType = expect.findTypeOf(subject); + const keys = subjectType.getKeys(subject); return expect.promise .any( keys.map((key, index) => { @@ -1153,7 +1158,11 @@ module.exports = expect => { expected = nextArg; } return expect.promise(() => - expect(subject[key], 'to [exhaustively] satisfy', expected) + expect( + subjectType.valueForKey(subject, key), + 'to [exhaustively] satisfy', + expected + ) ); }) ) diff --git a/test/assertions/to-have-a-value-satisfying.spec.js b/test/assertions/to-have-a-value-satisfying.spec.js index eeb0782e9..8d39b1659 100644 --- a/test/assertions/to-have-a-value-satisfying.spec.js +++ b/test/assertions/to-have-a-value-satisfying.spec.js @@ -157,4 +157,33 @@ describe('to have a value satisfying assertion', function() { ); }); }); + + describe('with a subtype that overrides valueForKey()', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'oneFooObject', + base: 'object', + identify: function(obj) { + return obj && typeof 'object' && obj.foo === ''; + }, + valueForKey: function(obj, key) { + if (key === 'foo') { + return 1; + } + return obj[key]; + } + }); + + it('should process the value in "to have a value satisfying"', function() { + expect( + clonedExpect( + { foo: '' }, + 'to have a value satisfying', + expect.it('to be a number') + ), + 'to be fulfilled' + ); + }); + }); }); diff --git a/test/assertions/to-have-keys.spec.js b/test/assertions/to-have-keys.spec.js index ea1daf8a9..d3113a73e 100644 --- a/test/assertions/to-have-keys.spec.js +++ b/test/assertions/to-have-keys.spec.js @@ -51,4 +51,39 @@ describe('to have keys assertion', function() { it('should work with non-enumerable keys returned by the getKeys function of the subject type', function() { expect(new Error('foo'), 'to only have key', 'message'); }); + + describe('with a subtype that overrides valueForKey()', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'upperFooObject', + base: 'object', + identify: function(obj) { + return obj && typeof 'object' && obj.foo === ''; + }, + valueForKey: function(obj, key) { + if (key === 'foo') { + return 'FOO'; + } + return obj[key]; + } + }); + + it('should process the value in "to only have keys"', function() { + expect( + function() { + clonedExpect({ oof: undefined, foo: '' }, 'to only have keys', [ + 'oof' + ]); + }, + 'to throw', + "expected { oof: undefined, foo: 'FOO' } to only have keys [ 'oof' ]\n" + + '\n' + + '{\n' + + ' oof: undefined,\n' + + " foo: 'FOO' // should be removed\n" + + '}' + ); + }); + }); }); diff --git a/test/assertions/to-have-property.spec.js b/test/assertions/to-have-property.spec.js index 26b553ce7..6c255fc47 100644 --- a/test/assertions/to-have-property.spec.js +++ b/test/assertions/to-have-property.spec.js @@ -157,4 +157,30 @@ describe('to have property assertion', function() { ' [not] to have own property ' ); }); + + describe('with a subtype that overrides valueForKey()', function() { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'upperCaseObject', + base: 'object', + identify: function(obj) { + return obj && typeof 'object'; + }, + valueForKey: function(obj, key) { + if (typeof obj[key] === 'string') { + return obj[key].toUpperCase(); + } + return obj[key]; + } + }); + + it('should process the value in "to have property"', function() { + expect( + clonedExpect({ foo: 'bAr' }, 'to have property', 'foo'), + 'to be fulfilled with', + 'BAR' + ); + }); + }); }); From 505c32bfd1d106159003cc8f0c906e39237b7fb6 Mon Sep 17 00:00:00 2001 From: Alex J Burke Date: Thu, 29 Mar 2018 14:14:43 +0200 Subject: [PATCH 17/17] Rework duplicateArrayLikeUsingType() so it uses the type getKeys. --- lib/utils.js | 40 ++++++++++++++++-------- test/types/array-like-type.spec.js | 49 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index e41722543..6b8217f87 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -28,22 +28,36 @@ const utils = (module.exports = { }), duplicateArrayLikeUsingType(obj, type) { - var arr = new Array(obj.length); - for (var i = 0; i < obj.length; i += 1) { - arr[i] = type.hasKey(obj, i) ? type.valueForKey(obj, i) : undefined; + const keys = type.getKeys(obj); + + let numericalKeyLength = keys.length; + if (!type.numericalPropertiesOnly) { + let nonNumericalKeyLength = 0; + // find non-numerical keys in reverse order to keep iteration minimal + for (let i = keys.length - 1; i > -1; i -= 1) { + const key = keys[i]; + if (typeof key === 'symbol' || !utils.numericalRegExp.test(key)) { + nonNumericalKeyLength += 1; + } else { + break; + } + } + // remove non-numerical keys to ensure the copy is sized correctly + numericalKeyLength -= nonNumericalKeyLength; } - Object.keys(obj).forEach(function(key) { - if (!utils.numericalRegExp.test(key)) { - arr[key] = type.hasKey(obj, key) - ? type.valueForKey(obj, key) - : undefined; + + const arr = new Array(numericalKeyLength); + + keys.forEach(function(key, index) { + const isNonNumericKey = index >= numericalKeyLength; + if (isNonNumericKey && !type.hasKey(obj, key)) { + // do not add non-numerical keys that are not actually attached + // to the array-like to ensure they will be treated as "missing" + return; } + arr[key] = type.hasKey(obj, key) ? type.valueForKey(obj, key) : undefined; }); - if (Object.getOwnPropertySymbols) { - Object.getOwnPropertySymbols(obj).forEach(function(key) { - arr[key] = type.valueForKey(obj, key); - }); - } + return arr; }, diff --git a/test/types/array-like-type.spec.js b/test/types/array-like-type.spec.js index febb69dcd..3605f3535 100644 --- a/test/types/array-like-type.spec.js +++ b/test/types/array-like-type.spec.js @@ -372,6 +372,55 @@ describe('array-like type', function() { ']' ); }); + + it('should honour the precise list of keys returned by getKeys in "to satisfy"', () => { + var clonedExpect = expect.clone(); + + clonedExpect.addType({ + name: 'foo', + base: 'array-like', + identify: function(obj) { + return obj && obj._isFoo; + }, + numericalPropertiesOnly: false, + getKeys: function(obj) { + var keys = this.baseType.getKeys(obj); + var fooIndex = keys.indexOf('_isFoo'); + if (fooIndex > -1) { + keys = keys.splice(fooIndex, 1); + } + keys.push('bar'); + return keys; + } + }); + + var foo1 = ['hey', 'there']; + foo1._isFoo = true; + Object.defineProperty(foo1, 'bar', { + value: 123, + enumerable: false + }); + var foo2 = ['hey', 'there']; + foo2._isFoo = true; + Object.defineProperty(foo2, 'bar', { + value: 456, + enumerable: false + }); + + expect( + function() { + clonedExpect(foo1, 'to satisfy', foo2); + }, + 'to throw', + "expected [ 'hey', 'there', bar: 123 ] to satisfy [ 'hey', 'there', bar: 456 ]\n" + + '\n' + + '[\n' + + " 'hey',\n" + + " 'there',\n" + + ' bar: 123 // should equal 456\n' + + ']' + ); + }); }); describe('with a custom subtype that comes with its own hasKey', function() {