From ec5e2d0fdb436492d489aa79929a560f0f47b50d Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Thu, 28 Feb 2019 16:11:05 -0500 Subject: [PATCH] expect: Fix non-object received value in toHaveProperty (#7986) --- CHANGELOG.md | 1 + .../__snapshots__/matchers.test.js.snap | 149 ++++++++++++++++++ .../expect/src/__tests__/matchers.test.js | 33 ++++ packages/expect/src/matchers.ts | 6 +- packages/expect/src/utils.ts | 8 +- 5 files changed, 193 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e612711dae..92d3499b322e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - `[jest-changed-files]` Fix `getChangedFilesFromRoots` to not return parts of the commit messages as if they were files, when the commit messages contained multiple paragraphs ([#7961](https://github.com/facebook/jest/pull/7961)) - `[jest-haste-map]` Enforce uniqueness in names (mocks and haste ids) ([#8002](https://github.com/facebook/jest/pull/8002)) - `[static]` Remove console log '-' on the front page +- `[expect]` Fix non-object received value in toHaveProperty ([#7986](https://github.com/facebook/jest/pull/7986)) - `[jest-jasmine2]`: Throw explicit error when errors happen after test is considered complete ([#8005](https://github.com/facebook/jest/pull/8005)) - `[jest-circus]`: Throw explicit error when errors happen after test is considered complete ([#8005](https://github.com/facebook/jest/pull/8005)) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index d576cdbad479..3b43f553d7d3 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -2822,6 +2822,8 @@ exports[`.toHaveProperty() {error} expect({"a": {"b": {}}}).toHaveProperty('unde Expected has value: undefined" `; +exports[`.toHaveProperty() {error} expect({}).toHaveProperty('') 1`] = `"pass must be initialized"`; + exports[`.toHaveProperty() {error} expect(null).toHaveProperty('a.b') 1`] = ` "expect(received).toHaveProperty(path) @@ -2838,6 +2840,16 @@ exports[`.toHaveProperty() {error} expect(undefined).toHaveProperty('a') 1`] = ` Received has value: undefined" `; +exports[`.toHaveProperty() {pass: false} expect("").toHaveProperty('key') 1`] = ` +"expect(object).toHaveProperty(path) + +Expected the object: + \\"\\" +To have a nested property: + \\"key\\" +" +`; + exports[`.toHaveProperty() {pass: false} expect("abc").toHaveProperty('a.b.c') 1`] = ` "expect(object).toHaveProperty(path) @@ -2963,6 +2975,19 @@ Difference: Comparing two different types of values. Expected undefined but received number." `; +exports[`.toHaveProperty() {pass: false} expect({"a": {}}).toHaveProperty('a.b', undefined) 1`] = ` +"expect(object).toHaveProperty(path, value) + +Expected the object: + {\\"a\\": {}} +To have a nested property: + \\"a.b\\" +With a value of: + undefined +Received: + object.a: {}" +`; + exports[`.toHaveProperty() {pass: false} expect({"a": 1}).toHaveProperty('a.b.c.d') 1`] = ` "expect(object).toHaveProperty(path) @@ -3012,6 +3037,16 @@ Received: 1" `; +exports[`.toHaveProperty() {pass: false} expect({"key": 1}).toHaveProperty('not') 1`] = ` +"expect(object).toHaveProperty(path) + +Expected the object: + {\\"key\\": 1} +To have a nested property: + \\"not\\" +" +`; + exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a') 1`] = ` "expect(object).toHaveProperty(path) @@ -3068,6 +3103,16 @@ Difference: Comparing two different types of values. Expected undefined but received string." `; +exports[`.toHaveProperty() {pass: false} expect(0).toHaveProperty('key') 1`] = ` +"expect(object).toHaveProperty(path) + +Expected the object: + 0 +To have a nested property: + \\"key\\" +" +`; + exports[`.toHaveProperty() {pass: false} expect(1).toHaveProperty('a.b.c') 1`] = ` "expect(object).toHaveProperty(path) @@ -3090,6 +3135,50 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: false} expect(Symbol()).toHaveProperty('key') 1`] = ` +"expect(object).toHaveProperty(path) + +Expected the object: + Symbol() +To have a nested property: + \\"key\\" +" +`; + +exports[`.toHaveProperty() {pass: false} expect(false).toHaveProperty('key') 1`] = ` +"expect(object).toHaveProperty(path) + +Expected the object: + false +To have a nested property: + \\"key\\" +" +`; + +exports[`.toHaveProperty() {pass: true} expect("").toHaveProperty('length', 0) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + \\"\\" +Not to have a nested property: + \\"length\\" +With a value of: + 0 +" +`; + +exports[`.toHaveProperty() {pass: true} expect([Function memoized]).toHaveProperty('memo', []) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + [Function memoized] +Not to have a nested property: + \\"memo\\" +With a value of: + [] +" +`; + exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1') 1`] = ` "expect(object).not.toHaveProperty(path) @@ -3234,6 +3323,18 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: true} expect({"nodeName": "DIV"}).toHaveProperty('nodeType', 1) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {\\"nodeName\\": \\"DIV\\"} +Not to have a nested property: + \\"nodeType\\" +With a value of: + 1 +" +`; + exports[`.toHaveProperty() {pass: true} expect({"property": 1}).toHaveProperty('property', 1) 1`] = ` "expect(object).not.toHaveProperty(path, value) @@ -3246,6 +3347,42 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('a', undefined) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {\\"val\\": true} +Not to have a nested property: + \\"a\\" +With a value of: + undefined +" +`; + +exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('c', "c") 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {\\"val\\": true} +Not to have a nested property: + \\"c\\" +With a value of: + \\"c\\" +" +`; + +exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('val', true) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {\\"val\\": true} +Not to have a nested property: + \\"val\\" +With a value of: + true +" +`; + exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('a', undefined) 1`] = ` "expect(object).not.toHaveProperty(path, value) @@ -3270,6 +3407,18 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('setter', undefined) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {} +Not to have a nested property: + \\"setter\\" +With a value of: + undefined +" +`; + exports[`.toMatch() {pass: true} expect(Foo bar).toMatch(/^foo/i) 1`] = ` "expect(received).not.toMatch(expected) diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index cff90804df79..6b4b90788672 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -1270,7 +1270,26 @@ describe('.toHaveProperty()', () => { get b() { return 'b'; } + set setter(val) { + this.val = val; + } + } + + class Foo2 extends Foo { + get c() { + return 'c'; + } } + const foo2 = new Foo2(); + foo2.setter = true; + + function E(nodeName) { + this.nodeName = nodeName.toUpperCase(); + } + E.prototype.nodeType = 1; + + const memoized = function() {}; + memoized.memo = []; [ [{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 1], @@ -1283,6 +1302,13 @@ describe('.toHaveProperty()', () => { [Object.assign(Object.create(null), {property: 1}), 'property', 1], [new Foo(), 'a', undefined], [new Foo(), 'b', 'b'], + [new Foo(), 'setter', undefined], + [foo2, 'a', undefined], + [foo2, 'c', 'c'], + [foo2, 'val', true], + [new E('div'), 'nodeType', 1], + ['', 'length', 0], + [memoized, 'memo', []], ].forEach(([obj, keyPath, value]) => { test(`{pass: true} expect(${stringify( obj, @@ -1309,6 +1335,7 @@ describe('.toHaveProperty()', () => { [{a: {b: {c: 5}}}, 'a.b', {c: 4}], [new Foo(), 'a', 'a'], [new Foo(), 'b', undefined], + [{a: {}}, 'a.b', undefined], ].forEach(([obj, keyPath, value]) => { test(`{pass: false} expect(${stringify( obj, @@ -1344,6 +1371,11 @@ describe('.toHaveProperty()', () => { [{}, 'a'], [1, 'a.b.c'], ['abc', 'a.b.c'], + [false, 'key'], + [0, 'key'], + ['', 'key'], + [Symbol(), 'key'], + [Object.assign(Object.create(null), {key: 1}), 'not'], ].forEach(([obj, keyPath]) => { test(`{pass: false} expect(${stringify( obj, @@ -1361,6 +1393,7 @@ describe('.toHaveProperty()', () => { [{a: {b: {}}}, undefined], [{a: {b: {}}}, null], [{a: {b: {}}}, 1], + [{}, []], // Residue: pass must be initialized ].forEach(([obj, keyPath]) => { test(`{error} expect(${stringify( obj, diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index e76bd64c63c6..dbf2ddb80ea9 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -602,9 +602,9 @@ const matchers: MatchersObject = { const result = getPath(object, keyPath); const {lastTraversedObject, hasEndProp} = result; - const pass = valuePassed - ? equals(result.value, value, [iterableEquality]) - : hasEndProp; + const pass = + hasEndProp && + (!valuePassed || equals(result.value, value, [iterableEquality])); const traversedPath = result.traversedPath.join('.'); diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 23d1fe1af797..13047d555ff1 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -6,6 +6,7 @@ * */ +import {isPrimitive} from 'jest-get-type'; import { equals, isA, @@ -80,7 +81,12 @@ export const getPath = ( result.traversedPath.unshift(prop); if (lastProp) { - result.hasEndProp = prop in object; + // Does object have the property with an undefined value? + // Although primitive values support bracket notation (above) + // they would throw TypeError for in operator (below). + result.hasEndProp = + newObject !== undefined || (!isPrimitive(object) && prop in object); + if (!result.hasEndProp) { result.traversedPath.shift(); }