Skip to content

Commit

Permalink
feat: add helper for chunking a list of items, closes #10
Browse files Browse the repository at this point in the history
  • Loading branch information
bahmutov committed Oct 19, 2021
1 parent 8c4bdd1 commit 38aedac
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 4 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
58 changes: 58 additions & 0 deletions cypress/integration/chunk-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @ts-check
/// <reference types="cypress" />

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])
})
})
24 changes: 22 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,36 @@ declare namespace Mocha {
type TestCallback<T> = (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<T = unknown>(
values: T[],
totalChunks?: number,
chunkIndex?: number,
): (titlePattern: string | TestTitleFn<T>, fn: TestCallback<T>) => 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<T = unknown>(
values: T[],
totalChunks?: number,
chunkIndex?: number,
): (titlePattern: string | TestTitleFn<T>, fn: TestCallback<T>) => void
}
}
33 changes: 31 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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) => {
Expand All @@ -89,4 +118,4 @@ if (!describe.each) {
}
}

module.exports = { formatTitle }
module.exports = { formatTitle, getChunk }

0 comments on commit 38aedac

Please sign in to comment.