From c95abca8082ad1e472828a6b2e5097745371707f Mon Sep 17 00:00:00 2001 From: Nimalan Date: Thu, 6 Aug 2020 17:26:34 +0530 Subject: [PATCH] feat: support `concurrent` in Jest Each (#9326) --- CHANGELOG.md | 1 + docs/GlobalAPI.md | 165 ++++++++++++++++++ e2e/__tests__/circusConcurrentEach.test.ts | 29 +++ e2e/__tests__/jasmineAsync.test.ts | 18 ++ .../__tests__/concurrent-each.test.js | 20 +++ .../__tests__/concurrent-only-each.test.js | 22 +++ e2e/circus-concurrent/package.json | 5 + .../__tests__/concurrent-each.test.js | 20 +++ .../__tests__/concurrent-only-each.test.js | 22 +++ .../jestAdapterInit.ts | 12 +- packages/jest-each/README.md | 57 ++++++ .../__snapshots__/array.test.ts.snap | 36 ++++ .../__snapshots__/template.test.ts.snap | 117 +++++++++++++ .../jest-each/src/__tests__/array.test.ts | 9 + .../jest-each/src/__tests__/index.test.ts | 15 ++ .../jest-each/src/__tests__/template.test.ts | 9 + packages/jest-each/src/bind.ts | 24 +-- packages/jest-each/src/index.ts | 27 ++- .../src/__tests__/concurrent.test.ts | 17 ++ packages/jest-jasmine2/src/each.ts | 12 ++ .../jest-jasmine2/src/jasmineAsyncInstall.ts | 9 +- packages/jest-types/src/Global.ts | 31 +++- 22 files changed, 649 insertions(+), 28 deletions(-) create mode 100644 e2e/__tests__/circusConcurrentEach.test.ts create mode 100644 e2e/circus-concurrent/__tests__/concurrent-each.test.js create mode 100644 e2e/circus-concurrent/__tests__/concurrent-only-each.test.js create mode 100644 e2e/circus-concurrent/package.json create mode 100644 e2e/jasmine-async/__tests__/concurrent-each.test.js create mode 100644 e2e/jasmine-async/__tests__/concurrent-only-each.test.js create mode 100644 packages/jest-jasmine2/src/__tests__/concurrent.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b5fe3ee8d19..457da1fd06b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[jest-circus, jest-jasmine2]` Include `failureDetails` property in test results ([#9496](https://github.com/facebook/jest/pull/9496)) +- `[jest-each, jest-jasmine, jest-circus]` Add support for .concurrent.each ([#9326](https://github.com/facebook/jest/pull/9326)) ### Fixes diff --git a/docs/GlobalAPI.md b/docs/GlobalAPI.md index c321d53f956b..6800dc752c6c 100644 --- a/docs/GlobalAPI.md +++ b/docs/GlobalAPI.md @@ -465,6 +465,171 @@ 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.concurrent(name, fn, timeout)` + +Also under the alias: `it.concurrent(name, fn, timeout)` + +Use `test.concurrent` if you want the test to run concurrently. + +> Note: `test.concurrent` is considered experimental - see [here])https://github.com/facebook/jest/labels/Area%3A%20Concurrent) for details on missing features and other issues + +The first argument is the test name; the second argument is an asynchronous function that contains the expectations to test. The third argument (optional) is `timeout` (in milliseconds) for specifying how long to wait before aborting. _Note: The default timeout is 5 seconds._ + +``` +test.concurrent('addition of 2 numbers', async () => { + expect(5 + 3).toBe(8); +}); + +test.concurrent('subtraction 2 numbers', async () => { + expect(5 - 3).toBe(2); +}); +``` + +> Note: Use `maxConcurrency` in configuration to prevents Jest from executing more than the specified amount of tests at the same time + +### `test.concurrent.each(table)(name, fn, timeout)` + +Also under the alias: `it.concurrent.each(table)(name, fn, timeout)` + +Use `test.concurrent.each` if you keep duplicating the same test with different data. `test.each` allows you to write the test once and pass data in, the tests are all run asynchronously. + +`test.concurrent.each` is available with two APIs: + +#### 1. `test.concurrent.each(table)(name, fn, timeout)` + +- `table`: `Array` of Arrays with the arguments that are passed into the test `fn` for each row. + - _Note_ If you pass in a 1D array of primitives, internally it will be mapped to a table i.e. `[1, 2, 3] -> [[1], [2], [3]]` +- `name`: `String` the title of the test block. + - Generate unique test titles by positionally injecting parameters with [`printf` formatting](https://nodejs.org/api/util.html#util_util_format_format_args): + - `%p` - [pretty-format](https://www.npmjs.com/package/pretty-format). + - `%s`- String. + - `%d`- Number. + - `%i` - Integer. + - `%f` - Floating point value. + - `%j` - JSON. + - `%o` - Object. + - `%#` - Index of the test case. + - `%%` - single percent sign ('%'). This does not consume an argument. +- `fn`: `Function` the test to be ran, this is the function that will receive the parameters in each row as function arguments, **this will have to be an asynchronous function**. +- Optionally, you can provide a `timeout` (in milliseconds) for specifying how long to wait for each row before aborting. _Note: The default timeout is 5 seconds._ + +Example: + +```js +test.concurrent.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('.add(%i, %i)', (a, b, expected) => { + expect(a + b).toBe(expected); +}); +``` + +#### 2. `` test.concurrent.each`table`(name, fn, timeout) `` + +- `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. + - To inject nested object values use you can supply a keyPath i.e. `$variable.path.to.value` +- `fn`: `Function` the test to be ran, this is the function that will receive the test data object, **this will have to be an asynchronous function**. +- Optionally, you can provide a `timeout` (in milliseconds) for specifying how long to wait for each row before aborting. _Note: The default timeout is 5 seconds._ + +Example: + +```js +test.concurrent.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.concurrent.only.each(table)(name, fn)` + +Also under the alias: `it.concurrent.only.each(table)(name, fn)` + +Use `test.concurrent.only.each` if you want to only run specific tests with different test data concurrently. + +`test.concurrent.only.each` is available with two APIs: + +#### `test.concurrent.only.each(table)(name, fn)` + +```js +test.concurrent.only.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('.add(%i, %i)', async (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.concurrent.only.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('returns $expected when $a is added $b', async ({a, b, expected}) => { + expect(a + b).toBe(expected); +}); + +test('will not be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + +### `test.concurrent.skip.each(table)(name, fn)` + +Also under the alias: `it.concurrent.skip.each(table)(name, fn)` + +Use `test.concurrent.skip.each` if you want to stop running a collection of asynchronous data driven tests. + +`test.concurrent.skip.each` is available with two APIs: + +#### `test.concurrent.skip.each(table)(name, fn)` + +```js +test.concurrent.skip.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('.add(%i, %i)', async (a, b, expected) => { + expect(a + b).toBe(expected); // will not be ran +}); + +test('will be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + +#### `` test.concurrent.skip.each`table`(name, fn) `` + +```js +test.concurrent.skip.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} + ${2} | ${1} | ${3} +`('returns $expected when $a is added $b', async ({a, b, expected}) => { + expect(a + b).toBe(expected); // will not be ran +}); + +test('will be ran', () => { + expect(1 / 0).toBe(Infinity); +}); +``` + ### `test.each(table)(name, fn, timeout)` Also under the alias: `it.each(table)(name, fn)` and `` it.each`table`(name, fn) `` diff --git a/e2e/__tests__/circusConcurrentEach.test.ts b/e2e/__tests__/circusConcurrentEach.test.ts new file mode 100644 index 000000000000..27ef656e99b9 --- /dev/null +++ b/e2e/__tests__/circusConcurrentEach.test.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import {json as runWithJson} from '../runJest'; + +it('works with concurrent.each', () => { + const {json} = runWithJson('circus-concurrent', [ + 'concurrent-each.test.js', + '--testRunner=jest-circus/runner', + ]); + expect(json.numTotalTests).toBe(4); + expect(json.numPassedTests).toBe(2); + expect(json.numFailedTests).toBe(0); + expect(json.numPendingTests).toBe(2); +}); + +it('works with concurrent.only.each', () => { + const {json} = runWithJson('circus-concurrent', [ + 'concurrent-only-each.test.js', + '--testRunner=jest-circus/runner', + ]); + expect(json.numTotalTests).toBe(4); + expect(json.numPassedTests).toBe(2); + expect(json.numFailedTests).toBe(0); + expect(json.numPendingTests).toBe(2); +}); diff --git a/e2e/__tests__/jasmineAsync.test.ts b/e2e/__tests__/jasmineAsync.test.ts index 6eaaa864e956..3006a04b3810 100644 --- a/e2e/__tests__/jasmineAsync.test.ts +++ b/e2e/__tests__/jasmineAsync.test.ts @@ -128,6 +128,24 @@ describe('async jasmine', () => { expect(json.testResults[0].message).toMatch(/concurrent test fails/); }); + it('works with concurrent.each', () => { + const {json} = runWithJson('jasmine-async', ['concurrent-each.test.js']); + expect(json.numTotalTests).toBe(4); + expect(json.numPassedTests).toBe(2); + expect(json.numFailedTests).toBe(0); + expect(json.numPendingTests).toBe(2); + }); + + it('works with concurrent.only.each', () => { + const {json} = runWithJson('jasmine-async', [ + 'concurrent-only-each.test.js', + ]); + expect(json.numTotalTests).toBe(4); + expect(json.numPassedTests).toBe(2); + expect(json.numFailedTests).toBe(0); + expect(json.numPendingTests).toBe(2); + }); + it("doesn't execute more than 5 tests simultaneously", () => { const {json} = runWithJson('jasmine-async', ['concurrent-many.test.js']); expect(json.numTotalTests).toBe(10); diff --git a/e2e/circus-concurrent/__tests__/concurrent-each.test.js b/e2e/circus-concurrent/__tests__/concurrent-each.test.js new file mode 100644 index 000000000000..1e358d9db85c --- /dev/null +++ b/e2e/circus-concurrent/__tests__/concurrent-each.test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +it.concurrent.each([ + [1, 2], + [2, 3], +])('adds one to number', async (a, b) => { + expect(a + 1).toBe(b); +}); + +it.concurrent.skip.each([ + [1, 2], + [2, 3], +])('should skip this test', Promise.resolve()); diff --git a/e2e/circus-concurrent/__tests__/concurrent-only-each.test.js b/e2e/circus-concurrent/__tests__/concurrent-only-each.test.js new file mode 100644 index 000000000000..c818e1de3d53 --- /dev/null +++ b/e2e/circus-concurrent/__tests__/concurrent-only-each.test.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +it.concurrent.only.each([ + [1, 2], + [2, 3], +])('adds one to number', async (a, b) => { + expect(a + 1).toBe(b); +}); + +it.concurrent.each([ + [1, 2], + [2, 3], +])('adds one to number', async (a, b) => { + expect(a + 1).toBe(b); +}); diff --git a/e2e/circus-concurrent/package.json b/e2e/circus-concurrent/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/circus-concurrent/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/e2e/jasmine-async/__tests__/concurrent-each.test.js b/e2e/jasmine-async/__tests__/concurrent-each.test.js new file mode 100644 index 000000000000..1e358d9db85c --- /dev/null +++ b/e2e/jasmine-async/__tests__/concurrent-each.test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +it.concurrent.each([ + [1, 2], + [2, 3], +])('adds one to number', async (a, b) => { + expect(a + 1).toBe(b); +}); + +it.concurrent.skip.each([ + [1, 2], + [2, 3], +])('should skip this test', Promise.resolve()); diff --git a/e2e/jasmine-async/__tests__/concurrent-only-each.test.js b/e2e/jasmine-async/__tests__/concurrent-only-each.test.js new file mode 100644 index 000000000000..c818e1de3d53 --- /dev/null +++ b/e2e/jasmine-async/__tests__/concurrent-only-each.test.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +it.concurrent.only.each([ + [1, 2], + [2, 3], +])('adds one to number', async (a, b) => { + expect(a + 1).toBe(b); +}); + +it.concurrent.each([ + [1, 2], + [2, 3], +])('adds one to number', async (a, b) => { + expect(a + 1).toBe(b); +}); diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index fba12da73473..c55a067919fb 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -22,6 +22,7 @@ import { addSerializer, buildSnapshotResolver, } from 'jest-snapshot'; +import {bind} from 'jest-each'; import throat from 'throat'; import { ROOT_DESCRIBE_BLOCK_NAME, @@ -77,7 +78,7 @@ export const initialize = async ({ nodeGlobal.test.concurrent = (test => { const concurrent = ( testName: string, - testFn: () => Promise, + testFn: () => Promise, timeout?: number, ) => { // For concurrent tests we first run the function that returns promise, and then register a @@ -90,9 +91,9 @@ export const initialize = async ({ nodeGlobal.test(testName, () => promise, timeout); }; - concurrent.only = ( + const only = ( testName: string, - testFn: () => Promise, + testFn: () => Promise, timeout?: number, ) => { const promise = mutex(() => testFn()); @@ -100,8 +101,13 @@ export const initialize = async ({ test.only(testName, () => promise, timeout); }; + concurrent.only = only; concurrent.skip = test.skip; + concurrent.each = bind(test, false); + concurrent.skip.each = bind(test.skip, false); + only.each = bind(test.only, false); + return concurrent; })(nodeGlobal.test); diff --git a/packages/jest-each/README.md b/packages/jest-each/README.md index e83a0be31ab2..16c845c7e41a 100644 --- a/packages/jest-each/README.md +++ b/packages/jest-each/README.md @@ -19,6 +19,12 @@ jest-each allows you to provide multiple arguments to your `test`/`describe` whi - 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` +- `.test.concurrent` + - Also under the alias: `.it.concurrent` +- `.test.concurrent.only` + - Also under the alias: `.it.concurrent.only` +- `.test.concurrent.skip` + - Also under the alias: `.it.concurrent.skip` - `.describe` to runs test suites with parameterised data - `.describe.only` to only run the parameterised suite of tests - Also under the aliases: `.fdescribe` @@ -180,6 +186,57 @@ each([ }); ``` +#### `.test.concurrent(name, fn)` + +Aliases: `.it.concurrent(name, fn)` + +```js +each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +]).test.concurrent( + 'returns the result of adding %d to %d', + (a, b, expected) => { + expect(a + b).toBe(expected); + }, +); +``` + +#### `.test.concurrent.only(name, fn)` + +Aliases: `.it.concurrent.only(name, fn)` + +```js +each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +]).test.concurrent.only( + 'returns the result of adding %d to %d', + (a, b, expected) => { + expect(a + b).toBe(expected); + }, +); +``` + +#### `.test.concurrent.skip(name, fn)` + +Aliases: `.it.concurrent.skip(name, fn)` + +```js +each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +]).test.concurrent.skip( + 'returns the result of adding %d to %d', + (a, b, expected) => { + expect(a + b).toBe(expected); + }, +); +``` + #### Asynchronous `.test(name, fn(done))` Alias: `.it(name, fn(done))` diff --git a/packages/jest-each/src/__tests__/__snapshots__/array.test.ts.snap b/packages/jest-each/src/__tests__/__snapshots__/array.test.ts.snap index d779a5e5df50..389742a74938 100644 --- a/packages/jest-each/src/__tests__/__snapshots__/array.test.ts.snap +++ b/packages/jest-each/src/__tests__/__snapshots__/array.test.ts.snap @@ -84,6 +84,42 @@ Instead was called with: undefined " `; +exports[`jest-each .test.concurrent throws an error when called with an empty array 1`] = ` +"Error: \`.each\` called with an empty Array of table data. +" +`; + +exports[`jest-each .test.concurrent throws an error when not called with an array 1`] = ` +"\`.each\` must be called with an Array or Tagged Template Literal. + +Instead was called with: undefined +" +`; + +exports[`jest-each .test.concurrent.only throws an error when called with an empty array 1`] = ` +"Error: \`.each\` called with an empty Array of table data. +" +`; + +exports[`jest-each .test.concurrent.only throws an error when not called with an array 1`] = ` +"\`.each\` must be called with an Array or Tagged Template Literal. + +Instead was called with: undefined +" +`; + +exports[`jest-each .test.concurrent.skip throws an error when called with an empty array 1`] = ` +"Error: \`.each\` called with an empty Array of table data. +" +`; + +exports[`jest-each .test.concurrent.skip throws an error when not called with an array 1`] = ` +"\`.each\` must be called with an Array or Tagged Template Literal. + +Instead was called with: undefined +" +`; + exports[`jest-each .test.only throws an error when called with an empty array 1`] = ` "Error: \`.each\` called with an empty Array of table data. " diff --git a/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap b/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap index f5097aa6218d..a7f2fabc0be5 100644 --- a/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap +++ b/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap @@ -273,6 +273,123 @@ exports[`jest-each .test throws error when there are no arguments for given head " `; +exports[`jest-each .test.concurrent throws an error when called with an empty string 1`] = ` +"Error: \`.each\` called with an empty Tagged Template Literal of table data. +" +`; + +exports[`jest-each .test.concurrent 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[`jest-each .test.concurrent 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[`jest-each .test.concurrent throws error when there are no arguments for given headings 1`] = ` +"Error: \`.each\` called with a Tagged Template Literal with no data, remember to interpolate with \${expression} syntax. +" +`; + +exports[`jest-each .test.concurrent.only throws an error when called with an empty string 1`] = ` +"Error: \`.each\` called with an empty Tagged Template Literal of table data. +" +`; + +exports[`jest-each .test.concurrent.only 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[`jest-each .test.concurrent.only 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[`jest-each .test.concurrent.only throws error when there are no arguments for given headings 1`] = ` +"Error: \`.each\` called with a Tagged Template Literal with no data, remember to interpolate with \${expression} syntax. +" +`; + +exports[`jest-each .test.concurrent.skip throws an error when called with an empty string 1`] = ` +"Error: \`.each\` called with an empty Tagged Template Literal of table data. +" +`; + +exports[`jest-each .test.concurrent.skip 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[`jest-each .test.concurrent.skip 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[`jest-each .test.concurrent.skip throws error when there are no arguments for given headings 1`] = ` +"Error: \`.each\` called with a Tagged Template Literal with no data, remember to interpolate with \${expression} syntax. +" +`; + exports[`jest-each .test.only throws an error when called with an empty string 1`] = ` "Error: \`.each\` called with an empty Tagged Template Literal of table data. " diff --git a/packages/jest-each/src/__tests__/array.test.ts b/packages/jest-each/src/__tests__/array.test.ts index 666e7aece2ec..19136ae65f81 100644 --- a/packages/jest-each/src/__tests__/array.test.ts +++ b/packages/jest-each/src/__tests__/array.test.ts @@ -28,6 +28,9 @@ const getGlobalTestMocks = () => { }; globals.test.only = jest.fn(); globals.test.skip = jest.fn(); + globals.test.concurrent = jest.fn(); + globals.test.concurrent.only = jest.fn(); + globals.test.concurrent.skip = jest.fn(); globals.it.only = jest.fn(); globals.it.skip = jest.fn(); globals.describe.only = jest.fn(); @@ -38,6 +41,9 @@ const getGlobalTestMocks = () => { describe('jest-each', () => { [ ['test'], + ['test', 'concurrent'], + ['test', 'concurrent', 'only'], + ['test', 'concurrent', 'skip'], ['test', 'only'], ['it'], ['fit'], @@ -289,6 +295,8 @@ describe('jest-each', () => { test.each([ [['test']], [['test', 'only']], + [['test', 'concurrent']], + [['test', 'concurrent', 'only']], [['it']], [['fit']], [['it', 'only']], @@ -327,6 +335,7 @@ describe('jest-each', () => { [ ['xtest'], ['test', 'skip'], + ['test', 'concurrent', 'skip'], ['xit'], ['it', 'skip'], ['xdescribe'], diff --git a/packages/jest-each/src/__tests__/index.test.ts b/packages/jest-each/src/__tests__/index.test.ts index 40ba0c391700..b2d5c7bec183 100644 --- a/packages/jest-each/src/__tests__/index.test.ts +++ b/packages/jest-each/src/__tests__/index.test.ts @@ -20,6 +20,21 @@ describe('array', () => { }); }); +describe('concurrent', () => { + describe('.add', () => { + each([ + [0, 0, 0], + [0, 1, 1], + [1, 1, 2], + ]).test.concurrent( + 'returns the result of adding %s to %s', + async (a, b, expected) => { + expect(a + b).toBe(expected); + }, + ); + }); +}); + describe('template', () => { describe('.add', () => { each` diff --git a/packages/jest-each/src/__tests__/template.test.ts b/packages/jest-each/src/__tests__/template.test.ts index 7952d90fcc5f..fe5d67217d0b 100644 --- a/packages/jest-each/src/__tests__/template.test.ts +++ b/packages/jest-each/src/__tests__/template.test.ts @@ -27,6 +27,9 @@ const getGlobalTestMocks = () => { }; globals.test.only = jest.fn(); globals.test.skip = jest.fn(); + globals.test.concurrent = jest.fn(); + globals.test.concurrent.only = jest.fn(); + globals.test.concurrent.skip = jest.fn(); globals.it.only = jest.fn(); globals.it.skip = jest.fn(); globals.describe.only = jest.fn(); @@ -37,6 +40,9 @@ const getGlobalTestMocks = () => { describe('jest-each', () => { [ ['test'], + ['test', 'concurrent'], + ['test', 'concurrent', 'only'], + ['test', 'concurrent', 'skip'], ['test', 'only'], ['it'], ['fit'], @@ -315,6 +321,7 @@ describe('jest-each', () => { test.each([ [['test']], [['test', 'only']], + [['test', 'concurrent', 'only']], [['it']], [['fit']], [['it', 'only']], @@ -361,6 +368,8 @@ describe('jest-each', () => { [ ['xtest'], ['test', 'skip'], + ['test', 'concurrent'], + ['test', 'concurrent', 'skip'], ['xit'], ['it', 'skip'], ['xdescribe'], diff --git a/packages/jest-each/src/bind.ts b/packages/jest-each/src/bind.ts index 773f722a4c6d..e2d0649c3952 100644 --- a/packages/jest-each/src/bind.ts +++ b/packages/jest-each/src/bind.ts @@ -18,16 +18,20 @@ export type EachTests = Array<{ arguments: Array; }>; -type TestFn = (done?: Global.DoneFn) => Promise | void | undefined; -type GlobalCallback = (testName: string, fn: TestFn, timeout?: number) => void; +// type TestFn = (done?: Global.DoneFn) => Promise | void | undefined; +type GlobalCallback = ( + testName: string, + fn: Global.ConcurrentTestFn, + timeout?: number, +) => void; -export default (cb: GlobalCallback, supportsDone: boolean = true) => ( - table: Global.EachTable, - ...taggedTemplateData: Global.TemplateData -) => +export default ( + cb: GlobalCallback, + supportsDone: boolean = true, +) => (table: Global.EachTable, ...taggedTemplateData: Global.TemplateData) => function eachBind( title: string, - test: Global.EachTestFn, + test: Global.EachTestFn, timeout?: number, ): void { try { @@ -70,11 +74,11 @@ const buildTemplateTests = ( const getHeadingKeys = (headings: string): Array => headings.replace(/\s/g, '').split('|'); -const applyArguments = ( +const applyArguments = ( supportsDone: boolean, params: Array, - test: Global.EachTestFn, -): Global.EachTestFn => + test: Global.EachTestFn, +): Global.EachTestFn => supportsDone && params.length < test.length ? (done: Global.DoneFn) => test(...params, done) : () => test(...params); diff --git a/packages/jest-each/src/index.ts b/packages/jest-each/src/index.ts index b22d4e305ef6..b18cfd546271 100644 --- a/packages/jest-each/src/index.ts +++ b/packages/jest-each/src/index.ts @@ -22,15 +22,32 @@ const install = ( '`.each` must only be called with an Array or Tagged Template Literal.', ); } - const test = (title: string, test: Global.EachTestFn, timeout?: number) => - bind(g.test)(table, ...data)(title, test, timeout); + const test = ( + title: string, + test: Global.EachTestFn, + timeout?: number, + ) => bind(g.test)(table, ...data)(title, test, timeout); test.skip = bind(g.test.skip)(table, ...data); test.only = bind(g.test.only)(table, ...data); - const it = (title: string, test: Global.EachTestFn, timeout?: number) => - bind(g.it)(table, ...data)(title, test, timeout); + const testConcurrent = ( + title: string, + test: Global.EachTestFn, + timeout?: number, + ) => bind(g.test.concurrent)(table, ...data)(title, test, timeout); + + test.concurrent = testConcurrent; + testConcurrent.only = bind(g.test.concurrent.only)(table, ...data); + testConcurrent.skip = bind(g.test.concurrent.skip)(table, ...data); + + const it = ( + title: string, + test: Global.EachTestFn, + timeout?: number, + ) => bind(g.it)(table, ...data)(title, test, timeout); it.skip = bind(g.it.skip)(table, ...data); it.only = bind(g.it.only)(table, ...data); + it.concurrent = testConcurrent; const xit = bind(g.xit)(table, ...data); const fit = bind(g.fit)(table, ...data); @@ -38,7 +55,7 @@ const install = ( const describe = ( title: string, - suite: Global.EachTestFn, + suite: Global.EachTestFn, timeout?: number, ) => bind(g.describe, false)(table, ...data)(title, suite, timeout); describe.skip = bind(g.describe.skip, false)(table, ...data); diff --git a/packages/jest-jasmine2/src/__tests__/concurrent.test.ts b/packages/jest-jasmine2/src/__tests__/concurrent.test.ts new file mode 100644 index 000000000000..9e65fc57a0d2 --- /dev/null +++ b/packages/jest-jasmine2/src/__tests__/concurrent.test.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +describe('concurrent', () => { + test.concurrent.each([ + [1, 2], + [2, 3], + [3, 4], + ])('should add 1 to number', async (a, sum) => { + expect(a + 1).toEqual(sum); + }); +}); diff --git a/packages/jest-jasmine2/src/each.ts b/packages/jest-jasmine2/src/each.ts index 054754e1b2f6..1b022929cf9a 100644 --- a/packages/jest-jasmine2/src/each.ts +++ b/packages/jest-jasmine2/src/each.ts @@ -24,4 +24,16 @@ export default (environment: JestEnvironment): void => { environment.global.fdescribe, false, ); + environment.global.it.concurrent.each = bindEach( + environment.global.it.concurrent, + false, + ); + environment.global.it.concurrent.only.each = bindEach( + environment.global.it.concurrent.only, + false, + ); + environment.global.it.concurrent.skip.each = bindEach( + environment.global.it.concurrent.skip, + false, + ); }; diff --git a/packages/jest-jasmine2/src/jasmineAsyncInstall.ts b/packages/jest-jasmine2/src/jasmineAsyncInstall.ts index b33e399cc304..f17288875fd7 100644 --- a/packages/jest-jasmine2/src/jasmineAsyncInstall.ts +++ b/packages/jest-jasmine2/src/jasmineAsyncInstall.ts @@ -163,7 +163,11 @@ function makeConcurrent( env: Jasmine['currentEnv_'], mutex: ReturnType, ): Global.ItConcurrentBase { - return function (specName, fn, timeout) { + const concurrentFn = function ( + specName: string, + fn: Global.TestFn, + timeout?: number, + ) { let promise: Promise = Promise.resolve(); const spec = originalFn.call(env, specName, () => promise, timeout); @@ -187,6 +191,9 @@ function makeConcurrent( return spec; }; + // each is binded after the function is made concurrent, so for now it is made noop + concurrentFn.each = () => {}; + return concurrentFn; } export default function jasmineAsyncInstall( diff --git a/packages/jest-types/src/Global.ts b/packages/jest-types/src/Global.ts index df96271c126a..2102d0c0e4b4 100644 --- a/packages/jest-types/src/Global.ts +++ b/packages/jest-types/src/Global.ts @@ -12,6 +12,9 @@ export type TestName = string; export type TestFn = ( done?: DoneFn, ) => Promise | void | undefined; +export type ConcurrentTestFn = ( + done?: DoneFn, +) => Promise; export type BlockFn = () => void; export type BlockName = string; export type HookFn = TestFn; @@ -23,21 +26,30 @@ export type ArrayTable = Table | Row; export type TemplateTable = TemplateStringsArray; export type TemplateData = Array; export type EachTable = ArrayTable | TemplateTable; -export type EachTestFn = ( + +export type TestCallback = BlockFn | TestFn | ConcurrentTestFn; + +export type EachTestFn = ( ...args: Array -) => Promise | void | undefined; +) => ReturnType; // TODO: Get rid of this at some point type Jasmine = {_DEFAULT_TIMEOUT_INTERVAL?: number; addMatchers: Function}; -type Each = ( - table: EachTable, - ...taggedTemplateData: Array -) => (title: string, test: EachTestFn, timeout?: number) => void; +type Each = + | (( + table: EachTable, + ...taggedTemplateData: Array + ) => ( + title: string, + test: EachTestFn, + timeout?: number, + ) => void) + | (() => void); export interface ItBase { (testName: TestName, fn: TestFn, timeout?: number): void; - each: Each; + each: Each; } export interface It extends ItBase { @@ -47,7 +59,8 @@ export interface It extends ItBase { } export interface ItConcurrentBase { - (testName: string, testFn: () => Promise, timeout?: number): void; + (testName: string, testFn: ConcurrentTestFn, timeout?: number): void; + each: Each; } export interface ItConcurrentExtended extends ItConcurrentBase { @@ -61,7 +74,7 @@ export interface ItConcurrent extends It { export interface DescribeBase { (blockName: BlockName, blockFn: BlockFn): void; - each: Each; + each: Each; } export interface Describe extends DescribeBase {