diff --git a/CHANGELOG.md b/CHANGELOG.md index af29b285dcff..35c9e3cd40ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-each]` Add support for keyPaths in test titles ([#6457](https://github.com/facebook/jest/pull/6457)) - `[jest-cli]` Add `jest --init` option that generates a basic configuration file with a short description for each option ([#6442](https://github.com/facebook/jest/pull/6442)) ### Fixes 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 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..1d72d097f394 100644 --- a/packages/jest-each/src/bind.js +++ b/packages/jest-each/src/bind.js @@ -129,12 +129,19 @@ 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 value = getPath(data, keyPath); + 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 +151,8 @@ const applyObjectParams = (obj: any, test: Function) => { const pluralize = (word: string, count: number) => word + (count === 1 ? '' : 's'); + +const getPath = (o: Object, [head, ...tail]: Array) => { + if (!head || !o.hasOwnProperty || !o.hasOwnProperty(head)) return o; + return getPath(o[head], tail); +};