Skip to content

Commit 5cd75f4

Browse files
authored
Enhancing the toHaveProperty matcher to support array selection (#12092)
1 parent f8c6e75 commit 5cd75f4

6 files changed

Lines changed: 73 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- `[jest-core]` Add support for `testResultsProcessor` written in ESM ([#12006](https://github.com/facebook/jest/pull/12006))
66
- `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys ([#11992](https://github.com/facebook/jest/pull/11992))
7+
- `[expect]` Enhancing the `toHaveProperty` matcher to support array selection ([#12092](https://github.com/facebook/jest/pull/12092))
78

89
### Fixes
910

docs/ExpectAPI.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,16 @@ const houseForSale = {
895895
wallColor: 'white',
896896
'nice.oven': true,
897897
},
898+
livingroom: {
899+
amenities: [
900+
{
901+
couch: [
902+
['large', {dimensions: [20, 20]}],
903+
['small', {dimensions: [10, 10]}],
904+
],
905+
},
906+
],
907+
},
898908
'ceiling.height': 2,
899909
};
900910

@@ -922,6 +932,10 @@ test('this house has my desired features', () => {
922932
['oven', 'stove', 'washer'],
923933
);
924934
expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven');
935+
expect(houseForSale).toHaveProperty(
936+
'livingroom.amenities[0].couch[0][1].dimensions[0]',
937+
20,
938+
);
925939
expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven']);
926940
expect(houseForSale).not.toHaveProperty(['kitchen', 'open']);
927941

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3334,6 +3334,15 @@ Expected value: <g>1</>
33343334
Received value: <r>{"c": {"d": 1}}</>
33353335
`;
33363336

3337+
exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {}}}}).toHaveProperty('.a.b.c') 1`] = `
3338+
<d>expect(</><r>received</><d>).</>toHaveProperty<d>(</><g>path</><d>)</>
3339+
3340+
Expected path: <g>".a.b.c"</>
3341+
Received path: <r>[]</>
3342+
3343+
Received value: <r>{"a": {"b": {"c": {}}}}</>
3344+
`;
3345+
33373346
exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {}}}}).toHaveProperty('a.b.c.d') 1`] = `
33383347
<d>expect(</><r>received</><d>).</>toHaveProperty<d>(</><g>path</><d>)</>
33393348

@@ -3551,6 +3560,30 @@ Expected path: <g>"memo"</>
35513560
Expected value: not <g>[]</>
35523561
`;
35533562

3563+
exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [[{"c": [{"d": 1}]}]]}}).toHaveProperty('a.b[0][0].c[0].d', 1) 1`] = `
3564+
<d>expect(</><r>received</><d>).</>not<d>.</>toHaveProperty<d>(</><g>path</><d>, </><g>value</><d>)</>
3565+
3566+
Expected path: <g>"a.b[0][0].c[0].d"</>
3567+
3568+
Expected value: not <g>1</>
3569+
`;
3570+
3571+
exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [{"c": [{"d": 1}]}]}}).toHaveProperty('a.b[0].c[0].d', 1) 1`] = `
3572+
<d>expect(</><r>received</><d>).</>not<d>.</>toHaveProperty<d>(</><g>path</><d>, </><g>value</><d>)</>
3573+
3574+
Expected path: <g>"a.b[0].c[0].d"</>
3575+
3576+
Expected value: not <g>1</>
3577+
`;
3578+
3579+
exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [{"c": {"d": [{"e": 1}, {"f": 2}]}}]}}).toHaveProperty('a.b[0].c.d[1].f', 2) 1`] = `
3580+
<d>expect(</><r>received</><d>).</>not<d>.</>toHaveProperty<d>(</><g>path</><d>, </><g>value</><d>)</>
3581+
3582+
Expected path: <g>"a.b[0].c.d[1].f"</>
3583+
3584+
Expected value: not <g>2</>
3585+
`;
3586+
35543587
exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1') 1`] = `
35553588
<d>expect(</><r>received</><d>).</>not<d>.</>toHaveProperty<d>(</><g>path</><d>)</>
35563589

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,6 +1885,9 @@ describe('.toHaveProperty()', () => {
18851885
[{a: {b: undefined}}, 'a.b', undefined],
18861886
[{a: {}}, 'a.b', undefined], // delete for breaking change in future major
18871887
[{a: {b: {c: 5}}}, 'a.b', {c: 5}],
1888+
[{a: {b: [{c: [{d: 1}]}]}}, 'a.b[0].c[0].d', 1],
1889+
[{a: {b: [{c: {d: [{e: 1}, {f: 2}]}}]}}, 'a.b[0].c.d[1].f', 2],
1890+
[{a: {b: [[{c: [{d: 1}]}]]}}, 'a.b[0][0].c[0].d', 1],
18881891
[Object.assign(Object.create(null), {property: 1}), 'property', 1],
18891892
[new Foo(), 'a', undefined],
18901893
[new Foo(), 'b', 'b'],
@@ -1955,6 +1958,7 @@ describe('.toHaveProperty()', () => {
19551958

19561959
[
19571960
[{a: {b: {c: {}}}}, 'a.b.c.d'],
1961+
[{a: {b: {c: {}}}}, '.a.b.c'],
19581962
[{a: 1}, 'a.b.c.d'],
19591963
[{}, 'a'],
19601964
[1, 'a.b.c'],

packages/expect/src/matchers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
getObjectSubset,
4545
getPath,
4646
iterableEquality,
47+
pathAsArray,
4748
sparseArrayEquality,
4849
subsetEquality,
4950
typeEquality,
@@ -704,7 +705,7 @@ const matchers: MatchersObject = {
704705

705706
const expectedPathLength =
706707
typeof expectedPath === 'string'
707-
? expectedPath.split('.').length
708+
? pathAsArray(expectedPath).length
708709
: expectedPath.length;
709710

710711
if (expectedPathType === 'array' && expectedPathLength === 0) {

packages/expect/src/utils.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const getPath = (
4545
propertyPath: string | Array<string>,
4646
): GetPath => {
4747
if (!Array.isArray(propertyPath)) {
48-
propertyPath = (propertyPath as string).split('.');
48+
propertyPath = pathAsArray(propertyPath);
4949
}
5050

5151
if (propertyPath.length) {
@@ -372,6 +372,24 @@ export const partition = <T>(
372372
return result;
373373
};
374374

375+
export const pathAsArray = (propertyPath: string): Array<any> => {
376+
// will match everything that's not a dot or a bracket, and "" for consecutive dots.
377+
const pattern = RegExp('[^.[\\]]+|(?=(?:\\.)(?:\\.|$))', 'g');
378+
const properties: Array<string> = [];
379+
380+
// Because the regex won't match a dot in the beginning of the path, if present.
381+
if (propertyPath[0] === '.') {
382+
properties.push('');
383+
}
384+
385+
propertyPath.replace(pattern, match => {
386+
properties.push(match);
387+
return match;
388+
});
389+
390+
return properties;
391+
};
392+
375393
// Copied from https://github.com/graingert/angular.js/blob/a43574052e9775cbc1d7dd8a086752c979b0f020/src/Angular.js#L685-L693
376394
export const isError = (value: unknown): value is Error => {
377395
switch (Object.prototype.toString.call(value)) {

0 commit comments

Comments
 (0)