Skip to content

Commit 0c7ec75

Browse files
authored
Fixed toHaveProperty returning false positives when looking for undefined property (#8923)
1 parent a79a59e commit 0c7ec75

File tree

6 files changed

+28
-43
lines changed

6 files changed

+28
-43
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- `[jest-config]` Pass `moduleTypes` to `ts-node` to enforce CJS when transpiling ([#12397](https://github.com/facebook/jest/pull/12397))
3636
- `[jest-config, jest-haste-map]` Allow searching for tests in `node_modules` by exposing `retainAllFiles` ([#11084](https://github.com/facebook/jest/pull/11084))
3737
- `[jest-environment-jsdom]` Make `jsdom` accessible to extending environments again ([#12232](https://github.com/facebook/jest/pull/12232))
38+
- `[@jest/expect-utils]` [**BREAKING**] Fix false positives when looking for `undefined` prop ([#8923](https://github.com/facebook/jest/pull/8923))
3839
- `[jest-haste-map]` Don't use partial results if file crawl errors ([#12420](https://github.com/facebook/jest/pull/12420))
3940
- `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125))
4041
- `[jest-matcher-utils]` Pass maxWidth to `pretty-format` to avoid printing every element in arrays by default ([#12402](https://github.com/facebook/jest/pull/12402))

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

+8
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import {
1919
describe('getPath()', () => {
2020
test('property exists', () => {
2121
expect(getPath({a: {b: {c: 5}}}, 'a.b.c')).toEqual({
22+
endPropIsDefined: true,
2223
hasEndProp: true,
2324
lastTraversedObject: {c: 5},
2425
traversedPath: ['a', 'b', 'c'],
2526
value: 5,
2627
});
2728

2829
expect(getPath({a: {b: {c: {d: 1}}}}, 'a.b.c.d')).toEqual({
30+
endPropIsDefined: true,
2931
hasEndProp: true,
3032
lastTraversedObject: {d: 1},
3133
traversedPath: ['a', 'b', 'c', 'd'],
@@ -35,6 +37,7 @@ describe('getPath()', () => {
3537

3638
test('property doesnt exist', () => {
3739
expect(getPath({a: {b: {}}}, 'a.b.c')).toEqual({
40+
endPropIsDefined: false,
3841
hasEndProp: false,
3942
lastTraversedObject: {},
4043
traversedPath: ['a', 'b'],
@@ -44,6 +47,7 @@ describe('getPath()', () => {
4447

4548
test('property exist but undefined', () => {
4649
expect(getPath({a: {b: {c: undefined}}}, 'a.b.c')).toEqual({
50+
endPropIsDefined: true,
4751
hasEndProp: true,
4852
lastTraversedObject: {c: undefined},
4953
traversedPath: ['a', 'b', 'c'],
@@ -62,12 +66,14 @@ describe('getPath()', () => {
6266
}
6367

6468
expect(getPath(new A(), 'a')).toEqual({
69+
endPropIsDefined: true,
6570
hasEndProp: true,
6671
lastTraversedObject: new A(),
6772
traversedPath: ['a'],
6873
value: 'a',
6974
});
7075
expect(getPath(new A(), 'b.c')).toEqual({
76+
endPropIsDefined: true,
7177
hasEndProp: true,
7278
lastTraversedObject: {c: 'c'},
7379
traversedPath: ['b', 'c'],
@@ -81,6 +87,7 @@ describe('getPath()', () => {
8187
A.prototype.a = 'a';
8288

8389
expect(getPath(new A(), 'a')).toEqual({
90+
endPropIsDefined: true,
8491
hasEndProp: true,
8592
lastTraversedObject: new A(),
8693
traversedPath: ['a'],
@@ -99,6 +106,7 @@ describe('getPath()', () => {
99106

100107
test('empty object at the end', () => {
101108
expect(getPath({a: {b: {c: {}}}}, 'a.b.c.d')).toEqual({
109+
endPropIsDefined: false,
102110
hasEndProp: false,
103111
lastTraversedObject: {},
104112
traversedPath: ['a', 'b', 'c'],

packages/expect-utils/src/utils.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616

1717
type GetPath = {
1818
hasEndProp?: boolean;
19+
endPropIsDefined?: boolean;
1920
lastTraversedObject: unknown;
2021
traversedPath: Array<string>;
2122
value?: unknown;
@@ -74,8 +75,8 @@ export const getPath = (
7475
// Does object have the property with an undefined value?
7576
// Although primitive values support bracket notation (above)
7677
// they would throw TypeError for in operator (below).
77-
result.hasEndProp =
78-
newObject !== undefined || (!isPrimitive(object) && prop in object);
78+
result.endPropIsDefined = !isPrimitive(object) && prop in object;
79+
result.hasEndProp = newObject !== undefined || result.endPropIsDefined;
7980

8081
if (!result.hasEndProp) {
8182
result.traversedPath.shift();

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

+10-12
Original file line numberDiff line numberDiff line change
@@ -3392,6 +3392,16 @@ Expected value: <g>undefined</>
33923392
Received value: <r>3</>
33933393
`;
33943394

3395+
exports[`.toHaveProperty() {pass: false} expect({"a": {}}).toHaveProperty('a.b', undefined) 1`] = `
3396+
<d>expect(</><r>received</><d>).</>toHaveProperty<d>(</><g>path</><d>, </><g>value</><d>)</>
3397+
3398+
Expected path: <g>"a.b"</>
3399+
Received path: <r>"a"</>
3400+
3401+
Expected value: <g>undefined</>
3402+
Received value: <r>{}</>
3403+
`;
3404+
33953405
exports[`.toHaveProperty() {pass: false} expect({"a": 1}).toHaveProperty('a.b.c.d') 1`] = `
33963406
<d>expect(</><r>received</><d>).</>toHaveProperty<d>(</><g>path</><d>)</>
33973407

@@ -3680,18 +3690,6 @@ Expected path: <g>"a.b"</>
36803690
Expected value: not <g>undefined</>
36813691
`;
36823692

3683-
exports[`.toHaveProperty() {pass: true} expect({"a": {}}).toHaveProperty('a.b', undefined) 1`] = `
3684-
<d>expect(</><r>received</><d>).</>not<d>.</>toHaveProperty<d>(</><g>path</><d>, </><g>value</><d>)</>
3685-
3686-
Expected path: <g>"a.b"</>
3687-
Received path: <r>"a"</>
3688-
3689-
Expected value: not <g>undefined</>
3690-
Received value: <r>{}</>
3691-
3692-
<d>Because a positive assertion passes for expected value undefined if the property does not exist, this negative assertion fails unless the property does exist and has a defined value</>
3693-
`;
3694-
36953693
exports[`.toHaveProperty() {pass: true} expect({"a": 0}).toHaveProperty('a') 1`] = `
36963694
<d>expect(</><r>received</><d>).</>not<d>.</>toHaveProperty<d>(</><g>path</><d>)</>
36973695

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -1883,7 +1883,6 @@ describe('.toHaveProperty()', () => {
18831883
[{a: {b: [1, 2, 3]}}, ['a', 'b', 1], expect.any(Number)],
18841884
[{a: 0}, 'a', 0],
18851885
[{a: {b: undefined}}, 'a.b', undefined],
1886-
[{a: {}}, 'a.b', undefined], // delete for breaking change in future major
18871886
[{a: {b: {c: 5}}}, 'a.b', {c: 5}],
18881887
[{a: {b: [{c: [{d: 1}]}]}}, 'a.b[0].c[0].d', 1],
18891888
[{a: {b: [{c: {d: [{e: 1}, {f: 2}]}}]}}, 'a.b[0].c.d[1].f', 2],
@@ -1927,7 +1926,7 @@ describe('.toHaveProperty()', () => {
19271926
[{a: {b: {c: 5}}}, 'a.b', {c: 4}],
19281927
[new Foo(), 'a', 'a'],
19291928
[new Foo(), 'b', undefined],
1930-
// [{a: {}}, 'a.b', undefined], // add for breaking change in future major
1929+
[{a: {}}, 'a.b', undefined],
19311930
].forEach(([obj, keyPath, value]) => {
19321931
test(`{pass: false} expect(${stringify(
19331932
obj,

packages/expect/src/matchers.ts

+5-27
Original file line numberDiff line numberDiff line change
@@ -719,37 +719,15 @@ const matchers: MatchersObject = {
719719
}
720720

721721
const result = getPath(received, expectedPath);
722-
const {lastTraversedObject, hasEndProp} = result;
722+
const {lastTraversedObject, endPropIsDefined, hasEndProp, value} = result;
723723
const receivedPath = result.traversedPath;
724724
const hasCompletePath = receivedPath.length === expectedPathLength;
725725
const receivedValue = hasCompletePath ? result.value : lastTraversedObject;
726726

727-
const pass = hasValue
728-
? equals(result.value, expectedValue, [iterableEquality])
729-
: Boolean(hasEndProp); // theoretically undefined if empty path
730-
// Remove type cast if we rewrite getPath as iterative algorithm.
731-
732-
// Delete this unique report if future breaking change
733-
// removes the edge case that expected value undefined
734-
// also matches absence of a property with the key path.
735-
if (pass && !hasCompletePath) {
736-
const message = () =>
737-
matcherHint(matcherName, undefined, expectedArgument, options) +
738-
'\n\n' +
739-
`Expected path: ${printExpected(expectedPath)}\n` +
740-
`Received path: ${printReceived(
741-
expectedPathType === 'array' || receivedPath.length === 0
742-
? receivedPath
743-
: receivedPath.join('.'),
744-
)}\n\n` +
745-
`Expected value: not ${printExpected(expectedValue)}\n` +
746-
`Received value: ${printReceived(receivedValue)}\n\n` +
747-
DIM_COLOR(
748-
'Because a positive assertion passes for expected value undefined if the property does not exist, this negative assertion fails unless the property does exist and has a defined value',
749-
);
750-
751-
return {message, pass};
752-
}
727+
const pass =
728+
hasValue && endPropIsDefined
729+
? equals(value, expectedValue, [iterableEquality])
730+
: Boolean(hasEndProp);
753731

754732
const message = pass
755733
? () =>

0 commit comments

Comments
 (0)