diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b117fe5e892..ee609f944a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,8 @@ ([#5980](https://github.com/facebook/jest/pull/5980)) * `[jest-message-util]` Include column in stack frames ([#5889](https://github.com/facebook/jest/pull/5889)) +* `[expect]` Introduce toStrictEqual + ([#6032](https://github.com/facebook/jest/pull/6032)) ### Fixes diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 9d0b47f25c53..50b87353188e 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -1096,6 +1096,33 @@ from the test. _Note: While snapshot testing is most commonly used with React components, any serializable value can be used as a snapshot._ +### `.toStrictEqual(value)` + +Use `.toStrictEqual` to test that objects have the same types as well as +structure. + +Differences from `.toEqual`: + +* Keys with `undefined` properties are checked. e.g. `{a: undefined, b: 2}` does + not match `{b: 2}` when using `.toStrictEqual`. +* Object types are checked to be equal. e.g. A class instance with fields `a` + and `b` will not equal a literal object with fields `a` and `b`. + +```js +class LaCroix { + constructor(flavor) { + this.flavor = flavor; + } +} + +describe('the La Croix cans on my desk', () => { + test('are not semantically the same', () => { + expect(new LaCroix('lemon')).toEqual({flavor: 'lemon'}); + expect(new LaCroix('lemon')).not.toStrictEqual({flavor: 'lemon'}); + }); +}); +``` + ### `.toThrow(error)` Also under the alias: `.toThrowError(error)` diff --git a/integration-tests/snapshot-escape/__tests__/__snapshots__/snapshot_escape_regex.js.snap b/integration-tests/snapshot-escape/__tests__/__snapshots__/snapshot_escape_regex.js.snap new file mode 100644 index 000000000000..c1e3ebd4c7f6 --- /dev/null +++ b/integration-tests/snapshot-escape/__tests__/__snapshots__/snapshot_escape_regex.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`escape regex 1`] = `/\\\\dd\\\\ \\\\s\\+ \\\\w \\\\\\\\\\\\\\[ \\\\\\. blahzz\\.\\* \\[xyz\\]\\+/`; + +exports[`escape regex nested in object 1`] = ` +Object { + "regex": /\\\\dd\\\\ \\\\s\\+ \\\\w \\\\\\\\\\\\\\[ \\\\\\. blahzz\\.\\* \\[xyz\\]\\+/, +} +`; diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index bcd836e78097..2e9c8a300e2b 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -204,6 +204,34 @@ describe('.toBe()', () => { }); }); +describe('.toStrictEqual()', () => { + class TestClass { + constructor(a, b) { + this.a = a; + this.b = b; + } + } + + it('does not ignore keys with undefined values', () => { + expect({ + a: undefined, + b: 2, + }).not.toStrictEqual({b: 2}); + }); + + it('passes when comparing same type', () => { + expect({ + test: new TestClass(1, 2), + }).toStrictEqual({test: new TestClass(1, 2)}); + }); + + it('does not pass for different types', () => { + expect({ + test: new TestClass(1, 2), + }).not.toStrictEqual({test: {a: 1, b: 2}}); + }); +}); + describe('.toEqual()', () => { [ [true, false], diff --git a/packages/expect/src/jasmine_utils.js b/packages/expect/src/jasmine_utils.js index 5a79261c873f..404a45f64528 100644 --- a/packages/expect/src/jasmine_utils.js +++ b/packages/expect/src/jasmine_utils.js @@ -28,9 +28,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. type Tester = (a: any, b: any) => boolean | typeof undefined; // Extracted out of jasmine 2.5.2 -export function equals(a: any, b: any, customTesters?: Array): boolean { +export function equals(a: any, b: any, customTesters?: Array, strictCheck?: boolean): boolean { customTesters = customTesters || []; - return eq(a, b, [], [], customTesters); + return eq(a, b, [], [], customTesters, strictCheck ? hasKey : hasDefinedKey); } function isAsymmetric(obj) { @@ -56,7 +56,7 @@ function asymmetricMatch(a, b) { // Equality function lovingly adapted from isEqual in // [Underscore](http://underscorejs.org) -function eq(a, b, aStack, bStack, customTesters): boolean { +function eq(a, b, aStack, bStack, customTesters, hasKey): boolean { var result = true; var asymmetricResult = asymmetricMatch(a, b); @@ -160,7 +160,7 @@ function eq(a, b, aStack, bStack, customTesters): boolean { } while (size--) { - result = eq(a[size], b[size], aStack, bStack, customTesters); + result = eq(a[size], b[size], aStack, bStack, customTesters, hasKey); if (!result) { return false; } @@ -168,12 +168,12 @@ function eq(a, b, aStack, bStack, customTesters): boolean { } // Deep compare objects. - var aKeys = keys(a, className == '[object Array]'), + var aKeys = keys(a, className == '[object Array]', hasKey), key; size = aKeys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b, className == '[object Array]').length !== size) { + if (keys(b, className == '[object Array]', hasKey).length !== size) { return false; } @@ -181,7 +181,7 @@ function eq(a, b, aStack, bStack, customTesters): boolean { key = aKeys[size]; // Deep compare each member - result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters); + result = hasKey(b, key) && eq(a[key], b[key], aStack, bStack, customTesters, hasKey); if (!result) { return false; @@ -194,11 +194,11 @@ function eq(a, b, aStack, bStack, customTesters): boolean { return result; } -function keys(obj, isArray) { +function keys(obj, isArray, hasKey) { var allKeys = (function(o) { var keys = []; for (var key in o) { - if (has(o, key)) { + if (hasKey(o, key)) { keys.push(key); } } @@ -223,9 +223,15 @@ function keys(obj, isArray) { return extraKeys; } -function has(obj, key) { +function hasDefinedKey(obj, key) { return ( - Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== undefined + hasKey(obj, key) && obj[key] !== undefined + ); +} + +function hasKey(obj, key) { + return ( + Object.prototype.hasOwnProperty.call(obj, key) ); } diff --git a/packages/expect/src/matchers.js b/packages/expect/src/matchers.js index 773502f7407b..a9046862e119 100644 --- a/packages/expect/src/matchers.js +++ b/packages/expect/src/matchers.js @@ -29,6 +29,7 @@ import { getPath, iterableEquality, subsetEquality, + typeEquality, } from './utils'; import {equals} from './jasmine_utils'; @@ -615,6 +616,43 @@ const matchers: MatchersObject = { return {message, pass}; }, + + toStrictEqual(received: any, expected: any) { + const pass = equals( + received, + expected, + [iterableEquality, typeEquality], + true, + ); + + const message = pass + ? () => + matcherHint('.not.toStrictEqual') + + '\n\n' + + `Expected value to not equal:\n` + + ` ${printExpected(expected)}\n` + + `Received:\n` + + ` ${printReceived(received)}` + : () => { + const diffString = diff(expected, received, { + expand: this.expand, + }); + return ( + matcherHint('.toStrictEqual') + + '\n\n' + + `Expected value to equal:\n` + + ` ${printExpected(expected)}\n` + + `Received:\n` + + ` ${printReceived(received)}` + + (diffString ? `\n\nDifference:\n\n${diffString}` : '') + ); + }; + + // Passing the the actual and expected objects so that a custom reporter + // could access them, for example in order to display a custom visual diff, + // or create a different error message + return {actual: received, expected, message, name: 'toStrictEqual', pass}; + }, }; export default matchers; diff --git a/packages/expect/src/utils.js b/packages/expect/src/utils.js index 12eceffd953f..a6cc277952e6 100644 --- a/packages/expect/src/utils.js +++ b/packages/expect/src/utils.js @@ -185,6 +185,14 @@ export const subsetEquality = (object: Object, subset: Object) => { ); }; +export const typeEquality = (a: any, b: any) => { + if (a == null || b == null || a.constructor.name === b.constructor.name) { + return undefined; + } + + return false; +}; + export const partition = ( items: Array, predicate: T => boolean,