Skip to content

Commit

Permalink
fix: Support class instances in .toHaveProperty() matcher (#5367)
Browse files Browse the repository at this point in the history
* fix: Support class instances in .toHaveProperty() matcher

* update changelog
  • Loading branch information
thymikee authored and cpojer committed Jan 22, 2018
1 parent 139f976 commit cfce36c
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 24 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
58 changes: 58 additions & 0 deletions packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2778,6 +2778,23 @@ To have a nested property:
"
`;

exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "a") 1`] = `
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</>, <green>value</><dim>)</>

Expected the object:
<red>{}</>
To have a nested property:
<green>\\"a\\"</>
With a value of:
<green>\\"a\\"</>
Received:
<red>undefined</>

Difference:

Comparing two different types of values. Expected <green>string</> but received <red>undefined</>."
`;

exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "test") 1`] = `
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</>, <green>value</><dim>)</>

Expand All @@ -2790,6 +2807,23 @@ With a value of:
"
`;

exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('b', undefined) 1`] = `
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</>, <green>value</><dim>)</>

Expected the object:
<red>{}</>
To have a nested property:
<green>\\"b\\"</>
With a value of:
<green>undefined</>
Received:
<red>\\"b\\"</>

Difference:

Comparing two different types of values. Expected <green>undefined</> but received <red>string</>."
`;

exports[`.toHaveProperty() {pass: false} expect(1).toHaveProperty('a.b.c') 1`] = `
"<dim>expect(</><red>object</><dim>).toHaveProperty(</><green>path</><dim>)</>

Expand Down Expand Up @@ -2968,6 +3002,30 @@ With a value of:
"
`;

exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('a', undefined) 1`] = `
"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</>, <green>value</><dim>)</>

Expected the object:
<red>{}</>
Not to have a nested property:
<green>\\"a\\"</>
With a value of:
<green>undefined</>
"
`;

exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('b', "b") 1`] = `
"<dim>expect(</><red>object</><dim>).not.toHaveProperty(</><green>path</>, <green>value</><dim>)</>

Expected the object:
<red>{}</>
Not to have a nested property:
<green>\\"b\\"</>
With a value of:
<green>\\"b\\"</>
"
`;

exports[`.toMatch() {pass: true} expect(Foo bar).toMatch(/^foo/i) 1`] = `
"<dim>expect(</><red>received</><dim>).not.toMatch(</><green>expected</><dim>)</>

Expand Down
13 changes: 13 additions & 0 deletions packages/expect/src/__tests__/matchers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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,
Expand All @@ -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,
Expand Down
27 changes: 26 additions & 1 deletion packages/expect/src/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
});
});
});
Expand Down
48 changes: 27 additions & 21 deletions packages/expect/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down

0 comments on commit cfce36c

Please sign in to comment.