From 7e610acadd58b8ba19d8439c38c0eb25b9f2abb4 Mon Sep 17 00:00:00 2001 From: frozenbonito Date: Sat, 8 May 2021 17:46:46 +0900 Subject: [PATCH] feat(jest-each): add support for interpolation with object properties (#11388) --- CHANGELOG.md | 1 + docs/GlobalAPI.md | 38 ++++++++ packages/jest-each/README.md | 41 +++++++++ .../jest-each/src/__tests__/array.test.ts | 77 +++++++++++++++++ packages/jest-each/src/table/array.ts | 24 +++++- packages/jest-each/src/table/interpolation.ts | 86 +++++++++++++++++++ packages/jest-each/src/table/template.ts | 78 +---------------- 7 files changed, 268 insertions(+), 77 deletions(-) create mode 100644 packages/jest-each/src/table/interpolation.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb083ec04f0..4e38d4a8e26a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - `[jest-core]` Add support for `globalSetup` and `globalTeardown` written in ESM ([#11267](https://github.com/facebook/jest/pull/11267)) - `[jest-core]` Add support for `watchPlugins` written in ESM ([#11315](https://github.com/facebook/jest/pull/11315)) - `[jest-core]` Add support for `runner` written in ESM ([#11232](https://github.com/facebook/jest/pull/11232)) +- `[jest-each]` Add support for interpolation with object properties ([#11388](https://github.com/facebook/jest/pull/11388)) - `[jest-environment-node]` Add AbortController to globals ([#11182](https://github.com/facebook/jest/pull/11182)) - `[@jest/fake-timers]` Update to `@sinonjs/fake-timers` to v7 ([#11198](https://github.com/facebook/jest/pull/11198)) - `[jest-haste-map]` Handle injected scm clocks ([#10966](https://github.com/facebook/jest/pull/10966)) diff --git a/docs/GlobalAPI.md b/docs/GlobalAPI.md index ec9f27633c91..775efe6125fa 100644 --- a/docs/GlobalAPI.md +++ b/docs/GlobalAPI.md @@ -245,6 +245,10 @@ Use `describe.each` if you keep duplicating the same test suites with different - `%o` - Object. - `%#` - Index of the test case. - `%%` - single percent sign ('%'). This does not consume an argument. + - Or generate unique test titles by injecting properties of test case object with `$variable` + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` + - You can use `$#` to inject the index of the test case + - You cannot use `$variable` with the `printf` formatting except for `%%` - `fn`: `Function` the suite of tests to be ran, this is the function that will receive the parameters in each row as function arguments. - Optionally, you can provide a `timeout` (in milliseconds) for specifying how long to wait for each row before aborting. _Note: The default timeout is 5 seconds._ @@ -270,6 +274,26 @@ describe.each([ }); ``` +```js +describe.each([ + {a: 1, b: 1, expected: 2}, + {a: 1, b: 2, expected: 3}, + {a: 2, b: 1, expected: 3}, +])('.add($a, $b)', ({a, b, expected}) => { + test(`returns ${expected}`, () => { + expect(a + b).toBe(expected); + }); + + test(`returned value not be greater than ${expected}`, () => { + expect(a + b).not.toBeGreaterThan(expected); + }); + + test(`returned value not be less than ${expected}`, () => { + expect(a + b).not.toBeLessThan(expected); + }); +}); +``` + #### 2. `` describe.each`table`(name, fn, timeout) `` - `table`: `Tagged Template Literal` @@ -655,6 +679,10 @@ Use `test.each` if you keep duplicating the same test with different data. `test - `%o` - Object. - `%#` - Index of the test case. - `%%` - single percent sign ('%'). This does not consume an argument. + - Or generate unique test titles by injecting properties of test case object with `$variable` + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` + - You can use `$#` to inject the index of the test case + - You cannot use `$variable` with the `printf` formatting except for `%%` - `fn`: `Function` the test to be ran, this is the function that will receive the parameters in each row as function arguments. - Optionally, you can provide a `timeout` (in milliseconds) for specifying how long to wait for each row before aborting. _Note: The default timeout is 5 seconds._ @@ -670,6 +698,16 @@ test.each([ }); ``` +```js +test.each([ + {a: 1, b: 1, expected: 2}, + {a: 1, b: 2, expected: 3}, + {a: 2, b: 1, expected: 3}, +])('.add($a, $b)', ({a, b, expected}) => { + expect(a + b).toBe(expected); +}); +``` + #### 2. `` test.each`table`(name, fn, timeout) `` - `table`: `Tagged Template Literal` diff --git a/packages/jest-each/README.md b/packages/jest-each/README.md index c06a6ad2b589..43dc11a4e5f2 100644 --- a/packages/jest-each/README.md +++ b/packages/jest-each/README.md @@ -41,6 +41,7 @@ jest-each allows you to provide multiple arguments to your `test`/`describe` whi - `%o` - Object. - `%#` - Index of the test case. - `%%` - single percent sign ('%'). This does not consume an argument. +- Unique test titles by injecting properties of test case object - 🖖 Spock like data tables with [Tagged Template Literals](#tagged-template-literal-of-rows) --- @@ -118,6 +119,10 @@ const each = require('jest-each').default; - `%o` - Object. - `%#` - Index of the test case. - `%%` - single percent sign ('%'). This does not consume an argument. + - Or generate unique test titles by injecting properties of test case object with `$variable` + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` + - You can use `$#` to inject the index of the test case + - You cannot use `$variable` with the `printf` formatting except for `%%` - testFn: `Function` the test logic, this is the function that will receive the parameters of each row as function arguments #### `each([parameters]).describe(name, suiteFn)` @@ -140,6 +145,10 @@ const each = require('jest-each').default; - `%o` - Object. - `%#` - Index of the test case. - `%%` - single percent sign ('%'). This does not consume an argument. + - Or generate unique test titles by injecting properties of test case object with `$variable` + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` + - You can use `$#` to inject the index of the test case + - You cannot use `$variable` with the `printf` formatting except for `%%` - 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 @@ -158,6 +167,16 @@ each([ }); ``` +```js +each([ + {a: 1, b: 1, expected: 2}, + {a: 1, b: 2, expected: 3}, + {a: 2, b: 1, expected: 3}, +]).test('returns the result of adding $a to $b', ({a, b, expected}) => { + expect(a + b).toBe(expected); +}); +``` + #### `.test.only(name, fn)` Aliases: `.it.only(name, fn)` or `.fit(name, fn)` @@ -278,6 +297,28 @@ each([ }); ``` +```js +each([ + {a: 1, b: 1, expected: 2}, + {a: 1, b: 2, expected: 3}, + {a: 2, b: 1, expected: 3}, +]).describe('.add($a, $b)', ({a, b, expected}) => { + test(`returns ${expected}`, () => { + expect(a + b).toBe(expected); + }); + + test('does not mutate first arg', () => { + a + b; + expect(a).toBe(a); + }); + + test('does not mutate second arg', () => { + a + b; + expect(b).toBe(b); + }); +}); +``` + #### `.describe.only(name, fn)` Aliases: `.fdescribe(name, fn)` diff --git a/packages/jest-each/src/__tests__/array.test.ts b/packages/jest-each/src/__tests__/array.test.ts index c76609d80505..a7eb3e0e11f4 100644 --- a/packages/jest-each/src/__tests__/array.test.ts +++ b/packages/jest-each/src/__tests__/array.test.ts @@ -291,6 +291,83 @@ describe('jest-each', () => { 10000, ); }); + + test('calls global with title containing object property when using $variable', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)([ + { + a: 'hello', + b: 1, + c: null, + d: undefined, + e: 1.2, + f: {key: 'foo'}, + g: () => {}, + h: [], + i: Infinity, + j: NaN, + }, + { + a: 'world', + b: 1, + c: null, + d: undefined, + e: 1.2, + f: {key: 'bar'}, + g: () => {}, + h: [], + i: Infinity, + j: NaN, + }, + ]); + const testFunction = get(eachObject, keyPath); + testFunction( + 'expected string: %% %%s $a $b $c $d $e $f $f.key $g $h $i $j $#', + noop, + ); + + const globalMock = get(globalTestMocks, keyPath); + expect(globalMock).toHaveBeenCalledTimes(2); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: % %s hello 1 null undefined 1.2 {"key": "foo"} foo [Function g] [] Infinity NaN 0', + expectFunction, + undefined, + ); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: % %s world 1 null undefined 1.2 {"key": "bar"} bar [Function g] [] Infinity NaN 1', + expectFunction, + undefined, + ); + }); + + test('calls global with title containing param values when using both % placeholder and $variable', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)([ + { + a: 'hello', + b: 1, + }, + { + a: 'world', + b: 1, + }, + ]); + const testFunction = get(eachObject, keyPath); + testFunction('expected string: %p %# $a $b $#', noop); + + const globalMock = get(globalTestMocks, keyPath); + expect(globalMock).toHaveBeenCalledTimes(2); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: {"a": "hello", "b": 1} 0 $a $b $#', + expectFunction, + undefined, + ); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: {"a": "world", "b": 1} 1 $a $b $#', + expectFunction, + undefined, + ); + }); }); }); diff --git a/packages/jest-each/src/table/array.ts b/packages/jest-each/src/table/array.ts index fe245e8e77bd..deef22c46e38 100644 --- a/packages/jest-each/src/table/array.ts +++ b/packages/jest-each/src/table/array.ts @@ -10,6 +10,8 @@ import * as util from 'util'; import type {Global} from '@jest/types'; import {format as pretty} from 'pretty-format'; import type {EachTests} from '../bind'; +import type {Templates} from './interpolation'; +import {interpolateVariables} from './interpolation'; const SUPPORTED_PLACEHOLDERS = /%[sdifjoOp]/g; const PRETTY_PLACEHOLDER = '%p'; @@ -18,11 +20,29 @@ const PLACEHOLDER_PREFIX = '%'; const ESCAPED_PLACEHOLDER_PREFIX = /%%/g; const JEST_EACH_PLACEHOLDER_ESCAPE = '@@__JEST_EACH_PLACEHOLDER_ESCAPE__@@'; -export default (title: string, arrayTable: Global.ArrayTable): EachTests => - normaliseTable(arrayTable).map((row, index) => ({ +export default (title: string, arrayTable: Global.ArrayTable): EachTests => { + if (isTemplates(title, arrayTable)) { + return arrayTable.map((template, index) => ({ + arguments: [template], + title: interpolateVariables(title, template, index).replace( + ESCAPED_PLACEHOLDER_PREFIX, + PLACEHOLDER_PREFIX, + ), + })); + } + return normaliseTable(arrayTable).map((row, index) => ({ arguments: row, title: formatTitle(title, row, index), })); +}; + +const isTemplates = ( + title: string, + arrayTable: Global.ArrayTable, +): arrayTable is Templates => + !SUPPORTED_PLACEHOLDERS.test(interpolateEscapedPlaceholders(title)) && + !isTable(arrayTable) && + arrayTable.every(col => col != null && typeof col === 'object'); const normaliseTable = (table: Global.ArrayTable): Global.Table => isTable(table) ? table : table.map(colToRow); diff --git a/packages/jest-each/src/table/interpolation.ts b/packages/jest-each/src/table/interpolation.ts new file mode 100644 index 000000000000..b239878e686b --- /dev/null +++ b/packages/jest-each/src/table/interpolation.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. 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 {isPrimitive} from 'jest-get-type'; +import {format as pretty} from 'pretty-format'; + +export type Template = Record; +export type Templates = Array