From 611dbf8286e6f1ce19e33707620c536dbd6efabe Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Wed, 13 Jun 2018 13:29:22 +0100 Subject: [PATCH 1/9] Add support for keyPaths in test titles --- .../jest-each/src/__tests__/template.test.js | 61 ++++++++++++++++- packages/jest-each/src/bind.js | 68 +++++++++++++++++-- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/packages/jest-each/src/__tests__/template.test.js b/packages/jest-each/src/__tests__/template.test.js index 2e5adeeae1f8..d9d4129a59f5 100644 --- a/packages/jest-each/src/__tests__/template.test.js +++ b/packages/jest-each/src/__tests__/template.test.js @@ -138,7 +138,66 @@ describe('jest-each', () => { ); }); - test('calls global with cb function with object built from tabel headings and values', () => { + test('calls global with title containing $key in multiple positions', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a | b | expected + ${0} | ${1} | ${1} + ${1} | ${1} | ${2} + `; + const testFunction = get(eachObject, keyPath); + testFunction( + 'add($a, $b) expected string: a=$a, b=$b, expected=$expected', + noop, + ); + + const globalMock = get(globalTestMocks, keyPath); + expect(globalMock).toHaveBeenCalledTimes(2); + expect(globalMock).toHaveBeenCalledWith( + 'add(0, 1) expected string: a=0, b=1, expected=1', + expectFunction, + ); + expect(globalMock).toHaveBeenCalledWith( + 'add(1, 1) expected string: a=1, b=1, expected=2', + expectFunction, + ); + }); + + test('calls global with title containing $key.path', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a + ${{foo: {bar: 'baz'}}} + `; + const testFunction = get(eachObject, keyPath); + testFunction('interpolates object keyPath to value: $a.foo.bar', noop); + + const globalMock = get(globalTestMocks, keyPath); + expect(globalMock).toHaveBeenCalledTimes(1); + expect(globalMock).toHaveBeenCalledWith( + 'interpolates object keyPath to value: "baz"', + expectFunction, + ); + }); + + test('calls global with title containing last seen object when $key.path is invalid', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a + ${{foo: {bar: 'baz'}}} + `; + const testFunction = get(eachObject, keyPath); + testFunction('interpolates object keyPath to value: $a.foo.qux', noop); + + const globalMock = get(globalTestMocks, keyPath); + expect(globalMock).toHaveBeenCalledTimes(1); + expect(globalMock).toHaveBeenCalledWith( + 'interpolates object keyPath to value: {"bar": "baz"}', + expectFunction, + ); + }); + + test('calls global with cb function with object built from table headings and values', () => { const globalTestMocks = getGlobalTestMocks(); const testCallBack = jest.fn(); const eachObject = each.withGlobal(globalTestMocks)` diff --git a/packages/jest-each/src/bind.js b/packages/jest-each/src/bind.js index f2abdcba0872..7b0e611806f5 100644 --- a/packages/jest-each/src/bind.js +++ b/packages/jest-each/src/bind.js @@ -129,12 +129,20 @@ const buildTable = ( ), ); +const getMatchingKeyPaths = title => (matches, key) => + matches.concat(title.match(new RegExp(`\\$${key}[\\.\\w]*`, 'g')) || []); + +const replaceKeyPathWithValue = data => (title, match) => { + const keyPath = match.replace('$', '').split('.'); + const result = getPath(data, keyPath); + const value = result.hasEndProp ? result.value : result.lastTraversedObject; + return title.replace(match, pretty(value, {maxDepth: 1, min: true})); +}; + const interpolate = (title: string, data: any) => - Object.keys(data).reduce( - (acc, key) => - acc.replace('$' + key, pretty(data[key], {maxDepth: 1, min: true})), - title, - ); + Object.keys(data) + .reduce(getMatchingKeyPaths(title), []) // aka flatMap + .reduce(replaceKeyPathWithValue(data), title); const applyObjectParams = (obj: any, test: Function) => { if (test.length > 1) return done => test(obj, done); @@ -144,3 +152,53 @@ const applyObjectParams = (obj: any, test: Function) => { const pluralize = (word: string, count: number) => word + (count === 1 ? '' : 's'); + +const hasOwnProperty = (object: Object, value: string) => + Object.prototype.hasOwnProperty.call(object, value) || + Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); + +const getPath = (object: Object, propertyPath: string | Array) => { + if (!Array.isArray(propertyPath)) { + propertyPath = propertyPath.split('.'); + } + + 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 has broken and we can return right away. + return { + hasEndProp: false, + lastTraversedObject: object, + traversedPath: [], + }; + } + + 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; + } + + return { + lastTraversedObject: null, + traversedPath: [], + value: object, + }; +}; From 6b98ef184662b72010452aa1e68a0a5d505e23e5 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Wed, 13 Jun 2018 14:16:16 +0100 Subject: [PATCH 2/9] Move getPath from expect/utils to jest-util --- packages/expect/package.json | 3 +- packages/expect/src/__tests__/utils.test.js | 85 +------------------ packages/expect/src/matchers.js | 2 +- packages/expect/src/utils.js | 56 ------------ packages/jest-each/package.json | 1 + packages/jest-each/src/bind.js | 51 +---------- .../jest-util/src/__tests__/get_path.test.js | 79 +++++++++++++++++ packages/jest-util/src/get_path.js | 61 +++++++++++++ packages/jest-util/src/index.js | 2 + 9 files changed, 148 insertions(+), 192 deletions(-) create mode 100644 packages/jest-util/src/__tests__/get_path.test.js create mode 100644 packages/jest-util/src/get_path.js diff --git a/packages/expect/package.json b/packages/expect/package.json index 661cbf518f99..248446c08bee 100644 --- a/packages/expect/package.json +++ b/packages/expect/package.json @@ -14,6 +14,7 @@ "jest-get-type": "^22.1.0", "jest-matcher-utils": "^23.0.1", "jest-message-util": "^23.1.0", - "jest-regex-util": "^23.0.0" + "jest-regex-util": "^23.0.0", + "jest-util": "^23.1.0" } } diff --git a/packages/expect/src/__tests__/utils.test.js b/packages/expect/src/__tests__/utils.test.js index 6a5e7d909680..8e4073f7beea 100644 --- a/packages/expect/src/__tests__/utils.test.js +++ b/packages/expect/src/__tests__/utils.test.js @@ -9,90 +9,7 @@ 'use strict'; const {stringify} = require('jest-matcher-utils'); -const { - emptyObject, - getObjectSubset, - getPath, - subsetEquality, -} = require('../utils'); - -describe('getPath()', () => { - test('property exists', () => { - expect(getPath({a: {b: {c: 5}}}, 'a.b.c')).toEqual({ - hasEndProp: true, - lastTraversedObject: {c: 5}, - traversedPath: ['a', 'b', 'c'], - value: 5, - }); - - expect(getPath({a: {b: {c: {d: 1}}}}, 'a.b.c.d')).toEqual({ - hasEndProp: true, - lastTraversedObject: {d: 1}, - traversedPath: ['a', 'b', 'c', 'd'], - value: 1, - }); - }); - - test('property doesnt exist', () => { - expect(getPath({a: {b: {}}}, 'a.b.c')).toEqual({ - hasEndProp: false, - lastTraversedObject: {}, - traversedPath: ['a', 'b'], - value: undefined, - }); - }); - - test('property exist but undefined', () => { - expect(getPath({a: {b: {c: undefined}}}, 'a.b.c')).toEqual({ - hasEndProp: true, - lastTraversedObject: {c: undefined}, - traversedPath: ['a', 'b', 'c'], - value: undefined, - }); - }); - - 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, - lastTraversedObject: {}, - traversedPath: ['a'], - value: undefined, - }); - }); - - 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, - }); - }); -}); +const {emptyObject, getObjectSubset, subsetEquality} = require('../utils'); describe('getObjectSubset()', () => { [ diff --git a/packages/expect/src/matchers.js b/packages/expect/src/matchers.js index 52feb63dea37..0529935f7b12 100644 --- a/packages/expect/src/matchers.js +++ b/packages/expect/src/matchers.js @@ -26,13 +26,13 @@ import { } from 'jest-matcher-utils'; import { getObjectSubset, - getPath, iterableEquality, subsetEquality, typeEquality, isOneline, } from './utils'; import {equals} from './jasmine_utils'; +import {getPath} from 'jest-util'; type ContainIterable = | Array diff --git a/packages/expect/src/utils.js b/packages/expect/src/utils.js index c25e9cc46c5d..d0795eb425a5 100644 --- a/packages/expect/src/utils.js +++ b/packages/expect/src/utils.js @@ -14,66 +14,10 @@ import { isImmutableUnorderedSet, } from './jasmine_utils'; -type GetPath = { - hasEndProp?: boolean, - lastTraversedObject: ?Object, - traversedPath: Array, - value?: any, -}; - export const hasOwnProperty = (object: Object, value: string) => Object.prototype.hasOwnProperty.call(object, value) || Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); -export const getPath = ( - object: Object, - propertyPath: string | Array, -): GetPath => { - if (!Array.isArray(propertyPath)) { - propertyPath = propertyPath.split('.'); - } - - 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 has broken and we can return right away. - return { - hasEndProp: false, - lastTraversedObject: object, - traversedPath: [], - }; - } - - 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; - } - - return { - lastTraversedObject: null, - traversedPath: [], - value: object, - }; -}; - // Strip properties from object that are not present in the subset. Useful for // printing the diff for toMatchObject() without adding unrelated noise. export const getObjectSubset = (object: Object, subset: Object) => { diff --git a/packages/jest-each/package.json b/packages/jest-each/package.json index 5340f62d9884..417922b91960 100644 --- a/packages/jest-each/package.json +++ b/packages/jest-each/package.json @@ -17,6 +17,7 @@ "license": "MIT", "dependencies": { "chalk": "^2.0.1", + "jest-util": "^23.1.0", "pretty-format": "^23.0.1" } } diff --git a/packages/jest-each/src/bind.js b/packages/jest-each/src/bind.js index 7b0e611806f5..e815324cf24c 100644 --- a/packages/jest-each/src/bind.js +++ b/packages/jest-each/src/bind.js @@ -10,6 +10,7 @@ import util from 'util'; import chalk from 'chalk'; import pretty from 'pretty-format'; +import {getPath} from 'jest-util'; type Table = Array>; type PrettyArgs = { @@ -152,53 +153,3 @@ const applyObjectParams = (obj: any, test: Function) => { const pluralize = (word: string, count: number) => word + (count === 1 ? '' : 's'); - -const hasOwnProperty = (object: Object, value: string) => - Object.prototype.hasOwnProperty.call(object, value) || - Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); - -const getPath = (object: Object, propertyPath: string | Array) => { - if (!Array.isArray(propertyPath)) { - propertyPath = propertyPath.split('.'); - } - - 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 has broken and we can return right away. - return { - hasEndProp: false, - lastTraversedObject: object, - traversedPath: [], - }; - } - - 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; - } - - return { - lastTraversedObject: null, - traversedPath: [], - value: object, - }; -}; diff --git a/packages/jest-util/src/__tests__/get_path.test.js b/packages/jest-util/src/__tests__/get_path.test.js new file mode 100644 index 000000000000..26dda6786945 --- /dev/null +++ b/packages/jest-util/src/__tests__/get_path.test.js @@ -0,0 +1,79 @@ +import getPath from '../get_path'; + +describe('getPath()', () => { + test('property exists', () => { + expect(getPath({a: {b: {c: 5}}}, 'a.b.c')).toEqual({ + hasEndProp: true, + lastTraversedObject: {c: 5}, + traversedPath: ['a', 'b', 'c'], + value: 5, + }); + + expect(getPath({a: {b: {c: {d: 1}}}}, 'a.b.c.d')).toEqual({ + hasEndProp: true, + lastTraversedObject: {d: 1}, + traversedPath: ['a', 'b', 'c', 'd'], + value: 1, + }); + }); + + test('property doesnt exist', () => { + expect(getPath({a: {b: {}}}, 'a.b.c')).toEqual({ + hasEndProp: false, + lastTraversedObject: {}, + traversedPath: ['a', 'b'], + value: undefined, + }); + }); + + test('property exist but undefined', () => { + expect(getPath({a: {b: {c: undefined}}}, 'a.b.c')).toEqual({ + hasEndProp: true, + lastTraversedObject: {c: undefined}, + traversedPath: ['a', 'b', 'c'], + value: undefined, + }); + }); + + 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, + lastTraversedObject: {}, + traversedPath: ['a'], + value: undefined, + }); + }); + + 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, + }); + }); +}); diff --git a/packages/jest-util/src/get_path.js b/packages/jest-util/src/get_path.js new file mode 100644 index 000000000000..4b138272b3bb --- /dev/null +++ b/packages/jest-util/src/get_path.js @@ -0,0 +1,61 @@ +type GetPath = { + hasEndProp?: boolean, + lastTraversedObject: ?Object, + traversedPath: Array, + value?: any, +}; + +const hasOwnProperty = (object: Object, value: string) => + Object.prototype.hasOwnProperty.call(object, value) || + Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); + +const getPath = ( + object: Object, + propertyPath: string | Array, +): GetPath => { + if (!Array.isArray(propertyPath)) { + propertyPath = propertyPath.split('.'); + } + + 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 has broken and we can return right away. + return { + hasEndProp: false, + lastTraversedObject: object, + traversedPath: [], + }; + } + + 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; + } + + return { + lastTraversedObject: null, + traversedPath: [], + value: object, + }; +}; + +export default getPath; diff --git a/packages/jest-util/src/index.js b/packages/jest-util/src/index.js index 2b00ae975dca..7f9ad2a17d9b 100644 --- a/packages/jest-util/src/index.js +++ b/packages/jest-util/src/index.js @@ -23,6 +23,7 @@ import getCallsite from './get_callsite'; import setGlobal from './set_global'; import deepCyclicCopy from './deep_cyclic_copy'; import convertDescriptorToString from './convert_descriptor_to_string'; +import getPath from './get_path'; const createDirectory = (path: string) => { try { @@ -47,6 +48,7 @@ module.exports = { getCallsite, getConsoleOutput, getFailedSnapshotTests, + getPath, installCommonGlobals, isInteractive, setGlobal, From 0f0066a763a2db115b5fbecce6257bdf09280aad Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Wed, 13 Jun 2018 14:19:42 +0100 Subject: [PATCH 3/9] Add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b2a46da94f..f82c079b338a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## master +### Features + +- `[jest-each]` Add support for keyPaths in test titles ([#6457](https://github.com/facebook/jest/pull/6457/files)) + ### Fixes - `[jest-config]` Add missing options to the `defaults` object ([#6428](https://github.com/facebook/jest/pull/6428)) From 776da6242d09b224c7d038d8d2f21bc1b22853de Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Wed, 13 Jun 2018 14:24:56 +0100 Subject: [PATCH 4/9] Add keypath interpolation docs --- docs/GlobalAPI.md | 2 ++ packages/jest-each/README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/GlobalAPI.md b/docs/GlobalAPI.md index 55e8c08c369c..98414cef4145 100644 --- a/docs/GlobalAPI.md +++ b/docs/GlobalAPI.md @@ -271,6 +271,7 @@ describe.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( - First row of variable name column headings separated with `|` - One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax. - `name`: `String` the title of the test suite, use `$variable` to inject test data into the suite title from the tagged template expressions. + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` - `fn`: `Function` the suite of tests to be ran, this is the function that will receive the test data object. Example: @@ -507,6 +508,7 @@ test.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( - First row of variable name column headings separated with `|` - One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax. - `name`: `String` the title of the test, use `$variable` to inject test data into the test title from the tagged template expressions. + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` - `fn`: `Function` the test to be ran, this is the function that will receive the test data object. Example: diff --git a/packages/jest-each/README.md b/packages/jest-each/README.md index d932cbaac14b..867586f36c76 100644 --- a/packages/jest-each/README.md +++ b/packages/jest-each/README.md @@ -271,6 +271,7 @@ each` ##### `.test`: - name: `String` the title of the `test`, use `$variable` in the name string to inject test values into the test title from the tagged template expressions + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` - testFn: `Function` the test logic, this is the function that will receive the parameters of each row as function arguments #### `each[tagged template].describe(name, suiteFn)` @@ -306,6 +307,7 @@ each` ##### `.describe`: - name: `String` the title of the `test`, use `$variable` in the name string to inject test values into the test title from the tagged template expressions + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` - suiteFn: `Function` the suite of `test`/`it`s to be ran, this is the function that will receive the parameters in each row as function arguments ### Usage From c7184d557d8ea513ee589107597a6f426b348795 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Wed, 13 Jun 2018 14:34:07 +0100 Subject: [PATCH 5/9] Add missing license and flow comments --- packages/jest-util/src/__tests__/get_path.test.js | 8 ++++++++ packages/jest-util/src/get_path.js | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/jest-util/src/__tests__/get_path.test.js b/packages/jest-util/src/__tests__/get_path.test.js index 26dda6786945..e2ad8ba577e0 100644 --- a/packages/jest-util/src/__tests__/get_path.test.js +++ b/packages/jest-util/src/__tests__/get_path.test.js @@ -1,3 +1,11 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + import getPath from '../get_path'; describe('getPath()', () => { diff --git a/packages/jest-util/src/get_path.js b/packages/jest-util/src/get_path.js index 4b138272b3bb..56637f59c3d4 100644 --- a/packages/jest-util/src/get_path.js +++ b/packages/jest-util/src/get_path.js @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + type GetPath = { hasEndProp?: boolean, lastTraversedObject: ?Object, From 8853f4034801b26c97dd73d00b831e7c0c393036 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Thu, 14 Jun 2018 09:20:50 +0100 Subject: [PATCH 6/9] Revert "Move getPath from expect/utils to jest-util" This reverts commit 6b98ef184662b72010452aa1e68a0a5d505e23e5. --- packages/expect/package.json | 3 +- packages/expect/src/__tests__/utils.test.js | 85 ++++++++++++++++++++- packages/expect/src/matchers.js | 2 +- packages/expect/src/utils.js | 56 ++++++++++++++ packages/jest-each/package.json | 1 - packages/jest-each/src/bind.js | 51 ++++++++++++- packages/jest-util/src/index.js | 2 - 7 files changed, 192 insertions(+), 8 deletions(-) diff --git a/packages/expect/package.json b/packages/expect/package.json index 248446c08bee..661cbf518f99 100644 --- a/packages/expect/package.json +++ b/packages/expect/package.json @@ -14,7 +14,6 @@ "jest-get-type": "^22.1.0", "jest-matcher-utils": "^23.0.1", "jest-message-util": "^23.1.0", - "jest-regex-util": "^23.0.0", - "jest-util": "^23.1.0" + "jest-regex-util": "^23.0.0" } } diff --git a/packages/expect/src/__tests__/utils.test.js b/packages/expect/src/__tests__/utils.test.js index 8e4073f7beea..6a5e7d909680 100644 --- a/packages/expect/src/__tests__/utils.test.js +++ b/packages/expect/src/__tests__/utils.test.js @@ -9,7 +9,90 @@ 'use strict'; const {stringify} = require('jest-matcher-utils'); -const {emptyObject, getObjectSubset, subsetEquality} = require('../utils'); +const { + emptyObject, + getObjectSubset, + getPath, + subsetEquality, +} = require('../utils'); + +describe('getPath()', () => { + test('property exists', () => { + expect(getPath({a: {b: {c: 5}}}, 'a.b.c')).toEqual({ + hasEndProp: true, + lastTraversedObject: {c: 5}, + traversedPath: ['a', 'b', 'c'], + value: 5, + }); + + expect(getPath({a: {b: {c: {d: 1}}}}, 'a.b.c.d')).toEqual({ + hasEndProp: true, + lastTraversedObject: {d: 1}, + traversedPath: ['a', 'b', 'c', 'd'], + value: 1, + }); + }); + + test('property doesnt exist', () => { + expect(getPath({a: {b: {}}}, 'a.b.c')).toEqual({ + hasEndProp: false, + lastTraversedObject: {}, + traversedPath: ['a', 'b'], + value: undefined, + }); + }); + + test('property exist but undefined', () => { + expect(getPath({a: {b: {c: undefined}}}, 'a.b.c')).toEqual({ + hasEndProp: true, + lastTraversedObject: {c: undefined}, + traversedPath: ['a', 'b', 'c'], + value: undefined, + }); + }); + + 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, + lastTraversedObject: {}, + traversedPath: ['a'], + value: undefined, + }); + }); + + 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, + }); + }); +}); describe('getObjectSubset()', () => { [ diff --git a/packages/expect/src/matchers.js b/packages/expect/src/matchers.js index 0529935f7b12..52feb63dea37 100644 --- a/packages/expect/src/matchers.js +++ b/packages/expect/src/matchers.js @@ -26,13 +26,13 @@ import { } from 'jest-matcher-utils'; import { getObjectSubset, + getPath, iterableEquality, subsetEquality, typeEquality, isOneline, } from './utils'; import {equals} from './jasmine_utils'; -import {getPath} from 'jest-util'; type ContainIterable = | Array diff --git a/packages/expect/src/utils.js b/packages/expect/src/utils.js index d0795eb425a5..c25e9cc46c5d 100644 --- a/packages/expect/src/utils.js +++ b/packages/expect/src/utils.js @@ -14,10 +14,66 @@ import { isImmutableUnorderedSet, } from './jasmine_utils'; +type GetPath = { + hasEndProp?: boolean, + lastTraversedObject: ?Object, + traversedPath: Array, + value?: any, +}; + export const hasOwnProperty = (object: Object, value: string) => Object.prototype.hasOwnProperty.call(object, value) || Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); +export const getPath = ( + object: Object, + propertyPath: string | Array, +): GetPath => { + if (!Array.isArray(propertyPath)) { + propertyPath = propertyPath.split('.'); + } + + 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 has broken and we can return right away. + return { + hasEndProp: false, + lastTraversedObject: object, + traversedPath: [], + }; + } + + 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; + } + + return { + lastTraversedObject: null, + traversedPath: [], + value: object, + }; +}; + // Strip properties from object that are not present in the subset. Useful for // printing the diff for toMatchObject() without adding unrelated noise. export const getObjectSubset = (object: Object, subset: Object) => { diff --git a/packages/jest-each/package.json b/packages/jest-each/package.json index 417922b91960..5340f62d9884 100644 --- a/packages/jest-each/package.json +++ b/packages/jest-each/package.json @@ -17,7 +17,6 @@ "license": "MIT", "dependencies": { "chalk": "^2.0.1", - "jest-util": "^23.1.0", "pretty-format": "^23.0.1" } } diff --git a/packages/jest-each/src/bind.js b/packages/jest-each/src/bind.js index e815324cf24c..7b0e611806f5 100644 --- a/packages/jest-each/src/bind.js +++ b/packages/jest-each/src/bind.js @@ -10,7 +10,6 @@ import util from 'util'; import chalk from 'chalk'; import pretty from 'pretty-format'; -import {getPath} from 'jest-util'; type Table = Array>; type PrettyArgs = { @@ -153,3 +152,53 @@ const applyObjectParams = (obj: any, test: Function) => { const pluralize = (word: string, count: number) => word + (count === 1 ? '' : 's'); + +const hasOwnProperty = (object: Object, value: string) => + Object.prototype.hasOwnProperty.call(object, value) || + Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); + +const getPath = (object: Object, propertyPath: string | Array) => { + if (!Array.isArray(propertyPath)) { + propertyPath = propertyPath.split('.'); + } + + 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 has broken and we can return right away. + return { + hasEndProp: false, + lastTraversedObject: object, + traversedPath: [], + }; + } + + 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; + } + + return { + lastTraversedObject: null, + traversedPath: [], + value: object, + }; +}; diff --git a/packages/jest-util/src/index.js b/packages/jest-util/src/index.js index 7f9ad2a17d9b..2b00ae975dca 100644 --- a/packages/jest-util/src/index.js +++ b/packages/jest-util/src/index.js @@ -23,7 +23,6 @@ import getCallsite from './get_callsite'; import setGlobal from './set_global'; import deepCyclicCopy from './deep_cyclic_copy'; import convertDescriptorToString from './convert_descriptor_to_string'; -import getPath from './get_path'; const createDirectory = (path: string) => { try { @@ -48,7 +47,6 @@ module.exports = { getCallsite, getConsoleOutput, getFailedSnapshotTests, - getPath, installCommonGlobals, isInteractive, setGlobal, From 6d1744004756bdb93d4b0c27122457336214c0c3 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Thu, 14 Jun 2018 09:29:37 +0100 Subject: [PATCH 7/9] Refactor getPath to be simplier --- packages/jest-each/src/bind.js | 54 +++------------------------------- 1 file changed, 4 insertions(+), 50 deletions(-) diff --git a/packages/jest-each/src/bind.js b/packages/jest-each/src/bind.js index 7b0e611806f5..1d72d097f394 100644 --- a/packages/jest-each/src/bind.js +++ b/packages/jest-each/src/bind.js @@ -134,8 +134,7 @@ const getMatchingKeyPaths = title => (matches, key) => const replaceKeyPathWithValue = data => (title, match) => { const keyPath = match.replace('$', '').split('.'); - const result = getPath(data, keyPath); - const value = result.hasEndProp ? result.value : result.lastTraversedObject; + const value = getPath(data, keyPath); return title.replace(match, pretty(value, {maxDepth: 1, min: true})); }; @@ -153,52 +152,7 @@ const applyObjectParams = (obj: any, test: Function) => { const pluralize = (word: string, count: number) => word + (count === 1 ? '' : 's'); -const hasOwnProperty = (object: Object, value: string) => - Object.prototype.hasOwnProperty.call(object, value) || - Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); - -const getPath = (object: Object, propertyPath: string | Array) => { - if (!Array.isArray(propertyPath)) { - propertyPath = propertyPath.split('.'); - } - - 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 has broken and we can return right away. - return { - hasEndProp: false, - lastTraversedObject: object, - traversedPath: [], - }; - } - - 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; - } - - return { - lastTraversedObject: null, - traversedPath: [], - value: object, - }; +const getPath = (o: Object, [head, ...tail]: Array) => { + if (!head || !o.hasOwnProperty || !o.hasOwnProperty(head)) return o; + return getPath(o[head], tail); }; From fbdbcb68687e84d61f2999bc98531b5006206b27 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Thu, 14 Jun 2018 09:30:42 +0100 Subject: [PATCH 8/9] Remove unused code --- .../jest-util/src/__tests__/get_path.test.js | 87 ------------------- packages/jest-util/src/get_path.js | 70 --------------- 2 files changed, 157 deletions(-) delete mode 100644 packages/jest-util/src/__tests__/get_path.test.js delete mode 100644 packages/jest-util/src/get_path.js diff --git a/packages/jest-util/src/__tests__/get_path.test.js b/packages/jest-util/src/__tests__/get_path.test.js deleted file mode 100644 index e2ad8ba577e0..000000000000 --- a/packages/jest-util/src/__tests__/get_path.test.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import getPath from '../get_path'; - -describe('getPath()', () => { - test('property exists', () => { - expect(getPath({a: {b: {c: 5}}}, 'a.b.c')).toEqual({ - hasEndProp: true, - lastTraversedObject: {c: 5}, - traversedPath: ['a', 'b', 'c'], - value: 5, - }); - - expect(getPath({a: {b: {c: {d: 1}}}}, 'a.b.c.d')).toEqual({ - hasEndProp: true, - lastTraversedObject: {d: 1}, - traversedPath: ['a', 'b', 'c', 'd'], - value: 1, - }); - }); - - test('property doesnt exist', () => { - expect(getPath({a: {b: {}}}, 'a.b.c')).toEqual({ - hasEndProp: false, - lastTraversedObject: {}, - traversedPath: ['a', 'b'], - value: undefined, - }); - }); - - test('property exist but undefined', () => { - expect(getPath({a: {b: {c: undefined}}}, 'a.b.c')).toEqual({ - hasEndProp: true, - lastTraversedObject: {c: undefined}, - traversedPath: ['a', 'b', 'c'], - value: undefined, - }); - }); - - 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, - lastTraversedObject: {}, - traversedPath: ['a'], - value: undefined, - }); - }); - - 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, - }); - }); -}); diff --git a/packages/jest-util/src/get_path.js b/packages/jest-util/src/get_path.js deleted file mode 100644 index 56637f59c3d4..000000000000 --- a/packages/jest-util/src/get_path.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -type GetPath = { - hasEndProp?: boolean, - lastTraversedObject: ?Object, - traversedPath: Array, - value?: any, -}; - -const hasOwnProperty = (object: Object, value: string) => - Object.prototype.hasOwnProperty.call(object, value) || - Object.prototype.hasOwnProperty.call(object.constructor.prototype, value); - -const getPath = ( - object: Object, - propertyPath: string | Array, -): GetPath => { - if (!Array.isArray(propertyPath)) { - propertyPath = propertyPath.split('.'); - } - - 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 has broken and we can return right away. - return { - hasEndProp: false, - lastTraversedObject: object, - traversedPath: [], - }; - } - - 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; - } - - return { - lastTraversedObject: null, - traversedPath: [], - value: object, - }; -}; - -export default getPath; From 341997aa4efc5dbc9f8ee45305c7fdda0dfa73c3 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Thu, 14 Jun 2018 10:31:33 +0100 Subject: [PATCH 9/9] Fix changelog link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f82c079b338a..3ce1183533f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- `[jest-each]` Add support for keyPaths in test titles ([#6457](https://github.com/facebook/jest/pull/6457/files)) +- `[jest-each]` Add support for keyPaths in test titles ([#6457](https://github.com/facebook/jest/pull/6457)) ### Fixes