diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17b8943c304d..b3e4e6ff26ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,12 @@
## master
+### Chore & Maintenance
+
+* `[jest-each]` Move jest-each into core Jest ([#6278](https://github.com/facebook/jest/pull/6278))
+
### Fixes
-* `[pretty-format]` Serialize inverse asymmetric matchers correctly
- ([#6272](https://github.com/facebook/jest/pull/6272))
+* `[pretty-format]` Serialize inverse asymmetric matchers correctly ([#6272](https://github.com/facebook/jest/pull/6272))
## 23.0.0
diff --git a/packages/jest-each/.npmignore b/packages/jest-each/.npmignore
new file mode 100644
index 000000000000..537a559e333b
--- /dev/null
+++ b/packages/jest-each/.npmignore
@@ -0,0 +1,4 @@
+**/__tests__/**
+**/__mocks__/**
+src
+assets
diff --git a/packages/jest-each/README.md b/packages/jest-each/README.md
new file mode 100644
index 000000000000..eeae55fef3b9
--- /dev/null
+++ b/packages/jest-each/README.md
@@ -0,0 +1,408 @@
+
+
jest-each
+ Jest Parameterised Testing
+
+
+
+
+[](https://www.npmjs.com/package/jest-each) [](http://npm-stat.com/charts.html?package=jest-each&from=2017-03-21) [](https://github.com/facebook/jest/blob/master/LICENSE)
+
+A parameterised testing library for [Jest](https://facebook.github.io/jest/) inspired by [mocha-each](https://github.com/ryym/mocha-each).
+
+jest-each allows you to provide multiple arguments to your `test`/`describe` which results in the test/suite being run once per row of parameters.
+
+## Features
+
+* `.test` to runs multiple tests with parameterised data
+ * Also under the alias: `.it`
+* `.test.only` to only run the parameterised tests
+ * Also under the aliases: `.it.only` or `.fit`
+* `.test.skip` to skip the parameterised tests
+ * Also under the aliases: `.it.skip` or `.xit` or `.xtest`
+* `.describe` to runs test suites with parameterised data
+* `.describe.only` to only run the parameterised suite of tests
+ * Also under the aliases: `.fdescribe`
+* `.describe.skip` to skip the parameterised suite of tests
+ * Also under the aliases: `.xdescribe`
+* Asynchronous tests with `done`
+* Unique test titles with: [sprintf](https://github.com/alexei/sprintf.js)
+* 🖖 Spock like data tables with [Tagged Template Literals](#tagged-template-literal-of-rows)
+
+---
+
+* [Demo](#demo)
+* [Installation](#installation)
+* [Importing](#importing)
+* APIs
+ * [Array of Rows](#array-of-rows)
+ * [Usage](#usage)
+ * [Tagged Template Literal of rows](#tagged-template-literal-of-rows)
+ * [Usage](#usage-1)
+
+## Demo
+
+#### Tests without jest-each
+
+
+
+#### Tests can be re-written with jest-each to:
+
+**`.test`**
+
+
+
+**`.test` with Tagged Template Literals**
+
+
+
+**`.describe`**
+
+
+
+## Installation
+
+`npm i --save-dev jest-each`
+
+`yarn add -D jest-each`
+
+## Importing
+
+jest-each is a default export so it can be imported with whatever name you like.
+
+```js
+// es6
+import each from 'jest-each';
+
+// es5
+const each = require('jest-each');
+```
+
+## Array of rows
+
+### API
+
+#### `each([parameters]).test(name, testFn)`
+
+##### `each`:
+
+* parameters: `Array` of Arrays with the arguments that are passed into the `testFn` for each row
+
+##### `.test`:
+
+* name: `String` the title of the `test`, use `%s` in the name string to positionally inject parameter values into the test title
+* 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)`
+
+##### `each`:
+
+* parameters: `Array` of Arrays with the arguments that are passed into the `suiteFn` for each row
+
+##### `.describe`:
+
+* name: `String` the title of the `describe`, use `%s` in the name string to positionally inject parameter values into the suite title
+* 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
+
+#### `.test(name, fn)`
+
+Alias: `.it(name, fn)`
+
+```js
+each([[1, 1, 2], [1, 2, 3], [2, 1, 3]]).test(
+ 'returns the result of adding %s to %s',
+ (a, b, expected) => {
+ expect(a + b).toBe(expected);
+ },
+);
+```
+
+#### `.test.only(name, fn)`
+
+Aliases: `.it.only(name, fn)` or `.fit(name, fn)`
+
+```js
+each([[1, 1, 2], [1, 2, 3], [2, 1, 3]]).test.only(
+ 'returns the result of adding %s to %s',
+ (a, b, expected) => {
+ expect(a + b).toBe(expected);
+ },
+);
+```
+
+#### `.test.skip(name, fn)`
+
+Aliases: `.it.skip(name, fn)` or `.xit(name, fn)` or `.xtest(name, fn)`
+
+```js
+each([[1, 1, 2][(1, 2, 3)], [2, 1, 3]]).test.skip(
+ 'returns the result of adding %s to %s',
+ (a, b, expected) => {
+ expect(a + b).toBe(expected);
+ },
+);
+```
+
+#### Asynchronous `.test(name, fn(done))`
+
+Alias: `.it(name, fn(done))`
+
+```js
+each([['hello'], ['mr'], ['spy']]).test(
+ 'gives 007 secret message ',
+ (str, done) => {
+ const asynchronousSpy = message => {
+ expect(message).toBe(str);
+ done();
+ };
+ callSomeAsynchronousFunction(asynchronousSpy)(str);
+ },
+);
+```
+
+#### `.describe(name, fn)`
+
+```js
+each([[1, 1, 2], [1, 2, 3], [2, 1, 3]]).describe(
+ '.add(%s, %s)',
+ (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)`
+
+```js
+each([[1, 1, 2], [1, 2, 3], [2, 1, 3]]).describe.only(
+ '.add(%s, %s)',
+ (a, b, expected) => {
+ test(`returns ${expected}`, () => {
+ expect(a + b).toBe(expected);
+ });
+ },
+);
+```
+
+#### `.describe.skip(name, fn)`
+
+Aliases: `.xdescribe(name, fn)`
+
+```js
+each([[1, 1, 2], [1, 2, 3], [2, 1, 3]]).describe.skip(
+ '.add(%s, %s)',
+ (a, b, expected) => {
+ test(`returns ${expected}`, () => {
+ expect(a + b).toBe(expected);
+ });
+ },
+);
+```
+
+---
+
+## Tagged Template Literal of rows
+
+### API
+
+#### `each[tagged template].test(name, suiteFn)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.test('returns $expected when adding $a to $b', ({a, b, expected}) => {
+ expect(a + b).toBe(expected);
+});
+```
+
+##### `each` takes a tagged template string with:
+
+* First row of variable name column headings seperated with `|`
+* One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax.
+
+##### `.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
+* 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)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.describe('$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);
+ });
+});
+```
+
+##### `each` takes a tagged template string with:
+
+* First row of variable name column headings seperated with `|`
+* One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax.
+
+##### `.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
+* 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
+
+#### `.test(name, fn)`
+
+Alias: `.it(name, fn)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.test('returns $expected when 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)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.test.only('returns $expected when adding $a to $b', ({a, b, expected}) => {
+ expect(a + b).toBe(expected);
+});
+```
+
+#### `.test.skip(name, fn)`
+
+Aliases: `.it.skip(name, fn)` or `.xit(name, fn)` or `.xtest(name, fn)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.test.skip('returns $expected when adding $a to $b', ({a, b, expected}) => {
+ expect(a + b).toBe(expected);
+});
+```
+
+#### Asynchronous `.test(name, fn(done))`
+
+Alias: `.it(name, fn(done))`
+
+```js
+each`
+ str
+ ${'hello'}
+ ${'mr'}
+ ${'spy'}
+`.test('gives 007 secret message: $str', ({str}, done) => {
+ const asynchronousSpy = message => {
+ expect(message).toBe(str);
+ done();
+ };
+ callSomeAsynchronousFunction(asynchronousSpy)(str);
+});
+```
+
+#### `.describe(name, fn)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.describe('$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)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.describe.only('$a + $b', ({a, b, expected}) => {
+ test(`returns ${expected}`, () => {
+ expect(a + b).toBe(expected);
+ });
+});
+```
+
+#### `.describe.skip(name, fn)`
+
+Aliases: `.xdescribe(name, fn)`
+
+```js
+each`
+ a | b | expected
+ ${1} | ${1} | ${2}
+ ${1} | ${2} | ${3}
+ ${2} | ${1} | ${3}
+`.describe.skip('$a + $b', ({a, b, expected}) => {
+ test(`returns ${expected}`, () => {
+ expect(a + b).toBe(expected);
+ });
+});
+```
+
+## License
+
+MIT
diff --git a/packages/jest-each/assets/default-demo.gif b/packages/jest-each/assets/default-demo.gif
new file mode 100644
index 000000000000..15c34d58a484
Binary files /dev/null and b/packages/jest-each/assets/default-demo.gif differ
diff --git a/packages/jest-each/assets/describe-demo.gif b/packages/jest-each/assets/describe-demo.gif
new file mode 100644
index 000000000000..e0745ee02f6f
Binary files /dev/null and b/packages/jest-each/assets/describe-demo.gif differ
diff --git a/packages/jest-each/assets/tagged-template-literal.gif b/packages/jest-each/assets/tagged-template-literal.gif
new file mode 100644
index 000000000000..94835684a547
Binary files /dev/null and b/packages/jest-each/assets/tagged-template-literal.gif differ
diff --git a/packages/jest-each/assets/test-demo.gif b/packages/jest-each/assets/test-demo.gif
new file mode 100644
index 000000000000..9686bb9452a3
Binary files /dev/null and b/packages/jest-each/assets/test-demo.gif differ
diff --git a/packages/jest-each/package.json b/packages/jest-each/package.json
new file mode 100644
index 000000000000..c27eadd98b51
--- /dev/null
+++ b/packages/jest-each/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "jest-each",
+ "version": "23.0.0",
+ "description": "Parameterised tests for Jest",
+ "main": "build/index.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/facebook/jest.git"
+ },
+ "keywords": [
+ "jest",
+ "parameterised",
+ "test",
+ "each"
+ ],
+ "author": "Matt Phillips (mattphillips)",
+ "license": "MIT"
+}
diff --git a/packages/jest-each/src/__tests__/__snapshots__/template.test.js.snap b/packages/jest-each/src/__tests__/__snapshots__/template.test.js.snap
new file mode 100644
index 000000000000..0d04a0bafa8c
--- /dev/null
+++ b/packages/jest-each/src/__tests__/__snapshots__/template.test.js.snap
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`jest-each .describe throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .describe throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
+
+exports[`jest-each .describe.only throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .describe.only throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
+
+exports[`jest-each .fdescribe throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .fdescribe throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
+
+exports[`jest-each .fit throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .fit throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
+
+exports[`jest-each .it throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .it throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
+
+exports[`jest-each .it.only throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .it.only throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
+
+exports[`jest-each .test throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .test throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
+
+exports[`jest-each .test.only throws error when there are fewer arguments than headings over multiple rows 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1,1,1,1"
+`;
+
+exports[`jest-each .test.only throws error when there are fewer arguments than headings when given one row 1`] = `
+"Tagged Template Literal test error:
+Not enough arguments supplied for given headings: a | b | expected
+Received: 0,1"
+`;
diff --git a/packages/jest-each/src/__tests__/array.test.js b/packages/jest-each/src/__tests__/array.test.js
new file mode 100644
index 000000000000..eb24d76efecf
--- /dev/null
+++ b/packages/jest-each/src/__tests__/array.test.js
@@ -0,0 +1,200 @@
+import each from '../';
+
+const noop = () => {};
+const expectFunction = expect.any(Function);
+
+const get = (object, lensPath) =>
+ lensPath.reduce((acc, key) => acc[key], object);
+
+describe('jest-each', () => {
+ [
+ ['test'],
+ ['test', 'only'],
+ ['it'],
+ ['fit'],
+ ['it', 'only'],
+ ['describe'],
+ ['fdescribe'],
+ ['describe', 'only'],
+ ].forEach(keyPath => {
+ describe(`.${keyPath.join('.')}`, () => {
+ const getGlobalTestMocks = () => {
+ const globals = {
+ describe: jest.fn(),
+ fdescribe: jest.fn(),
+ fit: jest.fn(),
+ it: jest.fn(),
+ test: jest.fn(),
+ };
+ globals.test.only = jest.fn();
+ globals.it.only = jest.fn();
+ globals.describe.only = jest.fn();
+ return globals;
+ };
+
+ test('calls global with given title', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)([[]]);
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(1);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with given title when multiple tests cases exist', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)([[], []]);
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with title containing param values when using sprintf format', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)([
+ ['hello', 1],
+ ['world', 2],
+ ]);
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string: %s %s', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: hello 1',
+ expectFunction,
+ );
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: world 2',
+ expectFunction,
+ );
+ });
+
+ test('calls global with cb function containing all parameters of each test case', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const testCallBack = jest.fn();
+ const eachObject = each.withGlobal(globalTestMocks)([
+ ['hello', 'world'],
+ ['joe', 'bloggs'],
+ ]);
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', testCallBack);
+
+ const globalMock = get(globalTestMocks, keyPath);
+
+ globalMock.mock.calls[0][1]();
+ expect(testCallBack).toHaveBeenCalledTimes(1);
+ expect(testCallBack).toHaveBeenCalledWith('hello', 'world');
+
+ globalMock.mock.calls[1][1]();
+ expect(testCallBack).toHaveBeenCalledTimes(2);
+ expect(testCallBack).toHaveBeenCalledWith('joe', 'bloggs');
+ });
+
+ test('calls global with async done when cb function has more args than params of given test row', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)([['hello']]);
+
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', (hello, done) => {
+ expect(hello).toBe('hello');
+ expect(done).toBe('DONE');
+ });
+ get(globalTestMocks, keyPath).mock.calls[0][1]('DONE');
+ });
+ });
+ });
+
+ [
+ ['xtest'],
+ ['test', 'skip'],
+ ['xit'],
+ ['it', 'skip'],
+ ['xdescribe'],
+ ['describe', 'skip'],
+ ].forEach(keyPath => {
+ describe(`.${keyPath.join('.')}`, () => {
+ const getGlobalTestMocks = () => {
+ const globals = {
+ describe: {
+ skip: jest.fn(),
+ },
+ it: {
+ skip: jest.fn(),
+ },
+ test: {
+ skip: jest.fn(),
+ },
+ xdescribe: jest.fn(),
+ xit: jest.fn(),
+ xtest: jest.fn(),
+ };
+ return globals;
+ };
+
+ test('calls global with given title', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)([[]]);
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(1);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with given title when multiple tests cases exist', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)([[], []]);
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with title containing param values when using sprintf format', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)([
+ ['hello', 1],
+ ['world', 2],
+ ]);
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string: %s %s', () => {});
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: hello 1',
+ expectFunction,
+ );
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: world 2',
+ expectFunction,
+ );
+ });
+ });
+ });
+});
diff --git a/packages/jest-each/src/__tests__/index.test.js b/packages/jest-each/src/__tests__/index.test.js
new file mode 100644
index 000000000000..6c63790edde2
--- /dev/null
+++ b/packages/jest-each/src/__tests__/index.test.js
@@ -0,0 +1,25 @@
+import each from '../';
+
+describe('array', () => {
+ describe('.add', () => {
+ each([[0, 0, 0], [0, 1, 1], [1, 1, 2]]).test(
+ 'returns the result of adding %s to %s',
+ (a, b, expected) => {
+ expect(a + b).toBe(expected);
+ },
+ );
+ });
+});
+
+describe('template', () => {
+ describe('.add', () => {
+ each`
+ a | b | expected
+ ${0} | ${0} | ${0}
+ ${0} | ${1} | ${1}
+ ${1} | ${1} | ${2}
+ `.test('returns $expected when given $a and $b', ({a, b, expected}) => {
+ expect(a + b).toBe(expected);
+ });
+ });
+});
diff --git a/packages/jest-each/src/__tests__/template.test.js b/packages/jest-each/src/__tests__/template.test.js
new file mode 100644
index 000000000000..2c7da7d1d045
--- /dev/null
+++ b/packages/jest-each/src/__tests__/template.test.js
@@ -0,0 +1,262 @@
+import each from '../';
+
+const noop = () => {};
+const expectFunction = expect.any(Function);
+
+const get = (object, lensPath) =>
+ lensPath.reduce((acc, key) => acc[key], object);
+
+describe('jest-each', () => {
+ [
+ ['test'],
+ ['test', 'only'],
+ ['it'],
+ ['fit'],
+ ['it', 'only'],
+ ['describe'],
+ ['fdescribe'],
+ ['describe', 'only'],
+ ].forEach(keyPath => {
+ describe(`.${keyPath.join('.')}`, () => {
+ const getGlobalTestMocks = () => {
+ const globals = {
+ describe: jest.fn(),
+ fdescribe: jest.fn(),
+ fit: jest.fn(),
+ it: jest.fn(),
+ test: jest.fn(),
+ };
+ globals.test.only = jest.fn();
+ globals.it.only = jest.fn();
+ globals.describe.only = jest.fn();
+ return globals;
+ };
+
+ test('throws error when there are fewer arguments than headings when given one row', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} |
+ `;
+ const testFunction = get(eachObject, keyPath);
+ const testCallBack = jest.fn();
+ testFunction('this will blow up :(', testCallBack);
+
+ const globalMock = get(globalTestMocks, keyPath);
+
+ expect(() =>
+ globalMock.mock.calls[0][1](),
+ ).toThrowErrorMatchingSnapshot();
+ expect(testCallBack).not.toHaveBeenCalled();
+ });
+
+ test('throws error when there are fewer arguments than headings over multiple rows', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ ${1} | ${1} |
+ `;
+ const testFunction = get(eachObject, keyPath);
+ const testCallBack = jest.fn();
+ testFunction('this will blow up :(', testCallBack);
+
+ const globalMock = get(globalTestMocks, keyPath);
+
+ expect(() =>
+ globalMock.mock.calls[0][1](),
+ ).toThrowErrorMatchingSnapshot();
+ expect(testCallBack).not.toHaveBeenCalled();
+ });
+
+ test('calls global with given title', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(1);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with given title when multiple tests cases exist', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ ${1} | ${1} | ${2}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with title containing param values when using $variable format', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ ${1} | ${1} | ${2}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string: a=$a, b=$b, expected=$expected', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: a=0, b=1, expected=1',
+ expectFunction,
+ );
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: a=1, b=1, expected=2',
+ expectFunction,
+ );
+ });
+
+ test('calls global with cb function with object built from tabel headings and values', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const testCallBack = jest.fn();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ ${1} | ${1} | ${2}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', testCallBack);
+
+ const globalMock = get(globalTestMocks, keyPath);
+
+ globalMock.mock.calls[0][1]();
+ expect(testCallBack).toHaveBeenCalledTimes(1);
+ expect(testCallBack).toHaveBeenCalledWith({a: 0, b: 1, expected: 1});
+
+ globalMock.mock.calls[1][1]();
+ expect(testCallBack).toHaveBeenCalledTimes(2);
+ expect(testCallBack).toHaveBeenCalledWith({a: 1, b: 1, expected: 2});
+ });
+
+ test('calls global with async done when cb function has more than one argument', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', ({a, b, expected}, done) => {
+ expect(a).toBe(0);
+ expect(b).toBe(1);
+ expect(expected).toBe(1);
+ expect(done).toBe('DONE');
+ });
+ get(globalTestMocks, keyPath).mock.calls[0][1]('DONE');
+ });
+ });
+ });
+
+ [
+ ['xtest'],
+ ['test', 'skip'],
+ ['xit'],
+ ['it', 'skip'],
+ ['xdescribe'],
+ ['describe', 'skip'],
+ ].forEach(keyPath => {
+ describe(`.${keyPath.join('.')}`, () => {
+ const getGlobalTestMocks = () => {
+ const globals = {
+ describe: {
+ skip: jest.fn(),
+ },
+ it: {
+ skip: jest.fn(),
+ },
+ test: {
+ skip: jest.fn(),
+ },
+ xdescribe: jest.fn(),
+ xit: jest.fn(),
+ xtest: jest.fn(),
+ };
+ return globals;
+ };
+
+ test('calls global with given title', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(1);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with given title when multiple tests cases exist', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ ${1} | ${1} | ${2}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string',
+ expectFunction,
+ );
+ });
+
+ test('calls global with title containing param values when using $variable format', () => {
+ const globalTestMocks = getGlobalTestMocks();
+ const eachObject = each.withGlobal(globalTestMocks)`
+ a | b | expected
+ ${0} | ${1} | ${1}
+ ${1} | ${1} | ${2}
+ `;
+ const testFunction = get(eachObject, keyPath);
+ testFunction('expected string: a=$a, b=$b, expected=$expected', noop);
+
+ const globalMock = get(globalTestMocks, keyPath);
+ expect(globalMock).toHaveBeenCalledTimes(2);
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: a=0, b=1, expected=1',
+ expectFunction,
+ );
+ expect(globalMock).toHaveBeenCalledWith(
+ 'expected string: a=1, b=1, expected=2',
+ expectFunction,
+ );
+ });
+ });
+ });
+});
diff --git a/packages/jest-each/src/array.js b/packages/jest-each/src/array.js
new file mode 100644
index 000000000000..43e84b54c913
--- /dev/null
+++ b/packages/jest-each/src/array.js
@@ -0,0 +1,40 @@
+import util from 'util';
+
+export default defaultGlobal => parameterRows => {
+ const tests = parameterisedTests(parameterRows);
+
+ const globalTest = defaultGlobal.test;
+ const test = tests(globalTest);
+ test.skip = tests(globalTest.skip);
+ test.only = tests(globalTest.only);
+
+ const globalIt = defaultGlobal.it;
+ const it = tests(globalIt);
+ it.skip = tests(globalIt.skip);
+ it.only = tests(globalIt.only);
+
+ const xtest = tests(defaultGlobal.xtest);
+ const xit = tests(defaultGlobal.xit);
+ const fit = tests(defaultGlobal.fit);
+
+ const globalDescribe = defaultGlobal.describe;
+ const describe = tests(globalDescribe);
+ describe.skip = tests(globalDescribe.skip);
+ describe.only = tests(globalDescribe.only);
+ const fdescribe = tests(defaultGlobal.fdescribe);
+ const xdescribe = tests(defaultGlobal.xdescribe);
+
+ return {describe, fdescribe, fit, it, test, xdescribe, xit, xtest};
+};
+
+const parameterisedTests = parameterRows => globalCb => (title, test) => {
+ parameterRows.forEach(params =>
+ globalCb(util.format(title, ...params), applyTestParams(params, test)),
+ );
+};
+
+const applyTestParams = (params, test) => {
+ if (params.length < test.length) return done => test(...params, done);
+
+ return () => test(...params);
+};
diff --git a/packages/jest-each/src/index.js b/packages/jest-each/src/index.js
new file mode 100644
index 000000000000..f7bd69994ef6
--- /dev/null
+++ b/packages/jest-each/src/index.js
@@ -0,0 +1,20 @@
+import arrayEach from './array';
+import templateEach from './template';
+
+const each = (...args) => {
+ if (args.length > 1) {
+ return templateEach(global)(...args);
+ }
+
+ return arrayEach(global)(...args);
+};
+
+each.withGlobal = g => (...args) => {
+ if (args.length > 1) {
+ return templateEach(g)(...args);
+ }
+
+ return arrayEach(g)(...args);
+};
+
+export default each;
diff --git a/packages/jest-each/src/template.js b/packages/jest-each/src/template.js
new file mode 100644
index 000000000000..d741a1f5334f
--- /dev/null
+++ b/packages/jest-each/src/template.js
@@ -0,0 +1,88 @@
+export default defaultGlobal => ([headings], ...data) => {
+ const keys = getHeadingKeys(headings);
+
+ const keysLength = keys.length;
+
+ if (data.length % keysLength !== 0) {
+ const errorFunction = notEnoughDataError(keys, data);
+
+ const test = errorFunction(defaultGlobal.test);
+ test.only = errorFunction(defaultGlobal.test.only);
+
+ const it = errorFunction(defaultGlobal.it);
+ it.only = errorFunction(defaultGlobal.it.only);
+
+ const fit = errorFunction(defaultGlobal.fit);
+
+ const describe = errorFunction(defaultGlobal.describe);
+ describe.only = errorFunction(defaultGlobal.describe.only);
+
+ const fdescribe = errorFunction(defaultGlobal.fdescribe);
+
+ return {describe, fdescribe, fit, it, test};
+ }
+
+ const parameterRows = Array.from({length: data.length / keysLength})
+ .map((_, index) =>
+ data.slice(index * keysLength, index * keysLength + keysLength),
+ )
+ .map(row =>
+ row.reduce(
+ (acc, value, index) => Object.assign({}, acc, {[keys[index]]: value}),
+ {},
+ ),
+ );
+
+ const tests = parameterisedTests(parameterRows);
+
+ const globalTest = defaultGlobal.test;
+ const test = tests(globalTest);
+ test.skip = tests(globalTest.skip);
+ test.only = tests(globalTest.only);
+
+ const globalIt = defaultGlobal.it;
+ const it = tests(globalIt);
+ it.skip = tests(globalIt.skip);
+ it.only = tests(globalIt.only);
+
+ const xtest = tests(defaultGlobal.xtest);
+ const xit = tests(defaultGlobal.xit);
+ const fit = tests(defaultGlobal.fit);
+
+ const globalDescribe = defaultGlobal.describe;
+ const describe = tests(globalDescribe);
+ describe.skip = tests(globalDescribe.skip);
+ describe.only = tests(globalDescribe.only);
+ const fdescribe = tests(defaultGlobal.fdescribe);
+ const xdescribe = tests(defaultGlobal.xdescribe);
+
+ return {describe, fdescribe, fit, it, test, xdescribe, xit, xtest};
+};
+
+const notEnoughDataError = (keys, data) => cb => title =>
+ cb(title, () => {
+ throw new Error(
+ `Tagged Template Literal test error:\nNot enough arguments supplied for given headings: ${keys.join(
+ ' | ',
+ )}\nReceived: ${data}`,
+ );
+ });
+
+const getHeadingKeys = headings => headings.replace(/\s/g, '').split('|');
+
+const parameterisedTests = parameterRows => globalCb => (title, test) => {
+ parameterRows.forEach(params =>
+ globalCb(interpolate(title, params), applyTestParams(params, test)),
+ );
+};
+
+const interpolate = (title, data) => {
+ const keys = Object.keys(data);
+ return keys.reduce((acc, key) => acc.replace('$' + key, data[key]), title);
+};
+
+const applyTestParams = (params, test) => {
+ if (test.length > 1) return done => test(params, done);
+
+ return () => test(params);
+};