Skip to content

Commit 5c6480b

Browse files
authored
fix: toMatchObject works with super getters (#10381)
1 parent a8addf8 commit 5c6480b

File tree

5 files changed

+71
-64
lines changed

5 files changed

+71
-64
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Fixes
66

7+
- `[expect]` Fix `toMatchObject` to work with inherited class getters ([#10381](https://github.com/facebook/jest/pull/10381))
8+
79
### Chore & Maintenance
810

911
### Performance

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3988,6 +3988,18 @@ exports[`toMatchObject() {pass: false} expect({"a": "b"}).toMatchObject({"c": "d
39883988
<d> }</>
39893989
`;
39903990

3991+
exports[`toMatchObject() {pass: false} expect({"a": "b"}).toMatchObject({"toString": Any<Function>}) 1`] = `
3992+
<d>expect(</><r>received</><d>).</>toMatchObject<d>(</><g>expected</><d>)</>
3993+
3994+
<g>- Expected - 0</>
3995+
<r>+ Received + 1</>
3996+
3997+
<d> Object {</>
3998+
<r>+ "a": "b",</>
3999+
<d> "toString": Any<Function>,</>
4000+
<d> }</>
4001+
`;
4002+
39914003
exports[`toMatchObject() {pass: false} expect({"a": [{"a": "a", "b": "b"}]}).toMatchObject({"a": [{"a": "c"}]}) 1`] = `
39924004
<d>expect(</><r>received</><d>).</>toMatchObject<d>(</><g>expected</><d>)</>
39934005

@@ -4246,6 +4258,13 @@ Expected: not <g>{"t": {"x": {"r": "r"}}}</>
42464258
Received: <r>{"a": "b", "t": {"x": {"r": "r"}, "z": "z"}}</>
42474259
`;
42484260

4261+
exports[`toMatchObject() {pass: true} expect({"a": "b", "toString": [Function toString]}).toMatchObject({"toString": Any<Function>}) 1`] = `
4262+
<d>expect(</><r>received</><d>).</>not<d>.</>toMatchObject<d>(</><g>expected</><d>)</>
4263+
4264+
Expected: not <g>{"toString": Any<Function>}</>
4265+
Received: <r>{"a": "b", "toString": [Function toString]}</>
4266+
`;
4267+
42494268
exports[`toMatchObject() {pass: true} expect({"a": "b"}).toMatchObject({"a": "b"}) 1`] = `
42504269
<d>expect(</><r>received</><d>).</>not<d>.</>toMatchObject<d>(</><g>expected</><d>)</>
42514270

@@ -4314,13 +4333,27 @@ exports[`toMatchObject() {pass: true} expect({"a": undefined}).toMatchObject({"a
43144333
Expected: not <g>{"a": undefined}</>
43154334
`;
43164335

4336+
exports[`toMatchObject() {pass: true} expect({}).toMatchObject({"a": undefined, "b": "b", "c": "c"}) 1`] = `
4337+
<d>expect(</><r>received</><d>).</>not<d>.</>toMatchObject<d>(</><g>expected</><d>)</>
4338+
4339+
Expected: not <g>{"a": undefined, "b": "b", "c": "c"}</>
4340+
Received: <r>{}</>
4341+
`;
4342+
43174343
exports[`toMatchObject() {pass: true} expect({}).toMatchObject({"a": undefined, "b": "b"}) 1`] = `
43184344
<d>expect(</><r>received</><d>).</>not<d>.</>toMatchObject<d>(</><g>expected</><d>)</>
43194345

43204346
Expected: not <g>{"a": undefined, "b": "b"}</>
43214347
Received: <r>{}</>
43224348
`;
43234349

4350+
exports[`toMatchObject() {pass: true} expect({}).toMatchObject({"d": 4}) 1`] = `
4351+
<d>expect(</><r>received</><d>).</>not<d>.</>toMatchObject<d>(</><g>expected</><d>)</>
4352+
4353+
Expected: not <g>{"d": 4}</>
4354+
Received: <r>{}</>
4355+
`;
4356+
43244357
exports[`toMatchObject() {pass: true} expect(2015-11-30T00:00:00.000Z).toMatchObject(2015-11-30T00:00:00.000Z) 1`] = `
43254358
<d>expect(</><r>received</><d>).</>not<d>.</>toMatchObject<d>(</><g>expected</><d>)</>
43264359

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,22 @@ describe('toMatchObject()', () => {
19581958
}
19591959
}
19601960

1961+
class Sub extends Foo {
1962+
get c() {
1963+
return 'c';
1964+
}
1965+
}
1966+
1967+
const withDefineProperty = (obj, key, val) => {
1968+
Object.defineProperty(obj, key, {
1969+
get() {
1970+
return val;
1971+
},
1972+
});
1973+
1974+
return obj;
1975+
};
1976+
19611977
const testNotToMatchSnapshots = tuples => {
19621978
tuples.forEach(([n1, n2]) => {
19631979
it(`{pass: true} expect(${stringify(n1)}).toMatchObject(${stringify(
@@ -2082,6 +2098,12 @@ describe('toMatchObject()', () => {
20822098
{a: 'b', c: 'd', [Symbol.for('jest')]: 'jest'},
20832099
{a: 'b', c: 'd', [Symbol.for('jest')]: 'jest'},
20842100
],
2101+
// These snapshots will show {} as the object because the properties
2102+
// are not enumerable. We will need to somehow make the serialization of
2103+
// these keys a little smarter before reporting accurately.
2104+
[new Sub(), {a: undefined, b: 'b', c: 'c'}],
2105+
[withDefineProperty(new Sub(), 'd', 4), {d: 4}],
2106+
[{a: 'b', toString() {}}, {toString: jestExpect.any(Function)}],
20852107
]);
20862108

20872109
testToMatchSnapshots([
@@ -2129,6 +2151,7 @@ describe('toMatchObject()', () => {
21292151
{a: 'b', c: 'd', [Symbol.for('jest')]: 'jest'},
21302152
{a: 'c', [Symbol.for('jest')]: expect.any(String)},
21312153
],
2154+
[{a: 'b'}, {toString: jestExpect.any(Function)}],
21322155
]);
21332156

21342157
[

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

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
emptyObject,
1212
getObjectSubset,
1313
getPath,
14-
hasOwnProperty,
1514
iterableEquality,
1615
subsetEquality,
1716
} from '../utils';
@@ -107,46 +106,6 @@ describe('getPath()', () => {
107106
});
108107
});
109108

110-
describe('hasOwnProperty', () => {
111-
it('does inherit getter from class', () => {
112-
class MyClass {
113-
get key() {
114-
return 'value';
115-
}
116-
}
117-
expect(hasOwnProperty(new MyClass(), 'key')).toBe(true);
118-
});
119-
120-
it('does not inherit setter from class', () => {
121-
class MyClass {
122-
set key(_value: unknown) {}
123-
}
124-
expect(hasOwnProperty(new MyClass(), 'key')).toBe(false);
125-
});
126-
127-
it('does not inherit method from class', () => {
128-
class MyClass {
129-
key() {}
130-
}
131-
expect(hasOwnProperty(new MyClass(), 'key')).toBe(false);
132-
});
133-
134-
it('does not inherit property from constructor prototype', () => {
135-
function MyClass() {}
136-
MyClass.prototype.key = 'value';
137-
// @ts-expect-error
138-
expect(hasOwnProperty(new MyClass(), 'key')).toBe(false);
139-
});
140-
141-
it('does not inherit __proto__ getter from Object', () => {
142-
expect(hasOwnProperty({}, '__proto__')).toBe(false);
143-
});
144-
145-
it('does not inherit toString method from Object', () => {
146-
expect(hasOwnProperty({}, 'toString')).toBe(false);
147-
});
148-
});
149-
150109
describe('getObjectSubset', () => {
151110
[
152111
[{a: 'b', c: 'd'}, {a: 'd'}, {a: 'b'}],

packages/expect/src/utils.ts

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,23 @@ type GetPath = {
2121
value?: unknown;
2222
};
2323

24-
// Return whether object instance inherits getter from its class.
25-
const hasGetterFromConstructor = (object: object, key: string) => {
26-
const constructor = object.constructor;
27-
if (constructor === Object) {
28-
// A literal object has Object as constructor.
29-
// Therefore, it cannot inherit application-specific getters.
30-
// Furthermore, Object has __proto__ getter which is not relevant.
31-
// Array, Boolean, Number, String constructors don’t have any getters.
32-
return false;
33-
}
34-
if (typeof constructor !== 'function') {
35-
// Object.create(null) constructs object with no constructor nor prototype.
36-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Custom_and_Null_objects
24+
/**
25+
* Checks if `hasOwnProperty(object, key)` up the prototype chain, stopping at `Object.prototype`.
26+
*/
27+
const hasPropertyInObject = (object: object, key: string): boolean => {
28+
const shouldTerminate =
29+
!object || typeof object !== 'object' || object === Object.prototype;
30+
31+
if (shouldTerminate) {
3732
return false;
3833
}
3934

40-
const descriptor = Object.getOwnPropertyDescriptor(
41-
constructor.prototype,
42-
key,
35+
return (
36+
Object.prototype.hasOwnProperty.call(object, key) ||
37+
hasPropertyInObject(Object.getPrototypeOf(object), key)
4338
);
44-
return descriptor !== undefined && typeof descriptor.get === 'function';
4539
};
4640

47-
export const hasOwnProperty = (object: object, key: string): boolean =>
48-
Object.prototype.hasOwnProperty.call(object, key) ||
49-
hasGetterFromConstructor(object, key);
50-
5141
export const getPath = (
5242
object: Record<string, any>,
5343
propertyPath: string | Array<string>,
@@ -129,7 +119,7 @@ export const getObjectSubset = (
129119
seenReferences.set(object, trimmed);
130120

131121
Object.keys(object)
132-
.filter(key => hasOwnProperty(subset, key))
122+
.filter(key => hasPropertyInObject(subset, key))
133123
.forEach(key => {
134124
trimmed[key] = seenReferences.has(object[key])
135125
? seenReferences.get(object[key])
@@ -299,7 +289,7 @@ export const subsetEquality = (
299289
}
300290
const result =
301291
object != null &&
302-
hasOwnProperty(object, key) &&
292+
hasPropertyInObject(object, key) &&
303293
equals(object[key], subset[key], [
304294
iterableEquality,
305295
subsetEqualityWithContext(seenReferences),

0 commit comments

Comments
 (0)