diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4241aefb77..657cbe73bcc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[jest-core]` Add support for `testResultsProcessor` written in ESM ([#12006](https://github.com/facebook/jest/pull/12006)) - `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys ([#11992](https://github.com/facebook/jest/pull/11992)) +- `[expect]` Enhancing the `toHaveProperty` matcher to support array selection ([#12092](https://github.com/facebook/jest/pull/12092)) ### Fixes diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index eddf1bec562e..bef99cce8ab5 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -895,6 +895,16 @@ const houseForSale = { wallColor: 'white', 'nice.oven': true, }, + livingroom: { + amenities: [ + { + couch: [ + ['large', {dimensions: [20, 20]}], + ['small', {dimensions: [10, 10]}], + ], + }, + ], + }, 'ceiling.height': 2, }; @@ -922,6 +932,10 @@ test('this house has my desired features', () => { ['oven', 'stove', 'washer'], ); expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven'); + expect(houseForSale).toHaveProperty( + 'livingroom.amenities[0].couch[0][1].dimensions[0]', + 20, + ); expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven']); expect(houseForSale).not.toHaveProperty(['kitchen', 'open']); diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index a66503240bab..16f85cccd725 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -3334,6 +3334,15 @@ Expected value: 1 Received value: {"c": {"d": 1}} `; +exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {}}}}).toHaveProperty('.a.b.c') 1`] = ` +expect(received).toHaveProperty(path) + +Expected path: ".a.b.c" +Received path: [] + +Received value: {"a": {"b": {"c": {}}}} +`; + exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {}}}}).toHaveProperty('a.b.c.d') 1`] = ` expect(received).toHaveProperty(path) @@ -3551,6 +3560,30 @@ Expected path: "memo" Expected value: not [] `; +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [[{"c": [{"d": 1}]}]]}}).toHaveProperty('a.b[0][0].c[0].d', 1) 1`] = ` +expect(received).not.toHaveProperty(path, value) + +Expected path: "a.b[0][0].c[0].d" + +Expected value: not 1 +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [{"c": [{"d": 1}]}]}}).toHaveProperty('a.b[0].c[0].d', 1) 1`] = ` +expect(received).not.toHaveProperty(path, value) + +Expected path: "a.b[0].c[0].d" + +Expected value: not 1 +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [{"c": {"d": [{"e": 1}, {"f": 2}]}}]}}).toHaveProperty('a.b[0].c.d[1].f', 2) 1`] = ` +expect(received).not.toHaveProperty(path, value) + +Expected path: "a.b[0].c.d[1].f" + +Expected value: not 2 +`; + exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1') 1`] = ` expect(received).not.toHaveProperty(path) diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 6599df72b2b2..20b48ca6053a 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -1885,6 +1885,9 @@ describe('.toHaveProperty()', () => { [{a: {b: undefined}}, 'a.b', undefined], [{a: {}}, 'a.b', undefined], // delete for breaking change in future major [{a: {b: {c: 5}}}, 'a.b', {c: 5}], + [{a: {b: [{c: [{d: 1}]}]}}, 'a.b[0].c[0].d', 1], + [{a: {b: [{c: {d: [{e: 1}, {f: 2}]}}]}}, 'a.b[0].c.d[1].f', 2], + [{a: {b: [[{c: [{d: 1}]}]]}}, 'a.b[0][0].c[0].d', 1], [Object.assign(Object.create(null), {property: 1}), 'property', 1], [new Foo(), 'a', undefined], [new Foo(), 'b', 'b'], @@ -1955,6 +1958,7 @@ describe('.toHaveProperty()', () => { [ [{a: {b: {c: {}}}}, 'a.b.c.d'], + [{a: {b: {c: {}}}}, '.a.b.c'], [{a: 1}, 'a.b.c.d'], [{}, 'a'], [1, 'a.b.c'], diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 5254722aee10..02e4c59813d0 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -44,6 +44,7 @@ import { getObjectSubset, getPath, iterableEquality, + pathAsArray, sparseArrayEquality, subsetEquality, typeEquality, @@ -704,7 +705,7 @@ const matchers: MatchersObject = { const expectedPathLength = typeof expectedPath === 'string' - ? expectedPath.split('.').length + ? pathAsArray(expectedPath).length : expectedPath.length; if (expectedPathType === 'array' && expectedPathLength === 0) { diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index eab02cd36204..725289ebb3a4 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -45,7 +45,7 @@ export const getPath = ( propertyPath: string | Array, ): GetPath => { if (!Array.isArray(propertyPath)) { - propertyPath = (propertyPath as string).split('.'); + propertyPath = pathAsArray(propertyPath); } if (propertyPath.length) { @@ -372,6 +372,24 @@ export const partition = ( return result; }; +export const pathAsArray = (propertyPath: string): Array => { + // will match everything that's not a dot or a bracket, and "" for consecutive dots. + const pattern = RegExp('[^.[\\]]+|(?=(?:\\.)(?:\\.|$))', 'g'); + const properties: Array = []; + + // Because the regex won't match a dot in the beginning of the path, if present. + if (propertyPath[0] === '.') { + properties.push(''); + } + + propertyPath.replace(pattern, match => { + properties.push(match); + return match; + }); + + return properties; +}; + // Copied from https://github.com/graingert/angular.js/blob/a43574052e9775cbc1d7dd8a086752c979b0f020/src/Angular.js#L685-L693 export const isError = (value: unknown): value is Error => { switch (Object.prototype.toString.call(value)) {