From ec5e2d0fdb436492d489aa79929a560f0f47b50d Mon Sep 17 00:00:00 2001
From: Mark Pedrotti <pedrottimark@gmail.com>
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: <green>undefined</>"
 `;
 
+exports[`.toHaveProperty() {error} expect({}).toHaveProperty('') 1`] = `"pass must be initialized"`;
+
 exports[`.toHaveProperty() {error} expect(null).toHaveProperty('a.b') 1`] = `
 "<dim>expect(</><red>received</><dim>).toHaveProperty(</><green>path</><dim>)</>
 
@@ -2838,6 +2840,16 @@ exports[`.toHaveProperty() {error} expect(undefined).toHaveProperty('a') 1`] = `
 Received has value: <red>undefined</>"
 `;
 
+exports[`.toHaveProperty() {pass: false} expect("").toHaveProperty('key') 1`] = `
+"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
+
+Expected the object:
+  <red>\\"\\"</>
+To have a nested property:
+  <green>\\"key\\"</>
+"
+`;
+
 exports[`.toHaveProperty() {pass: false} expect("abc").toHaveProperty('a.b.c') 1`] = `
 "<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
 
@@ -2963,6 +2975,19 @@ Difference:
   Comparing two different types of values. Expected <green>undefined</> but received <red>number</>."
 `;
 
+exports[`.toHaveProperty() {pass: false} expect({"a": {}}).toHaveProperty('a.b', undefined) 1`] = `
+"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>{\\"a\\": {}}</>
+To have a nested property:
+  <green>\\"a.b\\"</>
+With a value of:
+  <green>undefined</>
+Received:
+  <red>object</>.a: <red>{}</>"
+`;
+
 exports[`.toHaveProperty() {pass: false} expect({"a": 1}).toHaveProperty('a.b.c.d') 1`] = `
 "<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
 
@@ -3012,6 +3037,16 @@ Received:
   <red>1</>"
 `;
 
+exports[`.toHaveProperty() {pass: false} expect({"key": 1}).toHaveProperty('not') 1`] = `
+"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
+
+Expected the object:
+  <red>{\\"key\\": 1}</>
+To have a nested property:
+  <green>\\"not\\"</>
+"
+`;
+
 exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a') 1`] = `
 "<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
 
@@ -3068,6 +3103,16 @@ Difference:
   Comparing two different types of values. Expected <green>undefined</> but received <red>string</>."
 `;
 
+exports[`.toHaveProperty() {pass: false} expect(0).toHaveProperty('key') 1`] = `
+"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
+
+Expected the object:
+  <red>0</>
+To have a nested property:
+  <green>\\"key\\"</>
+"
+`;
+
 exports[`.toHaveProperty() {pass: false} expect(1).toHaveProperty('a.b.c') 1`] = `
 "<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
 
@@ -3090,6 +3135,50 @@ With a value of:
 "
 `;
 
+exports[`.toHaveProperty() {pass: false} expect(Symbol()).toHaveProperty('key') 1`] = `
+"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
+
+Expected the object:
+  <red>Symbol()</>
+To have a nested property:
+  <green>\\"key\\"</>
+"
+`;
+
+exports[`.toHaveProperty() {pass: false} expect(false).toHaveProperty('key') 1`] = `
+"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
+
+Expected the object:
+  <red>false</>
+To have a nested property:
+  <green>\\"key\\"</>
+"
+`;
+
+exports[`.toHaveProperty() {pass: true} expect("").toHaveProperty('length', 0) 1`] = `
+"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>\\"\\"</>
+Not to have a nested property:
+  <green>\\"length\\"</>
+With a value of:
+  <green>0</>
+"
+`;
+
+exports[`.toHaveProperty() {pass: true} expect([Function memoized]).toHaveProperty('memo', []) 1`] = `
+"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>[Function memoized]</>
+Not to have a nested property:
+  <green>\\"memo\\"</>
+With a value of:
+  <green>[]</>
+"
+`;
+
 exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1') 1`] = `
 "<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>)</>
 
@@ -3234,6 +3323,18 @@ With a value of:
 "
 `;
 
+exports[`.toHaveProperty() {pass: true} expect({"nodeName": "DIV"}).toHaveProperty('nodeType', 1) 1`] = `
+"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>{\\"nodeName\\": \\"DIV\\"}</>
+Not to have a nested property:
+  <green>\\"nodeType\\"</>
+With a value of:
+  <green>1</>
+"
+`;
+
 exports[`.toHaveProperty() {pass: true} expect({"property": 1}).toHaveProperty('property', 1) 1`] = `
 "<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
 
@@ -3246,6 +3347,42 @@ With a value of:
 "
 `;
 
+exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('a', undefined) 1`] = `
+"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>{\\"val\\": true}</>
+Not to have a nested property:
+  <green>\\"a\\"</>
+With a value of:
+  <green>undefined</>
+"
+`;
+
+exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('c', "c") 1`] = `
+"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>{\\"val\\": true}</>
+Not to have a nested property:
+  <green>\\"c\\"</>
+With a value of:
+  <green>\\"c\\"</>
+"
+`;
+
+exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('val', true) 1`] = `
+"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>{\\"val\\": true}</>
+Not to have a nested property:
+  <green>\\"val\\"</>
+With a value of:
+  <green>true</>
+"
+`;
+
 exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('a', undefined) 1`] = `
 "<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
 
@@ -3270,6 +3407,18 @@ With a value of:
 "
 `;
 
+exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('setter', undefined) 1`] = `
+"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</><dim>, </><green>value</><dim>)</>
+
+Expected the object:
+  <red>{}</>
+Not to have a nested property:
+  <green>\\"setter\\"</>
+With a value of:
+  <green>undefined</>
+"
+`;
+
 exports[`.toMatch() {pass: true} expect(Foo bar).toMatch(/^foo/i) 1`] = `
 "<dim>expect(</><red>received</><dim>).not.toMatch(</><green>expected</><dim>)</>
 
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();
       }