Skip to content

Commit 7e610ac

Browse files
frozenbonitomprinc
authored andcommitted
feat(jest-each): add support for interpolation with object properties (jestjs#11388)
1 parent 67fdb75 commit 7e610ac

7 files changed

Lines changed: 268 additions & 77 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- `[jest-core]` Add support for `globalSetup` and `globalTeardown` written in ESM ([#11267](https://github.com/facebook/jest/pull/11267))
2020
- `[jest-core]` Add support for `watchPlugins` written in ESM ([#11315](https://github.com/facebook/jest/pull/11315))
2121
- `[jest-core]` Add support for `runner` written in ESM ([#11232](https://github.com/facebook/jest/pull/11232))
22+
- `[jest-each]` Add support for interpolation with object properties ([#11388](https://github.com/facebook/jest/pull/11388))
2223
- `[jest-environment-node]` Add AbortController to globals ([#11182](https://github.com/facebook/jest/pull/11182))
2324
- `[@jest/fake-timers]` Update to `@sinonjs/fake-timers` to v7 ([#11198](https://github.com/facebook/jest/pull/11198))
2425
- `[jest-haste-map]` Handle injected scm clocks ([#10966](https://github.com/facebook/jest/pull/10966))

docs/GlobalAPI.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ Use `describe.each` if you keep duplicating the same test suites with different
245245
- `%o` - Object.
246246
- `%#` - Index of the test case.
247247
- `%%` - single percent sign ('%'). This does not consume an argument.
248+
- Or generate unique test titles by injecting properties of test case object with `$variable`
249+
- To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value`
250+
- You can use `$#` to inject the index of the test case
251+
- You cannot use `$variable` with the `printf` formatting except for `%%`
248252
- `fn`: `Function` the suite of tests to be ran, this is the function that will receive the parameters in each row as function arguments.
249253
- 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._
250254

@@ -270,6 +274,26 @@ describe.each([
270274
});
271275
```
272276

277+
```js
278+
describe.each([
279+
{a: 1, b: 1, expected: 2},
280+
{a: 1, b: 2, expected: 3},
281+
{a: 2, b: 1, expected: 3},
282+
])('.add($a, $b)', ({a, b, expected}) => {
283+
test(`returns ${expected}`, () => {
284+
expect(a + b).toBe(expected);
285+
});
286+
287+
test(`returned value not be greater than ${expected}`, () => {
288+
expect(a + b).not.toBeGreaterThan(expected);
289+
});
290+
291+
test(`returned value not be less than ${expected}`, () => {
292+
expect(a + b).not.toBeLessThan(expected);
293+
});
294+
});
295+
```
296+
273297
#### 2. `` describe.each`table`(name, fn, timeout) ``
274298

275299
- `table`: `Tagged Template Literal`
@@ -655,6 +679,10 @@ Use `test.each` if you keep duplicating the same test with different data. `test
655679
- `%o` - Object.
656680
- `%#` - Index of the test case.
657681
- `%%` - single percent sign ('%'). This does not consume an argument.
682+
- Or generate unique test titles by injecting properties of test case object with `$variable`
683+
- To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value`
684+
- You can use `$#` to inject the index of the test case
685+
- You cannot use `$variable` with the `printf` formatting except for `%%`
658686
- `fn`: `Function` the test to be ran, this is the function that will receive the parameters in each row as function arguments.
659687
- 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._
660688

@@ -670,6 +698,16 @@ test.each([
670698
});
671699
```
672700

701+
```js
702+
test.each([
703+
{a: 1, b: 1, expected: 2},
704+
{a: 1, b: 2, expected: 3},
705+
{a: 2, b: 1, expected: 3},
706+
])('.add($a, $b)', ({a, b, expected}) => {
707+
expect(a + b).toBe(expected);
708+
});
709+
```
710+
673711
#### 2. `` test.each`table`(name, fn, timeout) ``
674712

675713
- `table`: `Tagged Template Literal`

packages/jest-each/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jest-each allows you to provide multiple arguments to your `test`/`describe` whi
4141
- `%o` - Object.
4242
- `%#` - Index of the test case.
4343
- `%%` - single percent sign ('%'). This does not consume an argument.
44+
- Unique test titles by injecting properties of test case object
4445
- 🖖 Spock like data tables with [Tagged Template Literals](#tagged-template-literal-of-rows)
4546

4647
---
@@ -118,6 +119,10 @@ const each = require('jest-each').default;
118119
- `%o` - Object.
119120
- `%#` - Index of the test case.
120121
- `%%` - single percent sign ('%'). This does not consume an argument.
122+
- Or generate unique test titles by injecting properties of test case object with `$variable`
123+
- To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value`
124+
- You can use `$#` to inject the index of the test case
125+
- You cannot use `$variable` with the `printf` formatting except for `%%`
121126
- testFn: `Function` the test logic, this is the function that will receive the parameters of each row as function arguments
122127

123128
#### `each([parameters]).describe(name, suiteFn)`
@@ -140,6 +145,10 @@ const each = require('jest-each').default;
140145
- `%o` - Object.
141146
- `%#` - Index of the test case.
142147
- `%%` - single percent sign ('%'). This does not consume an argument.
148+
- Or generate unique test titles by injecting properties of test case object with `$variable`
149+
- To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value`
150+
- You can use `$#` to inject the index of the test case
151+
- You cannot use `$variable` with the `printf` formatting except for `%%`
143152
- 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
144153

145154
### Usage
@@ -158,6 +167,16 @@ each([
158167
});
159168
```
160169

170+
```js
171+
each([
172+
{a: 1, b: 1, expected: 2},
173+
{a: 1, b: 2, expected: 3},
174+
{a: 2, b: 1, expected: 3},
175+
]).test('returns the result of adding $a to $b', ({a, b, expected}) => {
176+
expect(a + b).toBe(expected);
177+
});
178+
```
179+
161180
#### `.test.only(name, fn)`
162181

163182
Aliases: `.it.only(name, fn)` or `.fit(name, fn)`
@@ -278,6 +297,28 @@ each([
278297
});
279298
```
280299

300+
```js
301+
each([
302+
{a: 1, b: 1, expected: 2},
303+
{a: 1, b: 2, expected: 3},
304+
{a: 2, b: 1, expected: 3},
305+
]).describe('.add($a, $b)', ({a, b, expected}) => {
306+
test(`returns ${expected}`, () => {
307+
expect(a + b).toBe(expected);
308+
});
309+
310+
test('does not mutate first arg', () => {
311+
a + b;
312+
expect(a).toBe(a);
313+
});
314+
315+
test('does not mutate second arg', () => {
316+
a + b;
317+
expect(b).toBe(b);
318+
});
319+
});
320+
```
321+
281322
#### `.describe.only(name, fn)`
282323

283324
Aliases: `.fdescribe(name, fn)`

packages/jest-each/src/__tests__/array.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,83 @@ describe('jest-each', () => {
291291
10000,
292292
);
293293
});
294+
295+
test('calls global with title containing object property when using $variable', () => {
296+
const globalTestMocks = getGlobalTestMocks();
297+
const eachObject = each.withGlobal(globalTestMocks)([
298+
{
299+
a: 'hello',
300+
b: 1,
301+
c: null,
302+
d: undefined,
303+
e: 1.2,
304+
f: {key: 'foo'},
305+
g: () => {},
306+
h: [],
307+
i: Infinity,
308+
j: NaN,
309+
},
310+
{
311+
a: 'world',
312+
b: 1,
313+
c: null,
314+
d: undefined,
315+
e: 1.2,
316+
f: {key: 'bar'},
317+
g: () => {},
318+
h: [],
319+
i: Infinity,
320+
j: NaN,
321+
},
322+
]);
323+
const testFunction = get(eachObject, keyPath);
324+
testFunction(
325+
'expected string: %% %%s $a $b $c $d $e $f $f.key $g $h $i $j $#',
326+
noop,
327+
);
328+
329+
const globalMock = get(globalTestMocks, keyPath);
330+
expect(globalMock).toHaveBeenCalledTimes(2);
331+
expect(globalMock).toHaveBeenCalledWith(
332+
'expected string: % %s hello 1 null undefined 1.2 {"key": "foo"} foo [Function g] [] Infinity NaN 0',
333+
expectFunction,
334+
undefined,
335+
);
336+
expect(globalMock).toHaveBeenCalledWith(
337+
'expected string: % %s world 1 null undefined 1.2 {"key": "bar"} bar [Function g] [] Infinity NaN 1',
338+
expectFunction,
339+
undefined,
340+
);
341+
});
342+
343+
test('calls global with title containing param values when using both % placeholder and $variable', () => {
344+
const globalTestMocks = getGlobalTestMocks();
345+
const eachObject = each.withGlobal(globalTestMocks)([
346+
{
347+
a: 'hello',
348+
b: 1,
349+
},
350+
{
351+
a: 'world',
352+
b: 1,
353+
},
354+
]);
355+
const testFunction = get(eachObject, keyPath);
356+
testFunction('expected string: %p %# $a $b $#', noop);
357+
358+
const globalMock = get(globalTestMocks, keyPath);
359+
expect(globalMock).toHaveBeenCalledTimes(2);
360+
expect(globalMock).toHaveBeenCalledWith(
361+
'expected string: {"a": "hello", "b": 1} 0 $a $b $#',
362+
expectFunction,
363+
undefined,
364+
);
365+
expect(globalMock).toHaveBeenCalledWith(
366+
'expected string: {"a": "world", "b": 1} 1 $a $b $#',
367+
expectFunction,
368+
undefined,
369+
);
370+
});
294371
});
295372
});
296373

packages/jest-each/src/table/array.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import * as util from 'util';
1010
import type {Global} from '@jest/types';
1111
import {format as pretty} from 'pretty-format';
1212
import type {EachTests} from '../bind';
13+
import type {Templates} from './interpolation';
14+
import {interpolateVariables} from './interpolation';
1315

1416
const SUPPORTED_PLACEHOLDERS = /%[sdifjoOp]/g;
1517
const PRETTY_PLACEHOLDER = '%p';
@@ -18,11 +20,29 @@ const PLACEHOLDER_PREFIX = '%';
1820
const ESCAPED_PLACEHOLDER_PREFIX = /%%/g;
1921
const JEST_EACH_PLACEHOLDER_ESCAPE = '@@__JEST_EACH_PLACEHOLDER_ESCAPE__@@';
2022

21-
export default (title: string, arrayTable: Global.ArrayTable): EachTests =>
22-
normaliseTable(arrayTable).map((row, index) => ({
23+
export default (title: string, arrayTable: Global.ArrayTable): EachTests => {
24+
if (isTemplates(title, arrayTable)) {
25+
return arrayTable.map((template, index) => ({
26+
arguments: [template],
27+
title: interpolateVariables(title, template, index).replace(
28+
ESCAPED_PLACEHOLDER_PREFIX,
29+
PLACEHOLDER_PREFIX,
30+
),
31+
}));
32+
}
33+
return normaliseTable(arrayTable).map((row, index) => ({
2334
arguments: row,
2435
title: formatTitle(title, row, index),
2536
}));
37+
};
38+
39+
const isTemplates = (
40+
title: string,
41+
arrayTable: Global.ArrayTable,
42+
): arrayTable is Templates =>
43+
!SUPPORTED_PLACEHOLDERS.test(interpolateEscapedPlaceholders(title)) &&
44+
!isTable(arrayTable) &&
45+
arrayTable.every(col => col != null && typeof col === 'object');
2646

2747
const normaliseTable = (table: Global.ArrayTable): Global.Table =>
2848
isTable(table) ? table : table.map(colToRow);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import {isPrimitive} from 'jest-get-type';
10+
import {format as pretty} from 'pretty-format';
11+
12+
export type Template = Record<string, unknown>;
13+
export type Templates = Array<Template>;
14+
export type Headings = Array<string>;
15+
16+
export const interpolateVariables = (
17+
title: string,
18+
template: Template,
19+
index: number,
20+
): string =>
21+
Object.keys(template)
22+
.reduce(getMatchingKeyPaths(title), []) // aka flatMap
23+
.reduce(replaceKeyPathWithValue(template), title)
24+
.replace('$#', '' + index);
25+
26+
const getMatchingKeyPaths = (title: string) => (
27+
matches: Headings,
28+
key: string,
29+
) => matches.concat(title.match(new RegExp(`\\$${key}[\\.\\w]*`, 'g')) || []);
30+
31+
const replaceKeyPathWithValue = (template: Template) => (
32+
title: string,
33+
match: string,
34+
) => {
35+
const keyPath = match.replace('$', '').split('.');
36+
const value = getPath(template, keyPath);
37+
38+
if (isPrimitive(value)) {
39+
return title.replace(match, String(value));
40+
}
41+
return title.replace(match, pretty(value, {maxDepth: 1, min: true}));
42+
};
43+
44+
/* eslint import/export: 0*/
45+
export function getPath<
46+
Obj extends Template,
47+
A extends keyof Obj,
48+
B extends keyof Obj[A],
49+
C extends keyof Obj[A][B],
50+
D extends keyof Obj[A][B][C],
51+
E extends keyof Obj[A][B][C][D]
52+
>(obj: Obj, path: [A, B, C, D, E]): Obj[A][B][C][D][E];
53+
export function getPath<
54+
Obj extends Template,
55+
A extends keyof Obj,
56+
B extends keyof Obj[A],
57+
C extends keyof Obj[A][B],
58+
D extends keyof Obj[A][B][C]
59+
>(obj: Obj, path: [A, B, C, D]): Obj[A][B][C][D];
60+
export function getPath<
61+
Obj extends Template,
62+
A extends keyof Obj,
63+
B extends keyof Obj[A],
64+
C extends keyof Obj[A][B]
65+
>(obj: Obj, path: [A, B, C]): Obj[A][B][C];
66+
export function getPath<
67+
Obj extends Template,
68+
A extends keyof Obj,
69+
B extends keyof Obj[A]
70+
>(obj: Obj, path: [A, B]): Obj[A][B];
71+
export function getPath<Obj extends Template, A extends keyof Obj>(
72+
obj: Obj,
73+
path: [A],
74+
): Obj[A];
75+
export function getPath<Obj extends Template>(
76+
obj: Obj,
77+
path: Array<string>,
78+
): unknown;
79+
export function getPath(
80+
template: Template,
81+
[head, ...tail]: Array<string>,
82+
): unknown {
83+
if (!head || !template.hasOwnProperty || !template.hasOwnProperty(head))
84+
return template;
85+
return getPath(template[head] as Template, tail);
86+
}

0 commit comments

Comments
 (0)