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 +
+ +
+ +[![version](https://img.shields.io/npm/v/jest-each.svg?style=flat-square)](https://www.npmjs.com/package/jest-each) [![downloads](https://img.shields.io/npm/dm/jest-each.svg?style=flat-square)](http://npm-stat.com/charts.html?package=jest-each&from=2017-03-21) [![MIT License](https://img.shields.io/npm/l/jest-each.svg?style=flat-square)](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 + +![Current jest tests](assets/default-demo.gif) + +#### Tests can be re-written with jest-each to: + +**`.test`** + +![Current jest tests](assets/test-demo.gif) + +**`.test` with Tagged Template Literals** + +![Current jest tests](assets/tagged-template-literal.gif) + +**`.describe`** + +![Current jest tests](assets/describe-demo.gif) + +## 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); +};