diff --git a/CHANGELOG.md b/CHANGELOG.md index b68bdb936a0b..1999f1519ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +* `[jest-jasmine2]` Add data driven testing based on `jest-each` + ([#6102](https://github.com/facebook/jest/pull/6102)) * `[jest-matcher-utils]` Change "suggest to equal" message to be more advisory ([#6103](https://github.com/facebook/jest/issues/6103)) * `[jest-message-util]` Don't ignore messages with `vendor` anymore diff --git a/docs/GlobalAPI.md b/docs/GlobalAPI.md index 79d292f0dd2c..4c31a99530b3 100644 --- a/docs/GlobalAPI.md +++ b/docs/GlobalAPI.md @@ -256,6 +256,77 @@ describe('binaryStringToNumber', () => { }); ``` +### `describe.each(table)(name, fn)` + +Use `describe.each` if you keep duplicating the same test suites with different +data. `describe.each` allows you to write the test suite once and pass data in. + +`describe.each` is available with two APIs: + +#### 1. `describe.each(table)(name, fn)` + +* `table`: `Array` of Arrays with the arguments that are passed into the `fn` + for each row. +* `name`: `String` the title of the test suite, use `%s` to positionally inject + test data into the suite title. +* `fn`: `Function` the suite of tests to be ran, this is the function that will + receive the parameters in each row as function arguments. + +Example: + +```js +describe.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( + '.add(%s, %s)', + (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) `` + +* `table`: `Tagged Template Literal` + * First row of variable name column headings separated with `|` + * One or more subsequent rows of data supplied as template literal expressions + using `${value}` syntax. +* `name`: `String` the title of the test suite, use `$variable` to inject test + data into the suite title from the tagged template expressions. +* `fn`: `Function` the suite of tests to be ran, this is the function that will + receive the test data object. + +Example: + +```js +describe.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('$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); + }); +}); +``` + ### `describe.only(name, fn)` Also under the alias: `fdescribe(name, fn)` @@ -278,6 +349,52 @@ describe('my other beverage', () => { }); ``` +### `describe.only.each(table)(name, fn)` + +Also under the aliases: `fdescribe.each(table)(name, fn)` and +`` fdescribe.each`table`(name, fn) `` + +Use `describe.only.each` if you want to only run specific tests suites of data +driven tests. + +`describe.only.each` is available with two APIs: + +#### `describe.only.each(table)(name, fn)` + +```js +describe.only.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( + '.add(%s, %s)', + (a, b, expected) => { + test(`returns ${expected}`, () => { + expect(a + b).toBe(expected); + }); + }, +); + +test('will not be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + +#### `` describe.only.each`table`(name, fn) `` + +```js +describe.only.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('returns $expected when $a is added $b', ({a, b, expected}) => { + test('passes', () => { + expect(a + b).toBe(expected); + }); +}); + +test('will not be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + ### `describe.skip(name, fn)` Also under the alias: `xdescribe(name, fn)` @@ -304,6 +421,52 @@ describe.skip('my other beverage', () => { Using `describe.skip` is often just an easier alternative to temporarily commenting out a chunk of tests. +### `describe.skip.each(table)(name, fn)` + +Also under the aliases: `xdescribe.each(table)(name, fn)` and +`` xdescribe.each`table`(name, fn) `` + +Use `describe.skip.each` if you want to stop running a suite of data driven +tests. + +`describe.skip.each` is available with two APIs: + +#### `describe.skip.each(table)(name, fn)` + +```js +describe.skip.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( + '.add(%s, %s)', + (a, b, expected) => { + test(`returns ${expected}`, () => { + expect(a + b).toBe(expected); // will not be ran + }); + }, +); + +test('will be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + +#### `` describe.skip.each`table`(name, fn) `` + +```js +describe.skip.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('returns $expected when $a is added $b', ({a, b, expected}) => { + test('will not be ran', () => { + expect(a + b).toBe(expected); // will not be ran + }); +}); + +test('will be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + ### `require.requireActual(moduleName)` Returns the actual module instead of a mock, bypassing all checks on whether the @@ -353,6 +516,60 @@ test('has lemon in it', () => { Even though the call to `test` will return right away, the test doesn't complete until the promise resolves as well. +### `test.each(table)(name, fn)` + +Also under the alias: `it.each(table)(name, fn)` and +`` it.each`table`(name, fn) `` + +Use `test.each` if you keep duplicating the same test with different data. +`test.each` allows you to write the test once and pass data in. + +`test.each` is available with two APIs: + +#### 1. `test.each(table)(name, fn)` + +* `table`: `Array` of Arrays with the arguments that are passed into the test + `fn` for each row. +* `name`: `String` the title of the test block, use `%s` to positionally inject + parameter values into the test title. +* `fn`: `Function` the test to be ran, this is the function that will receive + the parameters in each row as function arguments. + +Example: + +```js +test.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( + '.add(%s, %s)', + (a, b, expected) => { + expect(a + b).toBe(expected); + }, +); +``` + +#### 2. `` test.each`table`(name, fn) `` + +* `table`: `Tagged Template Literal` + * First row of variable name column headings separated with `|` + * One or more subsequent rows of data supplied as template literal expressions + using `${value}` syntax. +* `name`: `String` the title of the test, use `$variable` to inject test data + into the test title from the tagged template expressions. +* `fn`: `Function` the test to be ran, this is the function that will receive + the test data object. + +Example: + +```js +test.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('returns $expected when $a is added $b', ({a, b, expected}) => { + expect(a + b).toBe(expected); +}); +``` + ### `test.only(name, fn, timeout)` Also under the aliases: `it.only(name, fn, timeout)` or `fit(name, fn, timeout)` @@ -383,6 +600,49 @@ Usually you wouldn't check code using `test.only` into source control - you would use it just for debugging, and remove it once you have fixed the broken tests. +### `test.only.each(table)(name, fn)` + +Also under the aliases: `it.only.each(table)(name, fn)`, +`fit.each(table)(name, fn)`, `` it.only.each`table`(name, fn) `` and +`` fit.each`table`(name, fn) `` + +Use `test.only.each` if you want to only run specific tests with different test +data. + +`test.only.each` is available with two APIs: + +#### `test.only.each(table)(name, fn)` + +```js +test.only.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( + '.add(%s, %s)', + (a, b, expected) => { + expect(a + b).toBe(expected); + }, +); + +test('will not be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + +#### `` test.only.each`table`(name, fn) `` + +```js +test.only.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('returns $expected when $a is added $b', ({a, b, expected}) => { + expect(a + b).toBe(expected); +}); + +test('will not be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + ### `test.skip(name, fn)` Also under the aliases: `it.skip(name, fn)` or `xit(name, fn)` or @@ -410,3 +670,47 @@ Only the "it is raining" test will run, since the other test is run with You could simply comment the test out, but it's often a bit nicer to use `test.skip` because it will maintain indentation and syntax highlighting. + +### `test.skip.each(table)(name, fn)` + +Also under the aliases: `it.skip.each(table)(name, fn)`, +`xit.each(table)(name, fn)`, `xtest.each(table)(name, fn)`, +`` it.skip.each`table`(name, fn) ``, `` xit.each`table`(name, fn) `` and +`` xtest.each`table`(name, fn) `` + +Use `test.skip.each` if you want to stop running a collection of data driven +tests. + +`test.skip.each` is available with two APIs: + +#### `test.skip.each(table)(name, fn)` + +```js +test.skip.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])( + '.add(%s, %s)', + (a, b, expected) => { + expect(a + b).toBe(expected); // will not be ran + }, +); + +test('will be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + +#### `` test.skip.each`table`(name, fn) `` + +```js +test.skip.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('returns $expected when $a is added $b', ({a, b, expected}) => { + expect(a + b).toBe(expected); // will not be ran +}); + +test('will be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` diff --git a/integration-tests/__tests__/__snapshots__/each.test.js.snap b/integration-tests/__tests__/__snapshots__/each.test.js.snap new file mode 100644 index 000000000000..f76a1b92fe86 --- /dev/null +++ b/integration-tests/__tests__/__snapshots__/each.test.js.snap @@ -0,0 +1,238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shows error message when not enough arguments are supplied to tests 1`] = ` +"FAIL __tests__/each-exception.test.js + ✕ throws exception when not enough arguments are supplied $left == $right + + ● throws exception when not enough arguments are supplied $left == $right + + Not enough arguments supplied for given headings: + left | right + + Received: + Array [ + true, + true, + true, + ] + + Missing 1 arguments + + at packages/jest-jasmine2/build/each.js:84:17 + +" +`; + +exports[`shows only the tests with .only as being ran 1`] = ` +"PASS __tests__/each-only.test.js + ✓ passes one row expected true == true + ✓ passes one row expected true == true + ✓ passes one row expected true == true + ✓ passes one row expected true == true + ○ skipped 4 tests + +" +`; + +exports[`shows only the tests without .skip as being ran 1`] = ` +"PASS __tests__/each-skip.test.js + ✓ passes one row expected true == true + ✓ passes one row expected true == true + ✓ passes one row expected true == true + ✓ passes one row expected true == true + ○ skipped 4 tests + +" +`; + +exports[`shows the correct errors in stderr when failing tests 1`] = ` +"FAIL __tests__/failure.test.js + ✓ array table fails on one row: expected true == true + ✕ array table fails on one row: expected true == false + ✕ array table fails on all rows expected 1 == 2 + ✕ array table fails on all rows expected 3 == 4 + ✕ template table fails on one row expected: true == false + ✓ template table fails on one row expected: true == true + ✕ template table fails on all rows expected: 1 == 2 + ✕ template table fails on all rows expected: 3 == 4 + array table describe fails on all rows expected a == b + ✕ fails + array table describe fails on all rows expected c == d + ✕ fails + template table describe fails on all rows expected a == b + ✕ fails + template table describe fails on all rows expected c == d + ✕ fails + + ● array table fails on one row: expected true == false + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + + 9 | 'array table fails on one row: expected %s == %s', + 10 | (left, right) => { + > 11 | expect(left).toBe(right); + | ^ + 12 | } + 13 | ); + 14 | + + at __tests__/failure.test.js:11:18 + + ● array table fails on all rows expected 1 == 2 + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + + 16 | 'array table fails on all rows expected %s == %s', + 17 | (left, right) => { + > 18 | expect(left).toBe(right); + | ^ + 19 | } + 20 | ); + 21 | + + at __tests__/failure.test.js:18:18 + + ● array table fails on all rows expected 3 == 4 + + expect(received).toBe(expected) // Object.is equality + + Expected: 4 + Received: 3 + + 16 | 'array table fails on all rows expected %s == %s', + 17 | (left, right) => { + > 18 | expect(left).toBe(right); + | ^ + 19 | } + 20 | ); + 21 | + + at __tests__/failure.test.js:18:18 + + ● array table describe fails on all rows expected a == b › fails + + expect(received).toBe(expected) // Object.is equality + + Expected: \\"b\\" + Received: \\"a\\" + + 24 | (left, right) => { + 25 | it('fails', () => { + > 26 | expect(left).toBe(right); + | ^ + 27 | }); + 28 | } + 29 | ); + + at __tests__/failure.test.js:26:20 + + ● array table describe fails on all rows expected c == d › fails + + expect(received).toBe(expected) // Object.is equality + + Expected: \\"d\\" + Received: \\"c\\" + + 24 | (left, right) => { + 25 | it('fails', () => { + > 26 | expect(left).toBe(right); + | ^ + 27 | }); + 28 | } + 29 | ); + + at __tests__/failure.test.js:26:20 + + ● template table fails on one row expected: true == false + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + + 36 | 'template table fails on one row expected: $left == $right', + 37 | ({left, right}) => { + > 38 | expect(left).toBe(right); + | ^ + 39 | } + 40 | ); + 41 | + + at __tests__/failure.test.js:38:18 + + ● template table fails on all rows expected: 1 == 2 + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + + 47 | 'template table fails on all rows expected: $left == $right', + 48 | ({left, right}) => { + > 49 | expect(left).toBe(right); + | ^ + 50 | } + 51 | ); + 52 | + + at __tests__/failure.test.js:49:18 + + ● template table fails on all rows expected: 3 == 4 + + expect(received).toBe(expected) // Object.is equality + + Expected: 4 + Received: 3 + + 47 | 'template table fails on all rows expected: $left == $right', + 48 | ({left, right}) => { + > 49 | expect(left).toBe(right); + | ^ + 50 | } + 51 | ); + 52 | + + at __tests__/failure.test.js:49:18 + + ● template table describe fails on all rows expected a == b › fails + + expect(received).toBe(expected) // Object.is equality + + Expected: \\"b\\" + Received: \\"a\\" + + 59 | ({left, right}) => { + 60 | it('fails ', () => { + > 61 | expect(left).toBe(right); + | ^ + 62 | }); + 63 | } + 64 | ); + + at __tests__/failure.test.js:61:20 + + ● template table describe fails on all rows expected c == d › fails + + expect(received).toBe(expected) // Object.is equality + + Expected: \\"d\\" + Received: \\"c\\" + + 59 | ({left, right}) => { + 60 | it('fails ', () => { + > 61 | expect(left).toBe(right); + | ^ + 62 | }); + 63 | } + 64 | ); + + at __tests__/failure.test.js:61:20 + +" +`; diff --git a/integration-tests/__tests__/each.test.js b/integration-tests/__tests__/each.test.js new file mode 100644 index 000000000000..54e16cfda251 --- /dev/null +++ b/integration-tests/__tests__/each.test.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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. + * + * @flow + */ + +'use strict'; + +const path = require('path'); +const runJest = require('../runJest'); +const {extractSummary} = require('../Utils'); +const dir = path.resolve(__dirname, '../each'); +const SkipOnWindows = require('../../scripts/SkipOnWindows'); + +SkipOnWindows.suite(); + +test('works with passing tests', () => { + const result = runJest(dir, ['success.test.js']); + expect(result.status).toBe(0); +}); + +test('shows error message when not enough arguments are supplied to tests', () => { + const result = runJest(dir, ['each-exception.test.js']); + expect(result.status).toBe(1); + const {rest} = extractSummary(result.stderr); + expect(rest).toMatchSnapshot(); +}); + +test('shows the correct errors in stderr when failing tests', () => { + const result = runJest(dir, ['failure.test.js']); + expect(result.status).toBe(1); + const output = extractSummary(result.stderr) + .rest.split('\n') + .map(line => line.trimRight()) + .join('\n'); + expect(output).toMatchSnapshot(); +}); + +test('shows only the tests with .only as being ran', () => { + const result = runJest(dir, ['each-only.test.js']); + expect(result.status).toBe(0); + const {rest} = extractSummary(result.stderr); + expect(rest).toMatchSnapshot(); +}); + +test('shows only the tests without .skip as being ran', () => { + const result = runJest(dir, ['each-skip.test.js']); + expect(result.status).toBe(0); + const {rest} = extractSummary(result.stderr); + expect(rest).toMatchSnapshot(); +}); diff --git a/integration-tests/each/__tests__/each-exception.test.js b/integration-tests/each/__tests__/each-exception.test.js new file mode 100644 index 000000000000..766fd719f649 --- /dev/null +++ b/integration-tests/each/__tests__/each-exception.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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. + */ + +it.each` + left | right + ${true} | ${true} + ${true} | +`( + 'throws exception when not enough arguments are supplied $left == $right', + ({left, right}) => { + expect(left).toBe(right); + } +); diff --git a/integration-tests/each/__tests__/each-only.test.js b/integration-tests/each/__tests__/each-only.test.js new file mode 100644 index 000000000000..a434a48f2b78 --- /dev/null +++ b/integration-tests/each/__tests__/each-only.test.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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. + */ + +it.only.each([[true, true], [true, true]])( + 'passes one row expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +it.each([[true, false], [true, true]])( + 'Should not be ran: fails all rows expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +it.only.each` + left | right + ${true} | ${true} + ${true} | ${true} +`('passes one row expected $left == $right', ({left, right}) => { + expect(left).toBe(right); +}); + +it.each` + left | right + ${true} | ${false} + ${true} | ${false} +`( + 'Should not be ran: fails all rows expected $left == $right', + ({left, right}) => { + expect(left).toBe(right); + } +); diff --git a/integration-tests/each/__tests__/each-skip.test.js b/integration-tests/each/__tests__/each-skip.test.js new file mode 100644 index 000000000000..08945a527331 --- /dev/null +++ b/integration-tests/each/__tests__/each-skip.test.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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. + */ + +it.each([[true, true], [true, true]])( + 'passes one row expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +it.skip.each([[true, false], [true, true]])( + 'Should not be ran: fails all rows expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +it.each` + left | right + ${true} | ${true} + ${true} | ${true} +`('passes one row expected $left == $right', ({left, right}) => { + expect(left).toBe(right); +}); + +it.skip.each` + left | right + ${true} | ${false} + ${true} | ${false} +`( + 'Should not be ran: fails all rows expected $left == $right', + ({left, right}) => { + expect(left).toBe(right); + } +); diff --git a/integration-tests/each/__tests__/failure.test.js b/integration-tests/each/__tests__/failure.test.js new file mode 100644 index 000000000000..44c132073063 --- /dev/null +++ b/integration-tests/each/__tests__/failure.test.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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. + */ + +it.each([[true, true], [true, false]])( + 'array table fails on one row: expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +it.each([[1, 2], [3, 4]])( + 'array table fails on all rows expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +describe.each([['a', 'b'], ['c', 'd']])( + 'array table describe fails on all rows expected %s == %s', + (left, right) => { + it('fails', () => { + expect(left).toBe(right); + }); + } +); + +it.each` + left | right + ${true} | ${false} + ${true} | ${true} + `( + 'template table fails on one row expected: $left == $right', + ({left, right}) => { + expect(left).toBe(right); + } +); + +it.each` + left | right + ${1} | ${2} + ${3} | ${4} + `( + 'template table fails on all rows expected: $left == $right', + ({left, right}) => { + expect(left).toBe(right); + } +); + +describe.each` + left | right + ${'a'} | ${'b'} + ${'c'} | ${'d'} + `( + 'template table describe fails on all rows expected $left == $right', + ({left, right}) => { + it('fails ', () => { + expect(left).toBe(right); + }); + } +); diff --git a/integration-tests/each/__tests__/success.test.js b/integration-tests/each/__tests__/success.test.js new file mode 100644 index 000000000000..9964f3c74c08 --- /dev/null +++ b/integration-tests/each/__tests__/success.test.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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. + */ + +it.each([[true, true], [true, true]])( + 'passes one row expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +it.each([[true, true], [true, true]])( + 'passes all rows expected %s == %s', + (left, right) => { + expect(left).toBe(right); + } +); + +describe.each([[true, true], [true, true]])( + 'passes all rows expected %s == %s', + (left, right) => { + it('passes', () => { + expect(left).toBe(right); + }); + } +); + +it.each` + left | right + ${true} | ${true} + ${true} | ${true} +`('passes one row expected $left == $right', ({left, right}) => { + expect(left).toBe(right); +}); + +it.each` + left | right + ${true} | ${true} + ${true} | ${true} +`('passes all rows expected $left == $right', ({left, right}) => { + expect(left).toBe(right); +}); + +describe.each` + left | right + ${true} | ${true} + ${true} | ${true} +`('passes all rows expected $left == $right', ({left, right}) => { + it('passes ', () => { + expect(left).toBe(right); + }); +}); diff --git a/integration-tests/each/package.json b/integration-tests/each/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/integration-tests/each/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/jest-jasmine2/src/__tests__/__snapshots__/each.test.js.snap b/packages/jest-jasmine2/src/__tests__/__snapshots__/each.test.js.snap new file mode 100644 index 000000000000..f3b4ed19fd97 --- /dev/null +++ b/packages/jest-jasmine2/src/__tests__/__snapshots__/each.test.js.snap @@ -0,0 +1,175 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`installEach .describe Table Tagged Template Literal throws error when there are fewer arguments than headings over multiple rows 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, + 1, + 1, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .describe Table Tagged Template Literal throws error when there are fewer arguments than headings when given one row 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .fdescribe Table Tagged Template Literal throws error when there are fewer arguments than headings over multiple rows 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, + 1, + 1, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .fdescribe Table Tagged Template Literal throws error when there are fewer arguments than headings when given one row 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .fit Table Tagged Template Literal throws error when there are fewer arguments than headings over multiple rows 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, + 1, + 1, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .fit Table Tagged Template Literal throws error when there are fewer arguments than headings when given one row 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .it Table Tagged Template Literal throws error when there are fewer arguments than headings over multiple rows 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, + 1, + 1, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .it Table Tagged Template Literal throws error when there are fewer arguments than headings when given one row 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .xdescribe Table Tagged Template Literal throws error when there are fewer arguments than headings over multiple rows 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, + 1, + 1, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .xdescribe Table Tagged Template Literal throws error when there are fewer arguments than headings when given one row 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .xit Table Tagged Template Literal throws error when there are fewer arguments than headings over multiple rows 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, + 1, + 1, + 1, +] + +Missing 2 arguments" +`; + +exports[`installEach .xit Table Tagged Template Literal throws error when there are fewer arguments than headings when given one row 1`] = ` +"Not enough arguments supplied for given headings: +a | b | expected + +Received: +Array [ + 0, + 1, +] + +Missing 2 arguments" +`; diff --git a/packages/jest-jasmine2/src/__tests__/each.test.js b/packages/jest-jasmine2/src/__tests__/each.test.js new file mode 100644 index 000000000000..98a5c79f6c66 --- /dev/null +++ b/packages/jest-jasmine2/src/__tests__/each.test.js @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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 installEach from '../each'; + +const noop = () => {}; +const expectFunction = expect.any(Function); + +describe('installEach', () => { + [ + ['it'], + ['fit'], + ['xit'], + ['describe'], + ['fdescribe'], + ['xdescribe'], + ].forEach(keyPath => { + describe(`.${keyPath.join('.')}`, () => { + const getEnvironmentMock = () => { + return { + global: { + describe: jest.fn(), + fdescribe: jest.fn(), + fit: jest.fn(), + it: jest.fn(), + xdescribe: jest.fn(), + xit: jest.fn(), + }, + }; + }; + + describe('Table Array', () => { + test('calls global function with given title', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + + globalMock.each([[]])('expected string', noop); + + expect(globalMock).toHaveBeenCalledTimes(1); + expect(globalMock).toHaveBeenCalledWith( + 'expected string', + expectFunction, + ); + }); + + test('calls global function with given title when multiple tests cases exist', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + + globalMock.each([[], []])('expected string', noop); + + expect(globalMock).toHaveBeenCalledTimes(2); + expect(globalMock).toHaveBeenCalledWith( + 'expected string', + expectFunction, + ); + expect(globalMock).toHaveBeenCalledWith( + 'expected string', + expectFunction, + ); + }); + + test('calls global function with title containing param values when using sprintf format', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + + globalMock.each([['hello', 1], ['world', 2]])( + 'expected string: %s %s', + noop, + ); + + expect(globalMock).toHaveBeenCalledTimes(2); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: hello 1', + expectFunction, + ); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: world 2', + expectFunction, + ); + }); + + test('calls global function with cb function containing all parameters of each test case', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + const testCallBack = jest.fn(); + globalMock.each([['hello', 'world'], ['joe', 'bloggs']])( + 'expected string: %s %s', + testCallBack, + ); + + 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 function with async done when cb function has more args than params of given test row', () => { + expect.hasAssertions(); + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + globalMock.each([['hello']])('a title', (hello, done) => { + expect(hello).toBe('hello'); + expect(done).toBe('DONE'); + }); + + globalMock.mock.calls[0][1]('DONE'); + }); + }); + + describe('Table Tagged Template Literal', () => { + test('throws error when there are fewer arguments than headings when given one row', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + const testCallBack = jest.fn(); + + globalMock.each` + a | b | expected + ${0} | ${1} | + `('this will blow up :(', testCallBack); + + 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 environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + const testCallBack = jest.fn(); + + globalMock.each` + a | b | expected + ${0} | ${1} | ${1} + ${1} | ${1} | + `('this will blow up :(', testCallBack); + + expect(() => + globalMock.mock.calls[0][1](), + ).toThrowErrorMatchingSnapshot(); + expect(testCallBack).not.toHaveBeenCalled(); + }); + + test('calls global function with given title', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + + globalMock.each` + a | b | expected + ${0} | ${1} | ${1} + `('expected string', noop); + + expect(globalMock).toHaveBeenCalledTimes(1); + expect(globalMock).toHaveBeenCalledWith( + 'expected string', + expectFunction, + ); + }); + + test('calls global function with given title when multiple tests cases exist', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + + globalMock.each` + a | b | expected + ${0} | ${1} | ${1} + ${1} | ${1} | ${2} + `('expected string', noop); + + expect(globalMock).toHaveBeenCalledTimes(2); + expect(globalMock).toHaveBeenCalledWith( + 'expected string', + expectFunction, + ); + expect(globalMock).toHaveBeenCalledWith( + 'expected string', + expectFunction, + ); + }); + + test('calls global function with title containing param values when using $variable format', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + + globalMock.each` + a | b | expected + ${0} | ${1} | ${1} + ${1} | ${1} | ${2} + `('expected string: a=$a, b=$b, expected=$expected', noop); + + 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 function with cb function containing all parameters of each test case', () => { + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + const testCallBack = jest.fn(); + globalMock.each` + a | b | expected + ${0} | ${1} | ${1} + ${1} | ${1} | ${2} + `('expected string: %s %s', testCallBack); + + 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 function with async done when cb function has more than one argument', () => { + expect.hasAssertions(); + const environmentMock = getEnvironmentMock(); + installEach(environmentMock); + + const globalMock = environmentMock.global[keyPath]; + globalMock.each` + a | b | expected + ${0} | ${1} | ${1} + `('a title', ({a, b, expected}, done) => { + expect(a).toBe(0); + expect(b).toBe(1); + expect(expected).toBe(1); + expect(done).toBe('DONE'); + }); + + globalMock.mock.calls[0][1]('DONE'); + }); + }); + }); + }); +}); diff --git a/packages/jest-jasmine2/src/each.js b/packages/jest-jasmine2/src/each.js new file mode 100644 index 000000000000..92fcacdb2c6e --- /dev/null +++ b/packages/jest-jasmine2/src/each.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. 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. + * + * @flow + */ + +import type {Environment} from 'types/Environment'; + +import util from 'util'; +import chalk from 'chalk'; +import pretty from 'pretty-format'; + +type Table = Array>; + +const EXPECTED_COLOR = chalk.green; +const RECEIVED_COLOR = chalk.red; + +export default (environment: Environment): void => { + environment.global.it.each = bindEach(environment.global.it); + environment.global.fit.each = bindEach(environment.global.fit); + environment.global.xit.each = bindEach(environment.global.xit); + environment.global.describe.each = bindEach(environment.global.describe); + environment.global.xdescribe.each = bindEach(environment.global.xdescribe); + environment.global.fdescribe.each = bindEach(environment.global.fdescribe); +}; + +const bindEach = (cb: Function) => (...args: any) => ( + title: string, + test: Function, +): void => { + if (args.length === 1) { + const table: Table = args[0]; + return table.forEach(row => + cb(util.format(title, ...row), applyRestParams(row, test)), + ); + } + + const templateStrings = args[0]; + const data = args.slice(1); + + const keys = getHeadingKeys(templateStrings[0]); + const table = buildTable(data, keys.length, keys); + + if (data.length % keys.length !== 0) { + return cb(title, () => { + throw new Error( + 'Not enough arguments supplied for given headings:\n' + + EXPECTED_COLOR(keys.join(' | ')) + + '\n\n' + + 'Received:\n' + + RECEIVED_COLOR(pretty(data)) + + '\n\n' + + `Missing ${RECEIVED_COLOR(`${data.length % keys.length}`)} arguments`, + ); + }); + } + + return table.forEach(row => + cb(interpolate(title, row), applyObjectParams(row, test)), + ); +}; + +const applyRestParams = (params: Array, test: Function) => { + if (params.length < test.length) return done => test(...params, done); + + return () => test(...params); +}; + +const getHeadingKeys = (headings: string): Array => + headings.replace(/\s/g, '').split('|'); + +const buildTable = ( + data: Array, + rowSize: number, + keys: Array, +): Array => + Array.from({length: data.length / rowSize}) + .map((_, index) => data.slice(index * rowSize, index * rowSize + rowSize)) + .map(row => + row.reduce( + (acc, value, index) => Object.assign({}, acc, {[keys[index]]: value}), + {}, + ), + ); + +const interpolate = (title: string, data: any) => + Object.keys(data).reduce( + (acc, key) => acc.replace('$' + key, data[key]), + title, + ); + +const applyObjectParams = (obj: any, test: Function) => { + if (test.length > 1) return done => test(obj, done); + + return () => test(obj); +}; diff --git a/packages/jest-jasmine2/src/index.js b/packages/jest-jasmine2/src/index.js index 3e822d1420c8..785fa5cf0548 100644 --- a/packages/jest-jasmine2/src/index.js +++ b/packages/jest-jasmine2/src/index.js @@ -16,6 +16,7 @@ import type Runtime from 'jest-runtime'; import path from 'path'; import fs from 'graceful-fs'; +import installEach from './each'; import {getCallsite} from 'jest-util'; import JasmineReporter from './reporter'; import {install as jasmineAsyncInstall} from './jasmine_async'; @@ -62,6 +63,8 @@ async function jasmine2( jasmineAsyncInstall(environment.global); + installEach(environment); + environment.global.test = environment.global.it; environment.global.it.only = environment.global.fit; environment.global.it.skip = environment.global.xit;