diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea43efcfcf5..89263d939ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,15 @@ ### features -* `[jest-mock]` Add util methods to create async functions. - ([#5318](https://github.com/facebook/jest/pull/5318)) +* `[jest-mock]` Add util methods to create async functions. + ([#5318](https://github.com/facebook/jest/pull/5318)) ### Fixes * `[jest]` Add `import-local` to `jest` package. ([#5353](https://github.com/facebook/jest/pull/5353)) +* `[expect]` Support class instances in `.toHaveProperty()` matcher. + ([#5367](https://github.com/facebook/jest/pull/5367)) ## jest 22.1.4 diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index f9492e48f432..19415cdb3828 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -2778,6 +2778,23 @@ To have a nested property: " `; +exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "a") 1`] = ` +"expect(object).toHaveProperty(path, value) + +Expected the object: + {} +To have a nested property: + \\"a\\" +With a value of: + \\"a\\" +Received: + undefined + +Difference: + + Comparing two different types of values. Expected string but received undefined." +`; + exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "test") 1`] = ` "expect(object).toHaveProperty(path, value) @@ -2790,6 +2807,23 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('b', undefined) 1`] = ` +"expect(object).toHaveProperty(path, value) + +Expected the object: + {} +To have a nested property: + \\"b\\" +With a value of: + undefined +Received: + \\"b\\" + +Difference: + + Comparing two different types of values. Expected undefined but received string." +`; + exports[`.toHaveProperty() {pass: false} expect(1).toHaveProperty('a.b.c') 1`] = ` "expect(object).toHaveProperty(path) @@ -2968,6 +3002,30 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('a', undefined) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {} +Not to have a nested property: + \\"a\\" +With a value of: + undefined +" +`; + +exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('b', "b") 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {} +Not to have a nested property: + \\"b\\" +With a value of: + \\"b\\" +" +`; + 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 bbce3f79d143..278497721087 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -749,6 +749,15 @@ describe('.toHaveLength', () => { }); describe('.toHaveProperty()', () => { + class Foo { + get a() { + return undefined; + } + get b() { + return 'b'; + } + } + [ [{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 1], [{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 1], @@ -758,6 +767,8 @@ describe('.toHaveProperty()', () => { [{a: {b: undefined}}, 'a.b', undefined], [{a: {b: {c: 5}}}, 'a.b', {c: 5}], [Object.assign(Object.create(null), {property: 1}), 'property', 1], + [new Foo(), 'a', undefined], + [new Foo(), 'b', 'b'], ].forEach(([obj, keyPath, value]) => { test(`{pass: true} expect(${stringify( obj, @@ -782,6 +793,8 @@ describe('.toHaveProperty()', () => { [1, 'a.b.c', 'test'], ['abc', 'a.b.c', {a: 5}], [{a: {b: {c: 5}}}, 'a.b', {c: 4}], + [new Foo(), 'a', 'a'], + [new Foo(), 'b', undefined], ].forEach(([obj, keyPath, value]) => { test(`{pass: false} expect(${stringify( obj, diff --git a/packages/expect/src/__tests__/utils.test.js b/packages/expect/src/__tests__/utils.test.js index c570b43f02ec..9e850b325a70 100644 --- a/packages/expect/src/__tests__/utils.test.js +++ b/packages/expect/src/__tests__/utils.test.js @@ -46,6 +46,30 @@ describe('getPath()', () => { }); }); + test('property is a getter on class instance', () => { + class A { + get a() { + return 'a'; + } + get b() { + return {c: 'c'}; + } + } + + expect(getPath(new A(), 'a')).toEqual({ + hasEndProp: true, + lastTraversedObject: new A(), + traversedPath: ['a'], + value: 'a', + }); + expect(getPath(new A(), 'b.c')).toEqual({ + hasEndProp: true, + lastTraversedObject: {c: 'c'}, + traversedPath: ['b', 'c'], + value: 'c', + }); + }); + test('path breaks', () => { expect(getPath({a: {}}, 'a.b.c')).toEqual({ hasEndProp: false, @@ -55,11 +79,12 @@ describe('getPath()', () => { }); }); - test('empry object at the end', () => { + test('empty object at the end', () => { expect(getPath({a: {b: {c: {}}}}, 'a.b.c.d')).toEqual({ hasEndProp: false, lastTraversedObject: {}, traversedPath: ['a', 'b', 'c'], + value: undefined, }); }); }); diff --git a/packages/expect/src/utils.js b/packages/expect/src/utils.js index 45049d527358..7d1859b95c65 100644 --- a/packages/expect/src/utils.js +++ b/packages/expect/src/utils.js @@ -17,7 +17,8 @@ type GetPath = { }; export const hasOwnProperty = (object: Object, value: string) => - Object.prototype.hasOwnProperty.call(object, value); + Object.prototype.hasOwnProperty.call(object, value) || + Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); export const getPath = ( object: Object, @@ -27,40 +28,45 @@ export const getPath = ( propertyPath = propertyPath.split('.'); } - const lastProp = propertyPath.length === 1; - if (propertyPath.length) { + const lastProp = propertyPath.length === 1; const prop = propertyPath[0]; const newObject = object[prop]; + if (!lastProp && (newObject === null || newObject === undefined)) { // This is not the last prop in the chain. If we keep recursing it will // hit a `can't access property X of undefined | null`. At this point we - // know that the chain broken and we return right away. + // know that the chain has broken and we can return right away. return { hasEndProp: false, lastTraversedObject: object, traversedPath: [], }; - } else { - const result = getPath(newObject, propertyPath.slice(1)); - result.lastTraversedObject || (result.lastTraversedObject = object); - result.traversedPath.unshift(prop); - if (propertyPath.length === 1) { - result.hasEndProp = hasOwnProperty(object, prop); - if (!result.hasEndProp) { - delete result.value; - result.traversedPath.shift(); - } + } + + const result = getPath(newObject, propertyPath.slice(1)); + + if (result.lastTraversedObject === null) { + result.lastTraversedObject = object; + } + + result.traversedPath.unshift(prop); + + if (lastProp) { + result.hasEndProp = hasOwnProperty(object, prop); + if (!result.hasEndProp) { + result.traversedPath.shift(); } - return result; } - } else { - return { - lastTraversedObject: null, - traversedPath: [], - value: object, - }; + + return result; } + + return { + lastTraversedObject: null, + traversedPath: [], + value: object, + }; }; // Strip properties from object that are not present in the subset. Useful for