Skip to content

Commit cfce36c

Browse files
thymikeecpojer
authored andcommitted
fix: Support class instances in .toHaveProperty() matcher (#5367)
* fix: Support class instances in .toHaveProperty() matcher * update changelog
1 parent 139f976 commit cfce36c

File tree

5 files changed

+128
-24
lines changed

5 files changed

+128
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
### features
44

5-
* `[jest-mock]` Add util methods to create async functions.
6-
([#5318](https://github.com/facebook/jest/pull/5318))
5+
* `[jest-mock]` Add util methods to create async functions.
6+
([#5318](https://github.com/facebook/jest/pull/5318))
77

88
### Fixes
99

1010
* `[jest]` Add `import-local` to `jest` package.
1111
([#5353](https://github.com/facebook/jest/pull/5353))
12+
* `[expect]` Support class instances in `.toHaveProperty()` matcher.
13+
([#5367](https://github.com/facebook/jest/pull/5367))
1214

1315
## jest 22.1.4
1416

packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2778,6 +2778,23 @@ To have a nested property:
27782778
"
27792779
`;
27802780

2781+
exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "a") 1`] = `
2782+
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</>, <green>value</><dim>)</>
2783+
2784+
Expected the object:
2785+
<red>{}</>
2786+
To have a nested property:
2787+
<green>\\"a\\"</>
2788+
With a value of:
2789+
<green>\\"a\\"</>
2790+
Received:
2791+
<red>undefined</>
2792+
2793+
Difference:
2794+
2795+
Comparing two different types of values. Expected <green>string</> but received <red>undefined</>."
2796+
`;
2797+
27812798
exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "test") 1`] = `
27822799
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</>, <green>value</><dim>)</>
27832800

@@ -2790,6 +2807,23 @@ With a value of:
27902807
"
27912808
`;
27922809

2810+
exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('b', undefined) 1`] = `
2811+
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</>, <green>value</><dim>)</>
2812+
2813+
Expected the object:
2814+
<red>{}</>
2815+
To have a nested property:
2816+
<green>\\"b\\"</>
2817+
With a value of:
2818+
<green>undefined</>
2819+
Received:
2820+
<red>\\"b\\"</>
2821+
2822+
Difference:
2823+
2824+
Comparing two different types of values. Expected <green>undefined</> but received <red>string</>."
2825+
`;
2826+
27932827
exports[`.toHaveProperty() {pass: false} expect(1).toHaveProperty('a.b.c') 1`] = `
27942828
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>
27952829

@@ -2968,6 +3002,30 @@ With a value of:
29683002
"
29693003
`;
29703004

3005+
exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('a', undefined) 1`] = `
3006+
"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</>, <green>value</><dim>)</>
3007+
3008+
Expected the object:
3009+
<red>{}</>
3010+
Not to have a nested property:
3011+
<green>\\"a\\"</>
3012+
With a value of:
3013+
<green>undefined</>
3014+
"
3015+
`;
3016+
3017+
exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('b', "b") 1`] = `
3018+
"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</>, <green>value</><dim>)</>
3019+
3020+
Expected the object:
3021+
<red>{}</>
3022+
Not to have a nested property:
3023+
<green>\\"b\\"</>
3024+
With a value of:
3025+
<green>\\"b\\"</>
3026+
"
3027+
`;
3028+
29713029
exports[`.toMatch() {pass: true} expect(Foo bar).toMatch(/^foo/i) 1`] = `
29723030
"<dim>expect(</><red>received</><dim>).not.toMatch(</><green>expected</><dim>)</>
29733031

packages/expect/src/__tests__/matchers.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,15 @@ describe('.toHaveLength', () => {
749749
});
750750

751751
describe('.toHaveProperty()', () => {
752+
class Foo {
753+
get a() {
754+
return undefined;
755+
}
756+
get b() {
757+
return 'b';
758+
}
759+
}
760+
752761
[
753762
[{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 1],
754763
[{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 1],
@@ -758,6 +767,8 @@ describe('.toHaveProperty()', () => {
758767
[{a: {b: undefined}}, 'a.b', undefined],
759768
[{a: {b: {c: 5}}}, 'a.b', {c: 5}],
760769
[Object.assign(Object.create(null), {property: 1}), 'property', 1],
770+
[new Foo(), 'a', undefined],
771+
[new Foo(), 'b', 'b'],
761772
].forEach(([obj, keyPath, value]) => {
762773
test(`{pass: true} expect(${stringify(
763774
obj,
@@ -782,6 +793,8 @@ describe('.toHaveProperty()', () => {
782793
[1, 'a.b.c', 'test'],
783794
['abc', 'a.b.c', {a: 5}],
784795
[{a: {b: {c: 5}}}, 'a.b', {c: 4}],
796+
[new Foo(), 'a', 'a'],
797+
[new Foo(), 'b', undefined],
785798
].forEach(([obj, keyPath, value]) => {
786799
test(`{pass: false} expect(${stringify(
787800
obj,

packages/expect/src/__tests__/utils.test.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ describe('getPath()', () => {
4646
});
4747
});
4848

49+
test('property is a getter on class instance', () => {
50+
class A {
51+
get a() {
52+
return 'a';
53+
}
54+
get b() {
55+
return {c: 'c'};
56+
}
57+
}
58+
59+
expect(getPath(new A(), 'a')).toEqual({
60+
hasEndProp: true,
61+
lastTraversedObject: new A(),
62+
traversedPath: ['a'],
63+
value: 'a',
64+
});
65+
expect(getPath(new A(), 'b.c')).toEqual({
66+
hasEndProp: true,
67+
lastTraversedObject: {c: 'c'},
68+
traversedPath: ['b', 'c'],
69+
value: 'c',
70+
});
71+
});
72+
4973
test('path breaks', () => {
5074
expect(getPath({a: {}}, 'a.b.c')).toEqual({
5175
hasEndProp: false,
@@ -55,11 +79,12 @@ describe('getPath()', () => {
5579
});
5680
});
5781

58-
test('empry object at the end', () => {
82+
test('empty object at the end', () => {
5983
expect(getPath({a: {b: {c: {}}}}, 'a.b.c.d')).toEqual({
6084
hasEndProp: false,
6185
lastTraversedObject: {},
6286
traversedPath: ['a', 'b', 'c'],
87+
value: undefined,
6388
});
6489
});
6590
});

packages/expect/src/utils.js

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type GetPath = {
1717
};
1818

1919
export const hasOwnProperty = (object: Object, value: string) =>
20-
Object.prototype.hasOwnProperty.call(object, value);
20+
Object.prototype.hasOwnProperty.call(object, value) ||
21+
Object.prototype.hasOwnProperty.call(object.constructor.prototype, value);
2122

2223
export const getPath = (
2324
object: Object,
@@ -27,40 +28,45 @@ export const getPath = (
2728
propertyPath = propertyPath.split('.');
2829
}
2930

30-
const lastProp = propertyPath.length === 1;
31-
3231
if (propertyPath.length) {
32+
const lastProp = propertyPath.length === 1;
3333
const prop = propertyPath[0];
3434
const newObject = object[prop];
35+
3536
if (!lastProp && (newObject === null || newObject === undefined)) {
3637
// This is not the last prop in the chain. If we keep recursing it will
3738
// hit a `can't access property X of undefined | null`. At this point we
38-
// know that the chain broken and we return right away.
39+
// know that the chain has broken and we can return right away.
3940
return {
4041
hasEndProp: false,
4142
lastTraversedObject: object,
4243
traversedPath: [],
4344
};
44-
} else {
45-
const result = getPath(newObject, propertyPath.slice(1));
46-
result.lastTraversedObject || (result.lastTraversedObject = object);
47-
result.traversedPath.unshift(prop);
48-
if (propertyPath.length === 1) {
49-
result.hasEndProp = hasOwnProperty(object, prop);
50-
if (!result.hasEndProp) {
51-
delete result.value;
52-
result.traversedPath.shift();
53-
}
45+
}
46+
47+
const result = getPath(newObject, propertyPath.slice(1));
48+
49+
if (result.lastTraversedObject === null) {
50+
result.lastTraversedObject = object;
51+
}
52+
53+
result.traversedPath.unshift(prop);
54+
55+
if (lastProp) {
56+
result.hasEndProp = hasOwnProperty(object, prop);
57+
if (!result.hasEndProp) {
58+
result.traversedPath.shift();
5459
}
55-
return result;
5660
}
57-
} else {
58-
return {
59-
lastTraversedObject: null,
60-
traversedPath: [],
61-
value: object,
62-
};
61+
62+
return result;
6363
}
64+
65+
return {
66+
lastTraversedObject: null,
67+
traversedPath: [],
68+
value: object,
69+
};
6470
};
6571

6672
// Strip properties from object that are not present in the subset. Useful for

0 commit comments

Comments
 (0)