diff --git a/README.md b/README.md index 4576372..9419fb1 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,26 @@ it.each([ See [cypress/integration/title-function.js](./cypress/integration/ title-function.js) for more examples +## Chunking + +There is a built-in chunking helper in `describe.each` and `it.each` to only take a subset of the items. For example, to split all items into 3 chunks, and take the middle one, use + +```js +it.each(items, 3, 1)(...) +``` + +The other spec files can take the other chunks. The index starts at 0, and should be less than the number of chunks. + +```js +// split all items among 3 specs +// spec-a.js +it.each(items, 3, 0)(...) +// spec-b.js +it.each(items, 3, 1)(...) +// spec-c.js +it.each(items, 3, 2)(...) +``` + ## Examples - Watch [Using cypress-each To Create Separate Tests](https://youtu.be/utPKRV_fL1E) diff --git a/cypress/integration/chunk-spec.js b/cypress/integration/chunk-spec.js new file mode 100644 index 0000000..1f074ae --- /dev/null +++ b/cypress/integration/chunk-spec.js @@ -0,0 +1,58 @@ +// @ts-check +/// + +import '../..' + +describe('chunking items', () => { + const items = [1, 2, 3, 4] + + // split all items across four machines + // and this is the first machine, so it should only run the first item + it.each( + items, + 4, + 0, + )('checks %d', (x) => { + expect(x, 'the first item only').to.equal(1) + }) + + it.each( + items, + 4, + 1, + )('checks %d', (x) => { + expect(x, 'the second item only').to.equal(2) + }) + + it.each( + items, + 4, + 2, + )('checks %d', (x) => { + expect(x, 'the third item only').to.equal(3) + }) + + it.each( + items, + 4, + 3, + )('checks %d', (x) => { + expect(x, 'the last item only').to.equal(4) + }) + + it.each( + items, + 2, // split all items into 2 chunks + 0, // and this is chunk index 0 + )('checks %d', (x) => { + expect(x, '1 or 2').to.be.oneOf([1, 2]) + }) + + it.each( + items, + 2, // split all items into 2 chunks + 1, // and this is chunk index 1 + )('checks %d', (x) => { + expect(x, '3 or 4').to.be.oneOf([3, 4]) + }) +}) diff --git a/src/index.d.ts b/src/index.d.ts index 05fca64..9976474 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -8,16 +8,36 @@ declare namespace Mocha { type TestCallback = (this: Context, arg0: T, arg1: any, arg2: any) => void interface TestFunction { - // definition for it.each + /** + * Iterates over each given item (optionally chunked), and creates + * a separate test for each one. + * @param values Input items to create the tests form + * @param totalChunks (Optional) number of chunks to split the items into + * @param chunkIndex (Optional) index of the chunk to get items from + * @example it.each([1, 2, 3])('test %K', (x) => ...) + * @see https://github.com/bahmutov/cypress-each + */ each( values: T[], + totalChunks?: number, + chunkIndex?: number, ): (titlePattern: string | TestTitleFn, fn: TestCallback) => void } interface SuiteFunction { - // definition for describe.each + /** + * Iterates over each given item (optionally chunked), and creates + * a separate suite for each one. + * @param values Input items to create the tests form + * @param totalChunks (Optional) number of chunks to split the items into + * @param chunkIndex (Optional) index of the chunk to get items from + * @example describe.each([1, 2, 3])('suite %K', (item) => ...) + * @see https://github.com/bahmutov/cypress-each + */ each( values: T[], + totalChunks?: number, + chunkIndex?: number, ): (titlePattern: string | TestTitleFn, fn: TestCallback) => void } } diff --git a/src/index.js b/src/index.js index 779c892..73bf0e3 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,25 @@ function formatTitle(pattern, ...values) { return format.apply(null, [pattern].concat(values.slice(0, count))) } +function getChunk(values, totalChunks, chunkIndex) { + // split all items into N chunks and take just a single chunk + if (totalChunks < 0) { + throw new Error('totalChunks must be >= 0') + } + + if (chunkIndex < 0 || chunkIndex >= totalChunks) { + throw new Error( + `Invalid chunk index ${chunkIndex} vs all chunks ${totalChunks}`, + ) + } + + const chunkSize = Math.ceil(values.length / totalChunks) + const chunkStart = chunkIndex * chunkSize + const chunkEnd = chunkStart + chunkSize + const chunk = values.slice(chunkStart, chunkEnd) + return chunk +} + function makeTitle(titlePattern, value, k, values) { if (typeof titlePattern === 'string') { const testTitle = titlePattern.replace('%k', k).replace('%K', k + 1) @@ -27,12 +46,17 @@ function makeTitle(titlePattern, value, k, values) { } if (!it.each) { - it.each = function (values) { + it.each = function (values, totalChunks, chunkIndex) { if (!Array.isArray(values)) { throw new Error('cypress-each: values must be an array') } return function (titlePattern, testCallback) { + if (typeof totalChunks === 'number' && typeof chunkIndex === 'number') { + // split all items into N chunks and take just a single chunk + values = getChunk(values, totalChunks, chunkIndex) + } + values.forEach(function (value, k) { // const testTitle = titlePattern.replace('%k', k).replace('%K', k + 1) const title = makeTitle(titlePattern, value, k, values) @@ -65,6 +89,11 @@ if (!describe.each) { throw new Error('cypress-each: values must be an array') } + if (typeof totalChunks === 'number' && typeof chunkIndex === 'number') { + // split all items into N chunks and take just a single chunk + values = getChunk(values, totalChunks, chunkIndex) + } + return function describeEach(titlePattern, testCallback) { // define a test for each value values.forEach((value, k) => { @@ -89,4 +118,4 @@ if (!describe.each) { } } -module.exports = { formatTitle } +module.exports = { formatTitle, getChunk }