diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ce77c05355..ee9305949add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,17 @@ ([#5166](https://github.com/facebook/jest/pull/5166)) * `[jest-config]` Allow configuration objects inside `projects` array ([#5176](https://github.com/facebook/jest/pull/5176)) +* `[expect]` Add support to `.toHaveProperty` matcher to accept the keyPath + argument as an array of properties/indices. + ([#5220](https://github.com/facebook/jest/pull/5220)) +* `[docs]` Add documentation for .toHaveProperty matcher to accept the keyPath + argument as an array of properties/indices. + ([#5220](https://github.com/facebook/jest/pull/5220)) ### Chore & Maintenance -* `[docs]` Describe the order of execution of describe and test blocks, and a note on using `test.concurrent`. +* `[docs]` Describe the order of execution of describe and test blocks, and a + note on using `test.concurrent`. ([#5217](https://github.com/facebook/jest/pull/5217)) ## jest 22.0.4 diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 7efd0ed2554e..9a502c8e0b1b 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -903,9 +903,10 @@ describe('toMatchObject applied to arrays arrays', () => { ### `.toHaveProperty(keyPath, value)` Use `.toHaveProperty` to check if property at provided reference `keyPath` -exists for an object. For checking deeply nested properties in an object use +exists for an object. For checking deeply nested properties in an object you may +use [dot notation](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors) -for deep references. +or an array containing the keyPath for deep references. Optionally, you can provide a `value` to check if it's equal to the value present at `keyPath` on the target object. This matcher uses 'deep equality' @@ -943,6 +944,16 @@ test('this house has my desired features', () => { ]); expect(houseForSale).not.toHaveProperty('kitchen.open'); + + // Deep referencing using an array containing the keyPath + expect(houseForSale).toHaveProperty(['kitchen', 'area'], 20); + expect(houseForSale).toHaveProperty( + ['kitchen', 'amenities'], + ['oven', 'stove', 'washer'], + ); + expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], '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 8b14ee7cf157..f9492e48f432 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -2562,21 +2562,21 @@ Expected value to have a 'length' property that is a number. Received: exports[`.toHaveProperty() {error} expect({"a": {"b": {}}}).toHaveProperty('1') 1`] = ` "expect(object)[.not].toHaveProperty(path) -Expected path to be a string. Received: +Expected path to be a string or an array. Received: number: 1" `; exports[`.toHaveProperty() {error} expect({"a": {"b": {}}}).toHaveProperty('null') 1`] = ` "expect(object)[.not].toHaveProperty(path) -Expected path to be a string. Received: +Expected path to be a string or an array. Received: null: null" `; exports[`.toHaveProperty() {error} expect({"a": {"b": {}}}).toHaveProperty('undefined') 1`] = ` "expect(object)[.not].toHaveProperty(path) -Expected path to be a string. Received: +Expected path to be a string or an array. Received: undefined: undefined" `; @@ -2616,6 +2616,19 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a,b,c,d', 2) 1`] = ` +"expect(object).toHaveProperty(path, value) + +Expected the object: + {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} +To have a nested property: + [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] +With a value of: + 2 +Received: + 1" +`; + exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a.b.c.d', 2) 1`] = ` "expect(object).toHaveProperty(path, value) @@ -2730,6 +2743,31 @@ Received: object.a: 1" `; +exports[`.toHaveProperty() {pass: false} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d', 2) 1`] = ` +"expect(object).toHaveProperty(path, value) + +Expected the object: + {\\"a.b.c.d\\": 1} +To have a nested property: + \\"a.b.c.d\\" +With a value of: + 2 +" +`; + +exports[`.toHaveProperty() {pass: false} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d', 2) 2`] = ` +"expect(object).toHaveProperty(path, value) + +Expected the object: + {\\"a.b.c.d\\": 1} +To have a nested property: + [\\"a.b.c.d\\"] +With a value of: + 2 +Received: + 1" +`; + exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a') 1`] = ` "expect(object).toHaveProperty(path) @@ -2774,7 +2812,51 @@ With a value of: " `; -exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a.b.c.d')' 1`] = ` +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1') 1`] = ` +"expect(object).not.toHaveProperty(path) + +Expected the object: + {\\"a\\": {\\"b\\": [1, 2, 3]}} +Not to have a nested property: + [\\"a\\", \\"b\\", 1] +" +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1', 2) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {\\"a\\": {\\"b\\": [1, 2, 3]}} +Not to have a nested property: + [\\"a\\", \\"b\\", 1] +With a value of: + 2 +" +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a,b,c,d') 1`] = ` +"expect(object).not.toHaveProperty(path) + +Expected the object: + {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} +Not to have a nested property: + [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] +" +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a,b,c,d', 1) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} +Not to have a nested property: + [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] +With a value of: + 1 +" +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a.b.c.d') 1`] = ` "expect(object).not.toHaveProperty(path) Expected the object: @@ -2808,7 +2890,7 @@ With a value of: " `; -exports[`.toHaveProperty() {pass: true} expect({"a": {"b": undefined}}).toHaveProperty('a.b')' 1`] = ` +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": undefined}}).toHaveProperty('a.b') 1`] = ` "expect(object).not.toHaveProperty(path) Expected the object: @@ -2830,7 +2912,7 @@ With a value of: " `; -exports[`.toHaveProperty() {pass: true} expect({"a": 0}).toHaveProperty('a')' 1`] = ` +exports[`.toHaveProperty() {pass: true} expect({"a": 0}).toHaveProperty('a') 1`] = ` "expect(object).not.toHaveProperty(path) Expected the object: @@ -2852,6 +2934,28 @@ With a value of: " `; +exports[`.toHaveProperty() {pass: true} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d') 1`] = ` +"expect(object).not.toHaveProperty(path) + +Expected the object: + {\\"a.b.c.d\\": 1} +Not to have a nested property: + [\\"a.b.c.d\\"] +" +`; + +exports[`.toHaveProperty() {pass: true} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d', 1) 1`] = ` +"expect(object).not.toHaveProperty(path, value) + +Expected the object: + {\\"a.b.c.d\\": 1} +Not to have a nested property: + [\\"a.b.c.d\\"] +With a value of: + 1 +" +`; + exports[`.toHaveProperty() {pass: true} expect({"property": 1}).toHaveProperty('property', 1) 1`] = ` "expect(object).not.toHaveProperty(path, value) diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 2caec7d65995..bbce3f79d143 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -751,6 +751,9 @@ describe('.toHaveLength', () => { describe('.toHaveProperty()', () => { [ [{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 1], + [{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 1], + [{'a.b.c.d': 1}, ['a.b.c.d'], 1], + [{a: {b: [1, 2, 3]}}, ['a', 'b', 1], 2], [{a: 0}, 'a', 0], [{a: {b: undefined}}, 'a.b', undefined], [{a: {b: {c: 5}}}, 'a.b', {c: 5}], @@ -769,6 +772,9 @@ describe('.toHaveProperty()', () => { [ [{a: {b: {c: {d: 1}}}}, 'a.b.ttt.d', 1], [{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 2], + [{'a.b.c.d': 1}, 'a.b.c.d', 2], + [{'a.b.c.d': 1}, ['a.b.c.d'], 2], + [{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 2], [{a: {b: {c: {}}}}, 'a.b.c.d', 1], [{a: 1}, 'a.b.c.d', 5], [{}, 'a', 'test'], @@ -789,12 +795,15 @@ describe('.toHaveProperty()', () => { [ [{a: {b: {c: {d: 1}}}}, 'a.b.c.d'], + [{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd']], + [{'a.b.c.d': 1}, ['a.b.c.d']], + [{a: {b: [1, 2, 3]}}, ['a', 'b', 1]], [{a: 0}, 'a'], [{a: {b: undefined}}, 'a.b'], ].forEach(([obj, keyPath]) => { test(`{pass: true} expect(${stringify( obj, - )}).toHaveProperty('${keyPath}')'`, () => { + )}).toHaveProperty('${keyPath}')`, () => { jestExpect(obj).toHaveProperty(keyPath); expect(() => jestExpect(obj).not.toHaveProperty(keyPath), diff --git a/packages/expect/src/matchers.js b/packages/expect/src/matchers.js index 4a559412db17..25258e7ef3b2 100644 --- a/packages/expect/src/matchers.js +++ b/packages/expect/src/matchers.js @@ -486,7 +486,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toHaveProperty(object: Object, keyPath: string, value?: any) { + toHaveProperty(object: Object, keyPath: string | Array, value?: any) { const valuePassed = arguments.length === 3; if (!object && typeof object !== 'string' && typeof object !== 'number') { @@ -500,13 +500,17 @@ const matchers: MatchersObject = { ); } - if (getType(keyPath) !== 'string') { + const keyPathType = getType(keyPath); + + if (keyPathType !== 'string' && keyPathType !== 'array') { throw new Error( matcherHint('[.not].toHaveProperty', 'object', 'path', { secondArgument: valuePassed ? 'value' : null, }) + '\n\n' + - `Expected ${EXPECTED_COLOR('path')} to be a string. Received:\n` + + `Expected ${EXPECTED_COLOR( + 'path', + )} to be a string or an array. Received:\n` + ` ${getType(keyPath)}: ${printReceived(keyPath)}`, ); }