From 1dc8fcbee7988f947bca48ac85497d010f16b155 Mon Sep 17 00:00:00 2001 From: Tim Seckinger Date: Wed, 13 Nov 2019 23:17:25 +0000 Subject: [PATCH] property-based test for Node `deepStrictEqual` equivalence (#9167) * property test toStrictEqual vs deepStrictEqual also, import expect so that property tests do not need rebuilds * fix primitives being considered equal to new Primitive()s * update changelog --- CHANGELOG.md | 3 + .../__snapshots__/matchers.test.js.snap | 79 +++++++++++++++---- .../matchers-toContain.property.test.ts | 1 + .../matchers-toContainEqual.property.test.ts | 1 + .../matchers-toEqual.property.test.ts | 1 + .../matchers-toStrictEqual.property.test.ts | 44 +++++++++-- .../expect/src/__tests__/matchers.test.js | 21 +++-- packages/expect/src/jasmineUtils.ts | 28 +++---- 8 files changed, 133 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bb90837c44..7f71e407fef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ - `[expect]` Display `expectedDiff` more carefully in `toBeCloseTo` ([#8389](https://github.com/facebook/jest/pull/8389)) - `[expect]` Avoid incorrect difference for subset when `toMatchObject` fails ([#9005](https://github.com/facebook/jest/pull/9005)) +- `[expect]` Consider all RegExp flags for equality ([#9167](https://github.com/facebook/jest/pull/9167)) +- `[expect]` [**BREAKING**] Consider primitives different from wrappers instantiated with `new` ([#9167](https://github.com/facebook/jest/pull/9167)) - `[jest-config]` Use half of the available cores when `watchAll` mode is enabled ([#9117](https://github.com/facebook/jest/pull/9117)) - `[jest-console]` Add missing `console.group` calls to `NullConsole` ([#9024](https://github.com/facebook/jest/pull/9024)) - `[jest-core]` Don't include unref'd timers in --detectOpenHandles results ([#8941](https://github.com/facebook/jest/pull/8941)) @@ -63,6 +65,7 @@ - `[docs]` Add alias and optional boolean value to `coverage` CLI Reference ([#8996](https://github.com/facebook/jest/pull/8996)) - `[docs]` Fix broken link pointing to legacy JS file in "Snapshot Testing". - `[docs]` Add `setupFilesAfterEnv` and `jest.setTimeout` example ([#8971](https://github.com/facebook/jest/pull/8971)) +- `[expect]` Test that `toStrictEqual` is equivalent to Node's `assert.deepStrictEqual` ([#9167](https://github.com/facebook/jest/pull/9167)) - `[jest]` [**BREAKING**] Use ESM exports ([#8874](https://github.com/facebook/jest/pull/8874)) - `[jest-cli]` [**BREAKING**] Use ESM exports ([#8874](https://github.com/facebook/jest/pull/8874)) - `[jest-cli]` [**BREAKING**] Remove re-exports from `@jest/core` ([#8874](https://github.com/facebook/jest/pull/8874)) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index f17d9dd254db..162652bbe6b4 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -1986,6 +1986,13 @@ Expected: {"asymmetricMatch": [Function asymmetricMatch]} Received: "Eve" `; +exports[`.toEqual() {pass: false} expect("abc").toEqual({"0": "a", "1": "b", "2": "c"}) 1`] = ` +expect(received).toEqual(expected) // deep equality + +Expected: {"0": "a", "1": "b", "2": "c"} +Received: "abc" +`; + exports[`.toEqual() {pass: false} expect("abd").toEqual(StringContaining "bc") 1`] = ` expect(received).toEqual(expected) // deep equality @@ -2021,6 +2028,13 @@ exports[`.toEqual() {pass: false} expect("type TypeName = T extends Function + type TypeName = T extends Function ? "function" : "object"; `; +exports[`.toEqual() {pass: false} expect(/abc/gy).toEqual(/abc/g) 1`] = ` +expect(received).toEqual(expected) // deep equality + +Expected: /abc/g +Received: /abc/gy +`; + exports[`.toEqual() {pass: false} expect([1, 2]).toEqual([2, 1]) 1`] = ` expect(received).toEqual(expected) // deep equality @@ -2053,6 +2067,13 @@ exports[`.toEqual() {pass: false} expect([1]).toEqual([2]) 1`] = ` ] `; +exports[`.toEqual() {pass: false} expect({"0": "a", "1": "b", "2": "c"}).toEqual("abc") 1`] = ` +expect(received).toEqual(expected) // deep equality + +Expected: "abc" +Received: {"0": "a", "1": "b", "2": "c"} +`; + exports[`.toEqual() {pass: false} expect({"a": 1, "b": 2}).toEqual(ObjectContaining {"a": 2}) 1`] = ` expect(received).toEqual(expected) // deep equality @@ -2112,6 +2133,27 @@ exports[`.toEqual() {pass: false} expect({"target": {"nodeType": 1, "value": "a" } `; +exports[`.toEqual() {pass: false} expect({}).toEqual({}) 1`] = ` +expect(received).toEqual(expected) // deep equality + +Expected: {} +Received: serializes to the same string +`; + +exports[`.toEqual() {pass: false} expect({}).toEqual(0) 1`] = ` +expect(received).toEqual(expected) // deep equality + +Expected: 0 +Received: {} +`; + +exports[`.toEqual() {pass: false} expect(0).toEqual({}) 1`] = ` +expect(received).toEqual(expected) // deep equality + +Expected: {} +Received: 0 +`; + exports[`.toEqual() {pass: false} expect(0).toEqual(-0) 1`] = ` expect(received).toEqual(expected) // deep equality @@ -2467,11 +2509,18 @@ Expected: not "abc" `; -exports[`.toEqual() {pass: true} expect("abc").not.toEqual({"0": "a", "1": "b", "2": "c"}) 1`] = ` +exports[`.toEqual() {pass: true} expect("abc").not.toEqual("abc") 2`] = ` +expect(received).not.toEqual(expected) // deep equality + +Expected: not "abc" + +`; + +exports[`.toEqual() {pass: true} expect("abc").not.toEqual("abc") 3`] = ` expect(received).not.toEqual(expected) // deep equality -Expected: not {"0": "a", "1": "b", "2": "c"} -Received: "abc" +Expected: not "abc" + `; exports[`.toEqual() {pass: true} expect("abcd").not.toEqual(StringContaining "bc") 1`] = ` @@ -2516,13 +2565,6 @@ Expected: not Any Received: [Function anonymous] `; -exports[`.toEqual() {pass: true} expect({"0": "a", "1": "b", "2": "c"}).not.toEqual("abc") 1`] = ` -expect(received).not.toEqual(expected) // deep equality - -Expected: not "abc" -Received: {"0": "a", "1": "b", "2": "c"} -`; - exports[`.toEqual() {pass: true} expect({"a": 1, "b": [Function b], "c": true}).not.toEqual({"a": 1, "b": Any, "c": Anything}) 1`] = ` expect(received).not.toEqual(expected) // deep equality @@ -2558,18 +2600,25 @@ Expected: not {} `; -exports[`.toEqual() {pass: true} expect({}).not.toEqual(0) 1`] = ` +exports[`.toEqual() {pass: true} expect({}).not.toEqual({}) 2`] = ` +expect(received).not.toEqual(expected) // deep equality + +Expected: not {} + +`; + +exports[`.toEqual() {pass: true} expect(0).not.toEqual(0) 1`] = ` expect(received).not.toEqual(expected) // deep equality Expected: not 0 -Received: {} + `; -exports[`.toEqual() {pass: true} expect(0).not.toEqual({}) 1`] = ` +exports[`.toEqual() {pass: true} expect(0).not.toEqual(0) 2`] = ` expect(received).not.toEqual(expected) // deep equality -Expected: not {} -Received: 0 +Expected: not 0 + `; exports[`.toEqual() {pass: true} expect(1).not.toEqual(1) 1`] = ` diff --git a/packages/expect/src/__tests__/matchers-toContain.property.test.ts b/packages/expect/src/__tests__/matchers-toContain.property.test.ts index 6cdcf3a907e0..4afc067d49f9 100644 --- a/packages/expect/src/__tests__/matchers-toContain.property.test.ts +++ b/packages/expect/src/__tests__/matchers-toContain.property.test.ts @@ -11,6 +11,7 @@ import { anythingSettings, assertSettings, } from './__arbitraries__/sharedSettings'; +import expect from '..'; describe('toContain', () => { it('should always find the value when inside the array', () => { diff --git a/packages/expect/src/__tests__/matchers-toContainEqual.property.test.ts b/packages/expect/src/__tests__/matchers-toContainEqual.property.test.ts index 33edfed6a40c..eb0804fd11bd 100644 --- a/packages/expect/src/__tests__/matchers-toContainEqual.property.test.ts +++ b/packages/expect/src/__tests__/matchers-toContainEqual.property.test.ts @@ -11,6 +11,7 @@ import { anythingSettings, assertSettings, } from './__arbitraries__/sharedSettings'; +import expect from '..'; describe('toContainEqual', () => { it('should always find the value when inside the array', () => { diff --git a/packages/expect/src/__tests__/matchers-toEqual.property.test.ts b/packages/expect/src/__tests__/matchers-toEqual.property.test.ts index 3ebefa5f3bd0..1d45a9a977c3 100644 --- a/packages/expect/src/__tests__/matchers-toEqual.property.test.ts +++ b/packages/expect/src/__tests__/matchers-toEqual.property.test.ts @@ -11,6 +11,7 @@ import { anythingSettings, assertSettings, } from './__arbitraries__/sharedSettings'; +import expect from '..'; describe('toEqual', () => { it('should be reflexive', () => { diff --git a/packages/expect/src/__tests__/matchers-toStrictEqual.property.test.ts b/packages/expect/src/__tests__/matchers-toStrictEqual.property.test.ts index 82b776cb175f..8be6bcd96d76 100644 --- a/packages/expect/src/__tests__/matchers-toStrictEqual.property.test.ts +++ b/packages/expect/src/__tests__/matchers-toStrictEqual.property.test.ts @@ -6,13 +6,32 @@ * */ +import assert from 'assert'; +import {onNodeVersions} from '@jest/test-utils'; import fc from 'fast-check'; import { anythingSettings, assertSettings, } from './__arbitraries__/sharedSettings'; +import expect from '..'; describe('toStrictEqual', () => { + const safeExpectStrictEqual = (a, b) => { + try { + expect(a).toStrictEqual(b); + return true; + } catch (err) { + return false; + } + }; + const safeAssertDeepStrictEqual = (a, b) => { + try { + assert.deepStrictEqual(a, b); + return true; + } catch (err) { + return false; + } + }; it('should be reflexive', () => { fc.assert( fc.property(fc.dedup(fc.anything(anythingSettings), 2), ([a, b]) => { @@ -24,14 +43,6 @@ describe('toStrictEqual', () => { }); it('should be symmetric', () => { - const safeExpectStrictEqual = (a, b) => { - try { - expect(a).toStrictEqual(b); - return true; - } catch (err) { - return false; - } - }; fc.assert( fc.property( fc.anything(anythingSettings), @@ -46,4 +57,21 @@ describe('toStrictEqual', () => { assertSettings, ); }); + + onNodeVersions('>=9', () => { + it('should be equivalent to Node deepStrictEqual', () => { + fc.assert( + fc.property( + fc.anything(anythingSettings), + fc.anything(anythingSettings), + (a, b) => { + expect(safeExpectStrictEqual(a, b)).toBe( + safeAssertDeepStrictEqual(a, b), + ); + }, + ), + assertSettings, + ); + }); + }); }); diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 4eb0b43e289b..97ad88b3333f 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -419,12 +419,19 @@ describe('.toStrictEqual()', () => { }); describe('.toEqual()', () => { + /* eslint-disable no-new-wrappers */ [ [true, false], [1, 2], [0, -0], [0, Number.MIN_VALUE], // issues/7941 [Number.MIN_VALUE, 0], + [0, new Number(0)], + [new Number(0), 0], + [new Number(0), new Number(1)], + ['abc', new String('abc')], + [new String('abc'), 'abc'], + [/abc/gsy, /abc/g], [{a: 1}, {a: 2}], [{a: 5}, {b: 6}], ['banana', 'apple'], @@ -548,15 +555,12 @@ describe('.toEqual()', () => { [true, true], [1, 1], [NaN, NaN], - // eslint-disable-next-line no-new-wrappers - [0, new Number(0)], - // eslint-disable-next-line no-new-wrappers - [new Number(0), 0], + [0, Number(0)], + [Number(0), 0], + [new Number(0), new Number(0)], ['abc', 'abc'], - // eslint-disable-next-line no-new-wrappers - [new String('abc'), 'abc'], - // eslint-disable-next-line no-new-wrappers - ['abc', new String('abc')], + [String('abc'), 'abc'], + ['abc', String('abc')], [[1], [1]], [ [1, 2], @@ -856,6 +860,7 @@ describe('.toEqual()', () => { expect(d).not.toEqual(c); }); }); + /* eslint-enable */ }); describe('.toBeInstanceOf()', () => { diff --git a/packages/expect/src/jasmineUtils.ts b/packages/expect/src/jasmineUtils.ts index 1c80689d0bc8..283af1331968 100644 --- a/packages/expect/src/jasmineUtils.ts +++ b/packages/expect/src/jasmineUtils.ts @@ -100,29 +100,29 @@ function eq( return false; } switch (className) { - // Strings, numbers, dates, and booleans are compared by value. + case '[object Boolean]': case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - return a == String(b); case '[object Number]': - return Object.is(Number(a), Number(b)); + if (typeof a !== typeof b) { + // One is a primitive, one a `new Primitive()` + return false; + } else if (typeof a !== 'object' && typeof b !== 'object') { + // both are proper primitives + return Object.is(a, b); + } else { + // both are `new Primitive()`s + return Object.is(a.valueOf(), b.valueOf()); + } case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // Coerce dates to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a == +b; // RegExps are compared by their source patterns and flags. case '[object RegExp]': - return ( - a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase - ); + return a.source === b.source && a.flags === b.flags; } - if (typeof a != 'object' || typeof b != 'object') { + if (typeof a !== 'object' || typeof b !== 'object') { return false; }