├── .github └── workflows │ ├── badges.yml │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── README.md ├── cypress.config.ts ├── cypress ├── e2e │ ├── array-of-arrays.cy.ts │ ├── chunk.cy.js │ ├── context-each.cy.js │ ├── context.cy.js │ ├── describe-each.cy.js │ ├── each-k.cy.js │ ├── filter.cy.js │ ├── format.cy.js │ ├── it-each.cy.js │ ├── it.cy.js │ ├── mocha-each.cy.js │ ├── nth.cy.js │ ├── object-input-three-types.cy.ts │ ├── object-input-three.cy.ts │ ├── object-input-two-types.cy.ts │ ├── object-input-two.cy.ts │ ├── object-input.cy.ts │ ├── passes-index.cy.ts │ ├── repeat.cy.js │ ├── sample.cy.js │ ├── title-function.cy.js │ ├── title-position.cy.js │ └── to-pairs.cy.ts └── expected.json ├── images └── titles.png ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── index.d.ts └── index.js └── tsconfig.json /.github/workflows/badges.yml: -------------------------------------------------------------------------------- 1 | name: badges 2 | on: 3 | schedule: 4 | # update badges every night 5 | # because we have a few badges that are linked 6 | # to the external repositories 7 | - cron: '0 3 * * *' 8 | 9 | jobs: 10 | badges: 11 | name: Badges 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Checkout 🛎 15 | uses: actions/checkout@v4 16 | 17 | - name: Update version badges 🏷 18 | run: npx -p dependency-version-badge update-badge cypress 19 | 20 | - name: Commit any changed files 💾 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Updated badges 24 | branch: main 25 | file_pattern: README.md 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push] 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-20.04 6 | 7 | steps: 8 | - name: Checkout 🛎 9 | uses: actions/checkout@v4 10 | 11 | - name: Setup node 20 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | 16 | - name: Cypress tests 🧪 17 | uses: cypress-io/github-action@v6 18 | with: 19 | # check types 20 | build: npm run lint 21 | # use a custom command to conform all expected tests 22 | # were correctly generated 23 | # using https://www.npmjs.com/package/cypress-expect 24 | command: npm test 25 | 26 | - name: Semantic Release 🚀 27 | if: github.ref == 'refs/heads/main' 28 | # https://github.com/cycjimmy/semantic-release-action 29 | uses: cycjimmy/semantic-release-action@v4 30 | with: 31 | branches: main 32 | env: 33 | # github token is automatically created by the GH Action workflow 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | # created using semantic-release 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-each ![cypress version](https://img.shields.io/badge/cypress-14.3.0-brightgreen) [![renovate-app badge][renovate-badge]][renovate-app] [![ci](https://github.com/bahmutov/cypress-each/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bahmutov/cypress-each/actions/workflows/ci.yml) 2 | 3 | > A demo of mocha-each and custom describe.each and it.each implementation for Cypress 4 | 5 | 🎓 Study the course [Cypress Plugins](https://cypress.tips/courses/cypress-plugins) 6 | 7 | - [Lesson f9: Test the data edge cases by making API calls](https://cypress.tips/courses/cypress-plugins/lessons/f9) 8 | - [Lesson f10: Run the same test in different resolutions](https://cypress.tips/courses/cypress-plugins/lessons/f10) 9 | - [Lesson f11: Split the data-driven tests across several spec files](https://cypress.tips/courses/cypress-plugins/lessons/f11) 10 | - [Lesson f12: Store viewports in a JSON file or config](https://cypress.tips/courses/cypress-plugins/lessons/f12) 11 | - [Lesson f13: Create tests from fetched data](https://cypress.tips/courses/cypress-plugins/lessons/f13) 12 | - [Lesson f14: Create N predetermined tests](https://cypress.tips/courses/cypress-plugins/lessons/f14) 13 | - [Lesson f15: Test multiple pages](https://cypress.tips/courses/cypress-plugins/lessons/f15) 14 | 15 | ## Blog posts 16 | 17 | - [Dynamic API Tests Using Cypress-Each Plugin](https://glebbahmutov.com/blog/dynamic-api-tests-using-cypress-each/) 18 | - [Refactor Tests To Be Independent And Fast Using Cypress-Each Plugin](https://glebbahmutov.com/blog/refactor-using-each/) 19 | - [Test your sitemap using Cypress](https://glebbahmutov.com/blog/test-sitemap/) 20 | 21 | ## Videos 22 | 23 | - [Using cypress-each To Create Separate Tests](https://youtu.be/utPKRV_fL1E) 24 | - [Test Each URL From Sitemap In Its Own Separate Cypress Test](https://youtu.be/qkofPocd7lY) 25 | - [Using Faker to generate test data and execute separate tests using cypress-each plugin](https://youtu.be/WO3ujoEhVUc) 26 | 27 | ## Install and use 28 | 29 | ``` 30 | # install using NPM 31 | $ npm i -D cypress-each 32 | # install using Yarn 33 | # yarn add -D cypress-each 34 | ``` 35 | 36 | Import `cypress-each` in a single spec or in Cypress support file 37 | 38 | ```js 39 | import 'cypress-each' 40 | // now can use describe.each and it.each 41 | ``` 42 | 43 | Let's create a separate test for each selector from a list 44 | 45 | ```js 46 | import 'cypress-each' 47 | 48 | // create a separate test for each selector 49 | const selectors = ['header', 'footer', '.new-todo'] 50 | it.each(selectors)('element %s is visible', (selector) => { 51 | cy.visit('/') 52 | cy.get(selector).should('be.visible') 53 | }) 54 | // creates tests 55 | // "element header is visible" 56 | // "element footer is visible" 57 | // "element .new-todo is visible" 58 | ``` 59 | 60 | ## item index 61 | 62 | In addition to the item, the callback receives the index 63 | 64 | ```js 65 | it.each(selectors)('element %s is visible', (selector, k) => { 66 | // k is 0, 1, 2, ... 67 | }) 68 | ``` 69 | 70 | ## Multiple arguments 71 | 72 | You can pass multiple arguments into the callback function by using an array of arrays. For example, to check if an element is visible, invisible, or exists, you can have both a selector and the assertion string for each item. 73 | 74 | ```js 75 | const data = [ 76 | // each entry is an array [selector, assertion] 77 | ['header', 'be.visible'], 78 | ['footer', 'exist'], 79 | ['.new-todo', 'not.be.visible'], 80 | ] 81 | it.each(data)('element %s should %s', (selector, assertion) => { 82 | cy.visit('/') 83 | cy.get(selector).should(assertion) 84 | }) 85 | // creates tests 86 | // "element header should be.visible" 87 | // "element footer should exist" 88 | // "element .new-todo should not.be.visible" 89 | ``` 90 | 91 | ## Repeat the test N times 92 | 93 | You can use this module to simply repeat the test N times 94 | 95 | ```js 96 | // repeat the same test 5 times 97 | it.each(5)('test %K of 5', function (k) { 98 | // note the iteration index k is passed to each test 99 | expect(k).to.be.within(0, 4) 100 | }) 101 | 102 | // you can repeat the suite of tests 103 | describe.each(3)('suite %K of 3', function (k) { 104 | ... 105 | }) 106 | ``` 107 | 108 | See the [repeat-spec.js](./cypress/integration/repeat-spec.js) 109 | 110 | ## Test and suite titles 111 | 112 | You can use the arguments to the test callback in the test title in order. 113 | 114 | ```js 115 | it.each([10, 20, 30])('number is %d', (x) => { ... }) 116 | // creates the tests 117 | // "number is 10" 118 | // "number is 20" 119 | // "number is 30" 120 | ``` 121 | 122 | You can also insert the arguments from the test callback via positions (0-based) into the title 123 | 124 | ```js 125 | const list = [ 126 | ['foo', 'main'], 127 | ['bar', 'edge'], 128 | ] 129 | it.each(list)('testing %1 value %0') 130 | // "testing main value foo" 131 | // "testing edge value bar" 132 | ``` 133 | 134 | If you want to use the iteration variable in the title, use `%k` for zero-based index, or `%K` for one-based index. 135 | 136 | ```js 137 | it.each([10, 20, 30])('checking item %k', (x) => { ... }) 138 | // creates the tests 139 | // "checking item 0" 140 | // "checking item 1" 141 | // "checking item 2" 142 | it.each([10, 20, 30])('checking item %K', (x) => { ... }) 143 | // creates the tests 144 | // "checking item 1" 145 | // "checking item 2" 146 | // "checking item 3" 147 | ``` 148 | 149 | You can use `%N` to insert the total number of items 150 | 151 | ```js 152 | it.each(['first', 'second'])('test %K of %N', (x) => { ... }) 153 | // creates the tests 154 | // "test 1 of 2" 155 | // "test 2 of 2" 156 | ``` 157 | 158 | Example: `it.each([10, 20, 30])('case %K: an item costs $%d.00 on sale', ...` 159 | 160 | ![Formatted test titles](./images/titles.png) 161 | 162 | ### Title function 163 | 164 | You can form the test title yourself using a function. The function will get the item, the index, and all items and should return a string with the test title. 165 | 166 | ```js 167 | function makeTestTitle(s, k, strings) { 168 | return `test ${k + 1} for "${s}"` 169 | } 170 | it.each(['first', 'second'])(makeTestTitle, () => ...) 171 | // creates the tests 172 | // 'test 1 for "first"' 173 | // 'test 2 for "second"' 174 | ``` 175 | 176 | It is very useful for forming a test title based on a property of an object, like 177 | 178 | ```js 179 | it.each([ 180 | { name: 'Joe', age: 30 }, 181 | { name: 'Mary', age: 20 }, 182 | ])( 183 | (person) => `tests person ${person.name}`, 184 | (person) => { ... } 185 | }) 186 | // creates the tests 187 | // "tests person Joe" 188 | // "tests person Mary" 189 | ``` 190 | 191 | See [cypress/integration/title-function.js](./cypress/integration/ title-function.js) for more examples 192 | 193 | ## Every Nth item 194 | 195 | You can quickly take every Nth item from an array 196 | 197 | ```js 198 | it.each(items, N)(...) 199 | ``` 200 | 201 | This is the same as taking the index of the item (zero-based) and doing `k % N === 0` 202 | 203 | ```js 204 | const items = [1, 2, 3, 4, 5, 6, ...] 205 | it.each(items, 3)(...) 206 | // tests item 1, 4, 7, ... 207 | ``` 208 | 209 | ## Chunking 210 | 211 | 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 212 | 213 | ```js 214 | it.each(items, 3, 1)(...) 215 | ``` 216 | 217 | The other spec files can take the other chunks. The index starts at 0, and should be less than the number of chunks. 218 | 219 | ```js 220 | // split all items among 3 specs 221 | // spec-a.js 222 | it.each(items, 3, 0)(...) 223 | // spec-b.js 224 | it.each(items, 3, 1)(...) 225 | // spec-c.js 226 | it.each(items, 3, 2)(...) 227 | ``` 228 | 229 | ## Sampling 230 | 231 | Cypress bundles [Lodash](https://lodash.com/) library which includes `_.sampleSize` method that you can use to randomly pick N items when passing the list to `it.each` 232 | 233 | ```js 234 | // pick 2 random items from the array and create 2 tests 235 | it.each(Cypress._.sampleSize(items, 2))(...) 236 | ``` 237 | 238 | ## Custom filter predicate 239 | 240 | You can filter the items by passing a predicate function 241 | 242 | ```js 243 | it.each(items, (x, k) => ...) 244 | // creates a test for every item the predicate returns a truthy value 245 | ``` 246 | 247 | ## Return value 248 | 249 | `it.each(...)(...)` and `describe.each(...)(...)` return the number of created tests. 250 | 251 | ```js 252 | const n = it.each([1, 2])(...) 253 | // n is 2 254 | ``` 255 | 256 | ## Exclusive tests 257 | 258 | Normally you could run just a selected test using `it.only` or a suite of tests using `describe.only`. Similarly, you could skip a single test or a suite of tests using `it.skip` and `describe.skip` methods. These methods are NOT supported by `it.each` and `describe.each`. Thus if you want to only run the `it.each` tests, surround it with its own `describe` block. 259 | 260 | ```js 261 | // only run the generated tests 262 | describe.only('my tests', () => { 263 | it.each(items)(...) 264 | }) 265 | // skip these tests 266 | describe.skip('obsolete generated tests', () => { 267 | it.each(items)(...) 268 | }) 269 | // run just these suites of generated tests 270 | describe.only('my suites of tests', () => { 271 | describe.each(items)(...) 272 | }) 273 | ``` 274 | 275 | ## Test configuration object 276 | 277 | Cypress allows to pass some of its configuration options in the `it` and `describe` arguments, see [the configuration](https://on.cypress.io/configuration) page. These methods `it.each` and `describe.each` do not support this, but you can create a wrapper `describe` block and set the options there, if needed. 278 | 279 | ```js 280 | // if a test inside this suite fails, 281 | // retry it up to two times before failing it 282 | describe('user', { retries: 2 }, () => { 283 | it.each(users)(...) 284 | }) 285 | ``` 286 | 287 | ## Run specs in parallel 288 | 289 | See the explanation in the blog post [Refactor Tests To Be Independent And Fast Using Cypress-Each Plugin](https://glebbahmutov.com/blog/refactor-using-each/), but basically you create separate specs file, and each just uses `cypress-each` to run a subset of the tests 290 | 291 | ```js 292 | // utils.js 293 | export const testTitle = (selector, k) => 294 | `testing ${k + 1} ...` 295 | 296 | export const testDataItem = (item) => { 297 | ... 298 | } 299 | 300 | // spec1.js 301 | import { data } from '...' 302 | import { testTitle, testDataItem } from './utils' 303 | it.each(data, 3, 0)(testTitle, testDataItem) 304 | 305 | // spec2.js 306 | import { data } from '...' 307 | import { testTitle, testDataItem } from './utils' 308 | it.each(data, 3, 1)(testTitle, testDataItem) 309 | 310 | // spec3.js 311 | import { data } from '...' 312 | import { testTitle, testDataItem } from './utils' 313 | it.each(data, 3, 2)(testTitle, testDataItem) 314 | ``` 315 | 316 | ## Test case object 317 | 318 | Sometimes you just want to have a single object that has all the tests cases together with the inputs. You can pass an object instead of an array to the `it.each` function. Each object key will become the test title, and the value will be passed to the test callback. If the value is an array, it will be destructured. See [object-input.cy.ts](./cypress/e2e/object-input.cy.ts) spec file for details. 319 | 320 | ```ts 321 | const testCases = { 322 | // key: the test label 323 | // value: list of inputs for each test case 324 | 'positive numbers': [1, 6, 7], // [a, b, expected result] 325 | 'negative numbers': [1, -6, -5], 326 | } 327 | it.each(testCases)((a, b, expectedResult) => { 328 | expect(add(a, b)).to.equal(expectedResult) 329 | }) 330 | ``` 331 | 332 | ### test case types 333 | 334 | Note that in most cases, the `it.each(TestCases)` tries to "guess" the types from the array value to the test callback function. When you need to, use the utility types to "explain" the value array: 335 | 336 | ```ts 337 | // two arguments 338 | // each value is [number, string] 339 | const toString: TestCaseObject2 = { 340 | one: [1, '1'], 341 | ten: [10, '10'], 342 | } 343 | 344 | it.each(toString)((a, b) => { 345 | // a is a number 346 | // b is a string 347 | }) 348 | 349 | // three arguments 350 | const additions: TestCaseObject3 = { 351 | one: [1, 2, '3'], // a + b in string form 352 | ten: [10, 20, '30'], 353 | } 354 | 355 | it.each(additions)((a, b, s) => { 356 | expect(String(a + b)).to.equal(s) 357 | }) 358 | ``` 359 | 360 | ## Static data 361 | 362 | ⚠️ **Important:** 363 | 364 | In order for this plugin to _create_ tests, the data must be available _before_ any tests are running. Thus we _cannot_ use `cy.fixture` to load the data and "give" it to `cypress-each`. 365 | 366 | ```js 367 | 🚨 DOES NOT WORK 368 | let list 369 | before(() => { 370 | cy.fixture('list.json').then(data => list = data) 371 | }) 372 | it.each(list)(...) 373 | // Nope, the list will always be undefined 374 | ``` 375 | 376 | There are a couple of workarounds: 377 | 378 | - import JSON data directly into the spec 379 | 380 | ```js 381 | // ✅ static JSON import 382 | import list from '../fixtures/list.json' 383 | it.each(list)(...) 384 | ``` 385 | 386 | - load the data from the config file and pass it via `Cypress.env` 387 | 388 | See the [Cypress Plugins](https://cypress.tips/courses/cypress-plugins) course for hands-on examples. 389 | 390 | ## Specs 391 | 392 | Find the implementation in [src/index.js](./src/index.js) 393 | 394 | - [it-spec.js](./cypress/integration/it-spec.js) uses no shortcuts to define multiple tests that are almost the same. We want to avoid the repetition 395 | - [it-each-spec.js](./cypress/integration/it-each-spec.js) uses the `it.each` helper to generate multiple `it` tests given a data array 396 | - [describe-each-spec.js](./cypress/integration/describe-each-spec.js) uses `describe.each` helper to create `describe` blocks for each item in the given data array 397 | - [mocha-each-spec.js](cypress/integration/mocha-each-spec.js) uses 3rd party [mocha-each](https://github.com/ryym/mocha-each) to generate `it` tests for each data item 398 | 399 | ## Types 400 | 401 | This package includes TypeScript definition for `it.each` and `describe.each`. Thus the parameter should be the right type from the array of values: 402 | 403 | ```js 404 | it.each([ 405 | { name: 'Joe', age: 30 }, 406 | { name: 'Mary', age: 20 }, 407 | ])('has correct types', (user) => { 408 | // the type for the "user" should be 409 | // name: string, age: number 410 | expect(user).to.have.keys('name', 'age') 411 | expect(user.name).to.be.a('string') 412 | expect(user.age).to.be.a('number') 413 | }) 414 | ``` 415 | 416 | Include this module with other library types, like 417 | 418 | ```json 419 | { 420 | "compilerOptions": { 421 | "types": ["cypress", "cypress-each"] 422 | } 423 | } 424 | ``` 425 | 426 | Or inside an individual spec file add 427 | 428 | ```js 429 | /// 430 | ``` 431 | 432 | ## Small print 433 | 434 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2021 435 | 436 | - [@bahmutov](https://twitter.com/bahmutov) 437 | - [glebbahmutov.com](https://glebbahmutov.com) 438 | - [blog](https://glebbahmutov.com/blog) 439 | - [videos](https://www.youtube.com/glebbahmutov) 440 | - [presentations](https://slides.com/bahmutov) 441 | - [cypress.tips](https://cypress.tips) 442 | 443 | License: MIT - do anything with the code, but don't blame me if it does not work. 444 | 445 | Support: if you find any problems with this module, email / tweet / 446 | [open issue](https://github.com/bahmutov/cypress-each/issues) on Github 447 | 448 | ## MIT License 449 | 450 | Copyright (c) 2021 Gleb Bahmutov <gleb.bahmutov@gmail.com> 451 | 452 | Permission is hereby granted, free of charge, to any person 453 | obtaining a copy of this software and associated documentation 454 | files (the "Software"), to deal in the Software without 455 | restriction, including without limitation the rights to use, 456 | copy, modify, merge, publish, distribute, sublicense, and/or sell 457 | copies of the Software, and to permit persons to whom the 458 | Software is furnished to do so, subject to the following 459 | conditions: 460 | 461 | The above copyright notice and this permission notice shall be 462 | included in all copies or substantial portions of the Software. 463 | 464 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 465 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 466 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 467 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 468 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 469 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 470 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 471 | OTHER DEALINGS IN THE SOFTWARE. 472 | 473 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 474 | [renovate-app]: https://renovateapp.com/ 475 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | fixturesFolder: false, 5 | e2e: { 6 | setupNodeEvents(on, config) {}, 7 | supportFile: false, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/array-of-arrays.cy.ts: -------------------------------------------------------------------------------- 1 | import '../../src' 2 | 3 | describe('Array of arrays', () => { 4 | it.each([ 5 | [1, 'foo', true], 6 | [2, 'bar', false], 7 | ])('test with %d %s', (a: number, b: string, c: boolean) => { 8 | // a should be a number 9 | // b should be a string 10 | // b should be a boolean 11 | expect(a, 'first argument').to.be.a('number') 12 | expect(b, 'second argument').to.be.a('string') 13 | expect(c, 'third argument').to.be.a('boolean') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /cypress/e2e/chunk.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../..' 5 | 6 | describe('chunking items', () => { 7 | const items = [1, 2, 3, 4] 8 | 9 | // split all items across four machines 10 | // and this is the first machine, so it should only run the first item 11 | it.each( 12 | items, 13 | 4, 14 | 0, 15 | )('checks %d', (x) => { 16 | expect(x, 'the first item only').to.equal(1) 17 | }) 18 | 19 | it.each( 20 | items, 21 | 4, 22 | 1, 23 | )('checks %d', (x) => { 24 | expect(x, 'the second item only').to.equal(2) 25 | }) 26 | 27 | it.each( 28 | items, 29 | 4, 30 | 2, 31 | )('checks %d', (x) => { 32 | expect(x, 'the third item only').to.equal(3) 33 | }) 34 | 35 | it.each( 36 | items, 37 | 4, 38 | 3, 39 | )('checks %d', (x) => { 40 | expect(x, 'the last item only').to.equal(4) 41 | }) 42 | 43 | it.each( 44 | items, 45 | 2, // split all items into 2 chunks 46 | 0, // and this is chunk index 0 47 | )('checks %d', (x) => { 48 | expect(x, '1 or 2').to.be.oneOf([1, 2]) 49 | }) 50 | 51 | it.each( 52 | items, 53 | 2, // split all items into 2 chunks 54 | 1, // and this is chunk index 1 55 | )('checks %d', (x) => { 56 | expect(x, '3 or 4').to.be.oneOf([3, 4]) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /cypress/e2e/context-each.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../../src' 5 | 6 | const n = context.each(['A', 1])('%s', (x) => { 7 | before(() => { 8 | expect(n, 'number of created suites').to.equal(2) 9 | }) 10 | // we can use the values passed into the "context" callback 11 | // because these are closure variables 12 | it(`checks out for ${x}`, () => { 13 | cy.wrap(x).should('equal', x) 14 | }) 15 | }) 16 | 17 | context.each([ 18 | { name: 'Joe', age: 30 }, 19 | { name: 'Mary', age: 20 }, 20 | ])('has correct types', (user) => { 21 | it('checks out', () => { 22 | expect(user).to.have.keys('name', 'age') 23 | expect(user.name).to.be.a('string') 24 | expect(user.age).to.be.a('number') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/e2e/context.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../../src' 5 | 6 | it('has test title', function () { 7 | expect(this, 'has test').to.have.property('test') 8 | // @ts-ignore 9 | expect(this.test.title).to.equal('has test title') 10 | }) 11 | 12 | it.each([1])('has test context', function (x) { 13 | expect(x).to.equal(1) 14 | // needs test context to be passed correctly by it.each 15 | expect(this, 'has test').to.have.property('test') 16 | // @ts-ignore 17 | expect(this.test.title).to.equal('has test context') 18 | }) 19 | 20 | it.each([[1, 2]])('has test context inside an array', function (x, y) { 21 | expect(x).to.equal(1) 22 | expect(y).to.equal(2) 23 | // needs test context to be passed correctly by it.each 24 | expect(this, 'has test').to.have.property('test') 25 | // @ts-ignore 26 | expect(this.test.title).to.equal('has test context inside an array') 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/describe-each.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../..' 5 | 6 | const n = describe.each(['A', 1])('%s', (x) => { 7 | before(() => { 8 | expect(n, 'number of created suites').to.equal(2) 9 | }) 10 | // we can use the values passed into the "describe" callback 11 | // because these are closure variables 12 | it(`checks out for ${x}`, () => { 13 | cy.wrap(x).should('equal', x) 14 | }) 15 | }) 16 | 17 | describe.each([ 18 | { name: 'Joe', age: 30 }, 19 | { name: 'Mary', age: 20 }, 20 | ])('has correct types', (user) => { 21 | it('checks out', () => { 22 | expect(user).to.have.keys('name', 'age') 23 | expect(user.name).to.be.a('string') 24 | expect(user.age).to.be.a('number') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/e2e/each-k.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../..' 5 | 6 | describe('Using %k and %K placeholders', () => { 7 | it('has test title', function () { 8 | expect(this, 'has test').to.have.property('test') 9 | // @ts-ignore 10 | expect(this.test.title).to.equal('has test title') 11 | }) 12 | 13 | it.each([1, 2, 3])('has 0-based index %k', function (x) { 14 | expect(x).to.be.oneOf([1, 2, 3]) 15 | // needs test context to be passed correctly by it.each 16 | // expect(this.test.title).to.equal('has 0-based index 0') 17 | }) 18 | 19 | it.each([1, 2, 3])('has 1-based index %K', (x) => { 20 | expect(x).to.be.oneOf([1, 2, 3]) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/e2e/filter.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../../src' 5 | 6 | describe('filter function', () => { 7 | const items = [1, 2, 3, 4] 8 | 9 | const n = it.each(items, (x, k) => x === 4)( 10 | 'only the last item matches %d', 11 | (x) => { 12 | expect(x).to.equal(4) 13 | expect(n, 'number of created tests').to.equal(1) 14 | }, 15 | ) 16 | 17 | it.each(items, (x, k) => k === 1)( 18 | 'only allows the 2nd item by index %d', 19 | (x) => { 20 | expect(x).to.equal(2) 21 | }, 22 | ) 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/e2e/format.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | // @ts-ignore 5 | require('../..') 6 | // @ts-ignore 7 | const { formatTitle, makeTitle } = require('../../src/index.js') 8 | 9 | describe('formatted title', () => { 10 | // each value will be automatically inserted into the formatted test title 11 | it.each([10, 20, 30])('case %K: an item costs $%d.00 on sale', function (x) { 12 | expect(x, 'item cost').to.be.oneOf([10, 20, 30]) 13 | if (this.test) { 14 | expect(this.test.title).to.be.oneOf([ 15 | 'case 1: an item costs $10.00 on sale', 16 | 'case 2: an item costs $20.00 on sale', 17 | 'case 3: an item costs $30.00 on sale', 18 | ]) 19 | } 20 | }) 21 | }) 22 | 23 | describe('format', () => { 24 | const person = { 25 | name: 'Joe', 26 | } 27 | 28 | it('formats title using %d', () => { 29 | expect(formatTitle('one is %d', 1)).to.equal('one is 1') 30 | }) 31 | 32 | // it should only use the number of arguments 33 | // in the string format, and not all available arguments 34 | it.each([['person', person, 42]])('I am a %s', (name, who, life) => { 35 | expect(name).to.equal('person') 36 | expect(who).to.equal(person) 37 | expect(life).to.equal(42) 38 | }) 39 | 40 | it.each([['person', 42, person]])('I am a %s at %d', (name, life, who) => { 41 | expect(name).to.equal('person') 42 | expect(who).to.equal(person) 43 | expect(life).to.equal(42) 44 | }) 45 | 46 | it.each([['person', 42, person]])( 47 | 'I use no format placeholders', 48 | (name, life, who) => { 49 | expect(name).to.equal('person') 50 | expect(who).to.equal(person) 51 | expect(life).to.equal(42) 52 | }, 53 | ) 54 | }) 55 | 56 | describe('makeTitle', () => { 57 | const values = [1, 2, 3] 58 | 59 | it('makes title using %K', () => { 60 | expect(makeTitle('one is %K', 1, 0, values)).to.equal('one is 1') 61 | }) 62 | 63 | it('makes title using %k and value', () => { 64 | expect(makeTitle('at index %k is value %d', 42, 0, values)).to.equal( 65 | 'at index 0 is value 42', 66 | ) 67 | }) 68 | 69 | it('makes title using number of values', () => { 70 | expect(makeTitle('value %d is %K of %N', 42, 0, values)).to.equal( 71 | 'value 42 is 1 of 3', 72 | ) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /cypress/e2e/it-each.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../..' 5 | 6 | describe('using it.each', () => { 7 | it.each(['A', 1])('checks %s', (x) => { 8 | cy.wrap(x).should('equal', x) 9 | }) 10 | 11 | it.each([['A', 'a']])('capital letter %s !== %s', (X, x) => { 12 | cy.wrap(X).should('not.equal', x) 13 | }) 14 | 15 | it.each([ 16 | { name: 'Joe', age: 30 }, 17 | { name: 'Mary', age: 20 }, 18 | ])('has correct types', (user) => { 19 | expect(user).to.have.keys('name', 'age') 20 | expect(user.name).to.be.a('string') 21 | expect(user.age).to.be.a('number') 22 | }) 23 | 24 | it.each([ 25 | [1, 'foo'], 26 | [2, 'bar'], 27 | ])('title', (a, b) => { 28 | expect(a).to.be.a('number') 29 | expect(b).to.be.a('string') 30 | expect(b.length).to.equal(3) 31 | }) 32 | }) 33 | 34 | // just an example for checking the thrown error 35 | // it.each()('throws meaningful error', () => {}) 36 | -------------------------------------------------------------------------------- /cypress/e2e/it.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | describe('has expected values', () => { 5 | it('A = A', () => { 6 | cy.wrap('A').should('equal', 'A') 7 | }) 8 | 9 | it('1 = 1', () => { 10 | cy.wrap(1).should('equal', 1) 11 | }) 12 | 13 | it('capital letter A !== a', () => { 14 | cy.wrap('A').should('not.equal', 'a') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /cypress/e2e/mocha-each.cy.js: -------------------------------------------------------------------------------- 1 | // https://github.com/ryym/mocha-each 2 | const forEach = require('mocha-each') 3 | 4 | function add(a, b) { 5 | return parseInt(a) + parseInt(b) 6 | } 7 | 8 | describe('add()', () => { 9 | forEach([ 10 | [1, 1, 2], 11 | [2, -2, 0], 12 | [140, 48, 188], 13 | ]).it('adds %d and %d then returns %d', (left, right, expected) => { 14 | expect(add(left, right)).to.equal(expected) 15 | }) 16 | 17 | context('with invalid arguments', () => { 18 | forEach([ 19 | [1, 'foo'], 20 | [null, 10], 21 | [{}, []], 22 | ]).it('adds %j and %j then returns NaN', (left, right) => { 23 | const value = add(left, right) 24 | expect(isNaN(value)).to.be.true 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/nth.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../../src' 5 | 6 | describe('nth item', () => { 7 | const items = [1, 2, 3, 4] 8 | 9 | context('every 2nd', () => { 10 | // take every 2nd item, same as taking the item's zero-index module 2 11 | const n = it.each(items, 2)('every 2nd item %K', (x) => { 12 | expect(x, '1 or 3').to.be.oneOf([1, 3]) 13 | expect(n, 'number of created tests').to.equal(2) 14 | }) 15 | }) 16 | 17 | context('every 3nd', () => { 18 | const n = it.each(items, 3)('every 3nd item %K', (x) => { 19 | expect(x, '1 or 4').to.be.oneOf([1, 4]) 20 | expect(n, 'number of created tests').to.equal(2) 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/e2e/object-input-three-types.cy.ts: -------------------------------------------------------------------------------- 1 | import '../../src' 2 | 3 | context('mixed types in the list', () => { 4 | const additions: TestCaseObject3 = { 5 | one: [1, 2, '3'], // a + b in string form 6 | ten: [10, 20, '30'], 7 | } 8 | 9 | it.each(additions)((a, b, s) => { 10 | expect(String(a + b)).to.equal(s) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/object-input-three.cy.ts: -------------------------------------------------------------------------------- 1 | import '../../src' 2 | 3 | function add(a: number, b: number) { 4 | return a + b 5 | } 6 | 7 | type TestCases = { [index: string]: [number, number, number] } 8 | 9 | const testCases: TestCases = { 10 | // key: the test label 11 | // value: list of inputs for each test case 12 | 'positive numbers': [1, 6, 7], // [a, b, expected result] 13 | 'negative numbers': [1, -6, -5], 14 | } 15 | 16 | it.each(testCases)((a: number, b: number, expectedResult: number) => { 17 | expect(add(a, b)).to.equal(expectedResult) 18 | }) 19 | -------------------------------------------------------------------------------- /cypress/e2e/object-input-two-types.cy.ts: -------------------------------------------------------------------------------- 1 | import '../../src' 2 | 3 | context('mixed types in the list', () => { 4 | const toString: TestCaseObject2 = { 5 | one: [1, '1'], 6 | ten: [10, '10'], 7 | } 8 | 9 | it.each(toString)((a, b) => { 10 | expect(String(a)).to.equal(b) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/object-input-two.cy.ts: -------------------------------------------------------------------------------- 1 | import '../../src' 2 | 3 | context('array with 2 arguments', () => { 4 | const lessThan: TestCaseObject2 = { 5 | 'one is less than two': [1, 2], 6 | 'ten is less than twenty': [10, 20], 7 | } 8 | 9 | it.each(lessThan)((a, b) => { 10 | expect(a).to.be.lessThan(b) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/object-input.cy.ts: -------------------------------------------------------------------------------- 1 | import '../../src' 2 | 3 | function add(a: number, b: number) { 4 | return a + b 5 | } 6 | // https://github.com/bahmutov/cypress-each/issues/53 7 | describe('automatic conversion', () => { 8 | context('plain values', () => { 9 | const evenCases = { 10 | two: 2, 11 | four: 4, 12 | } 13 | 14 | function isEven(x: number) { 15 | return x % 2 === 0 16 | } 17 | 18 | it.each(evenCases)((x) => { 19 | expect(isEven(x)).to.be.true 20 | expect(x).to.be.a('number') 21 | expect(x).to.satisfy(isEven) 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /cypress/e2e/passes-index.cy.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../..' 5 | 6 | describe('passes index', () => { 7 | const items = ['foo', 'bar', 'baz'] 8 | 9 | it.each(items)('index %K', (item, k) => { 10 | expect(k, `index ${k}`).to.be.within(0, items.length - 1) 11 | expect(item, `item ${item}`).to.equal(items[k]) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /cypress/e2e/repeat.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../..' 5 | 6 | describe('Simply repeating the test N times', () => { 7 | it.each(5)('test %K of 5', function (k) { 8 | expect(k).to.be.within(0, 4) 9 | }) 10 | }) 11 | 12 | describe.each(3)('suite %K of 3', function (k) { 13 | it('works', () => { 14 | expect(k).to.be.within(0, 2) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /cypress/e2e/sample.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../..' 5 | 6 | describe('_.sampleSize', () => { 7 | const items = [1, 2, 3, 4] 8 | 9 | // pick two elements from the array 10 | const n = it.each(Cypress._.sampleSize(items, 2))( 11 | 'checks %K sample %d', 12 | (x) => { 13 | expect(x, 'one of the four').to.be.oneOf(items) 14 | expect(n, 'number of created tests').to.equal(2) 15 | }, 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /cypress/e2e/title-function.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../../src' 5 | 6 | /** 7 | * Forms the test title from the item and index 8 | * @param {string} s Individual string 9 | * @param {number} k Item index (0-based) 10 | * @param {string[]} strings All strings 11 | * @returns 12 | */ 13 | function makeTestTitle(s, k, strings) { 14 | expect(s, 'first argument is the item').to.equal('foo') 15 | expect(k, 'second argument is the index').to.equal(0) 16 | expect(strings, 'all items').to.deep.equal(['foo']) 17 | 18 | return `test ${k + 1} for "${s}"` 19 | } 20 | 21 | describe('Form test title using a function', () => { 22 | it.each(['foo'])(makeTestTitle, function (s) { 23 | expect(s, 'item value').to.equal('foo') 24 | // @ts-ignore 25 | expect(this.test.title, 'computed test title').to.equal('test 1 for "foo"') 26 | }) 27 | 28 | it.each([ 29 | { name: 'Joe', age: 30 }, 30 | { name: 'Mary', age: 20 }, 31 | ])( 32 | (person) => `tests person ${person.name}`, 33 | function (user) { 34 | expect(user).to.have.keys('name', 'age') 35 | // @ts-ignore 36 | expect(this.test.title, 'computed test title').to.be.oneOf([ 37 | 'tests person Joe', 38 | 'tests person Mary', 39 | ]) 40 | }, 41 | ) 42 | }) 43 | -------------------------------------------------------------------------------- /cypress/e2e/title-position.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import '../../src' 5 | 6 | describe('Positional title arguments', () => { 7 | const list = [['foo', 'main']] 8 | 9 | // https://github.com/bahmutov/cypress-each/issues/76 10 | // @ts-ignore 11 | it.each(list)('title is %1 and %0', function (a, b) { 12 | expect(a, 'first').to.equal('foo') 13 | expect(b, 'second').to.equal('main') 14 | // @ts-ignore 15 | expect(this.test.title, 'computed test title').to.equal( 16 | 'title is main and foo', 17 | ) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /cypress/e2e/to-pairs.cy.ts: -------------------------------------------------------------------------------- 1 | import '../../src' 2 | 3 | function add(a: number, b: number) { 4 | return a + b 5 | } 6 | 7 | const testCases = { 8 | // key: the test label 9 | // value: list of inputs for each test case 10 | 'positive numbers': [1, 6, 7], // [a, b, expected result] 11 | 'negative numbers': [1, -6, -5], 12 | } 13 | 14 | describe('converted to an array of arrays', () => { 15 | // we can convert the above object ourselves 16 | const pairs = Cypress._.toPairs(testCases) 17 | it.each(pairs)('testing %0', (title, numbers: number[]) => { 18 | const [a, b, expectedResult] = numbers 19 | expect(add(a, b)).to.equal(expectedResult) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /cypress/expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": { 3 | "checks out for A": "passed" 4 | }, 5 | "1": { 6 | "checks out for 1": "passed" 7 | }, 8 | "using it.each": { 9 | "checks A": "passed", 10 | "checks 1": "passed", 11 | "capital letter A !== a": "passed" 12 | }, 13 | "has expected values": { 14 | "A = A": "passed", 15 | "1 = 1": "passed", 16 | "capital letter A !== a": "passed" 17 | }, 18 | "add()": { 19 | "adds 1 and 1 then returns 2": "passed", 20 | "adds 2 and -2 then returns 0": "passed", 21 | "adds 140 and 48 then returns 188": "passed", 22 | "with invalid arguments": { 23 | "adds 1 and \"foo\" then returns NaN": "passed", 24 | "adds null and 10 then returns NaN": "passed", 25 | "adds {} and [] then returns NaN": "passed" 26 | } 27 | }, 28 | "format": { 29 | "I am a person": "passed", 30 | "I am a person at 42": "passed", 31 | "I use no format placeholders": "passed" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /images/titles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-each/85d8b1fb2b24e4a171f7ad33c9442c7f8acc78cb/images/titles.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-each", 3 | "version": "0.0.0-development", 4 | "description": "Simple implementation for describe.each and it.each", 5 | "main": "src", 6 | "types": "src/index.d.ts", 7 | "files": [ 8 | "src" 9 | ], 10 | "scripts": { 11 | "lint": "tsc --pretty --allowJs --strict --noEmit src/index.js cypress/**/*.js cypress/**/*.ts", 12 | "test": "cypress-expect run --expect cypress/expected.json", 13 | "semantic-release": "semantic-release" 14 | }, 15 | "keywords": [], 16 | "author": "Gleb Bahmutov ", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "cypress": "14.3.0", 20 | "cypress-expect": "2.5.3", 21 | "mocha-each": "^2.0.1", 22 | "prettier": "^2.4.1", 23 | "semantic-release": "24.2.3", 24 | "typescript": "^4.8.4" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/bahmutov/cypress-each.git" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "prHourlyLimit": 2, 5 | "updateNotScheduled": false, 6 | "timezone": "America/New_York", 7 | "schedule": ["after 10pm and before 5am on every weekday", "every weekend"], 8 | "masterIssue": true, 9 | "labels": ["type: dependencies", "renovate"], 10 | "packageRules": [ 11 | { 12 | "packagePatterns": ["*"], 13 | "excludePackagePatterns": [ 14 | "cypress", 15 | "cypress-expect", 16 | "semantic-release" 17 | ], 18 | "enabled": false 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // types for it.each and describe.each 2 | // any help improving them is welcome 3 | // https://github.com/bahmutov/cypress-each 4 | 5 | type TestTitleFn = (item: T, index: number, items: T[]) => string 6 | type ItemPredicateFunction = (item: T, index: number, items: T[]) => boolean 7 | 8 | type TestCaseObject3 = { 9 | [index: string]: [T0, T1, T2] 10 | } 11 | 12 | type TestCaseObject2 = { 13 | [index: string]: [T0, T1] 14 | } 15 | 16 | type TestCaseObject = { 17 | [index: string]: T 18 | } 19 | 20 | declare namespace Mocha { 21 | type TestCallback = T extends [] 22 | ? (this: Context, arg1: any, arg2: any) => void 23 | : Parameters<(...res: [...T, any, any]) => void> extends [...infer R] 24 | ? R extends readonly [...T, any, any] 25 | ? (this: Context, ...res: [...R]) => void 26 | : never 27 | : never 28 | type TestCallback1 = (this: Context, arg0: T0) => void 29 | type TestCallback2 = (this: Context, arg0: T0, arg1: T1) => void 30 | type TestCallback3 = ( 31 | this: Context, 32 | arg0: T0, 33 | arg1: T1, 34 | arg2: T2, 35 | ) => void 36 | 37 | interface TestFunction { 38 | /** 39 | * Iterates over each given item (optionally chunked), and creates 40 | * a separate test for each one. 41 | * @param values Input items to create the tests form OR number of times to repeat a test 42 | * @param totalChunks (Optional) number of chunks to split the items into, or Nth filter, or a predicate function 43 | * @param chunkIndex (Optional) index of the chunk to get items from 44 | * @example it.each([1, 2, 3])('test %K', (x) => ...) 45 | * @see https://github.com/bahmutov/cypress-each 46 | */ 47 | each( 48 | values: Array, 49 | totalChunks?: number, 50 | chunkIndex?: number, 51 | ): ( 52 | titlePattern: string | TestTitleFn<[...T]>, 53 | fn: TestCallback<[...T]>, 54 | ) => void 55 | each( 56 | values: T[] | number, 57 | totalChunks?: number | ItemPredicateFunction, 58 | chunkIndex?: number, 59 | ): (titlePattern: string | TestTitleFn, fn: TestCallback<[T]>) => void 60 | 61 | /** 62 | * A single test case object where the keys are test titles, 63 | * and the values are used as inputs to the test callback 64 | * @see https://github.com/bahmutov/cypress-each#test-case-object 65 | * @example 66 | * const testCases = { 67 | * // key: the test label 68 | * // value: list of inputs for each test case 69 | * 'positive numbers': [1, 6, 7], // [a, b, expected result] 70 | * 'negative numbers': [1, -6, -5], 71 | * } 72 | * it.each(testCases)((a, b, result) => { ... }) 73 | */ 74 | each( 75 | testCases: TestCaseObject3, 76 | ): (fn: TestCallback3) => void 77 | 78 | /** 79 | * A single test case object where the keys are test titles, 80 | * and the values are used as inputs to the test callback 81 | * @see https://github.com/bahmutov/cypress-each#test-case-object 82 | * @example 83 | * const testCases = { 84 | * // key: the test label 85 | * // value: list of inputs for each test case 86 | * 'positive numbers': [1, 6, 7], // [a, b, expected result] 87 | * 'negative numbers': [1, -6, -5], 88 | * } 89 | * it.each(testCases)((a, b, result) => { ... }) 90 | */ 91 | each( 92 | testCases: TestCaseObject2, 93 | ): (fn: TestCallback2) => void 94 | 95 | /** 96 | * A single test case object where the keys are test titles, 97 | * and the single value are used as inputs to the test callback 98 | * @see https://github.com/bahmutov/cypress-each#test-case-object 99 | * @example 100 | * const testCases = { 101 | * 'two': 2, 102 | * 'three': 3, 103 | * } 104 | * it.each(testCases)((a) => { ... }) 105 | */ 106 | each(testCases: TestCaseObject): (fn: TestCallback1) => void 107 | } 108 | 109 | interface SuiteFunction { 110 | /** 111 | * Iterates over each given item (optionally chunked), and creates 112 | * a separate suite for each one. 113 | * @param values Input items to create the tests form 114 | * @param totalChunks (Optional) number of chunks to split the items into 115 | * @param chunkIndex (Optional) index of the chunk to get items from 116 | * @example describe.each([1, 2, 3])('suite %K', (item) => ...) 117 | * @see https://github.com/bahmutov/cypress-each 118 | */ 119 | each( 120 | values: T[] | number, 121 | totalChunks?: number | ItemPredicateFunction, 122 | chunkIndex?: number, 123 | ): (titlePattern: string | TestTitleFn, fn: TestCallback<[T]>) => void 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // standard Node module "util" has "format" function 4 | const { format } = require('util') 5 | 6 | function formatTitle(pattern, ...values) { 7 | // count how many format placeholders are in the pattern 8 | // by counting the "%" characters 9 | const placeholders = pattern.match(/%/g) 10 | const count = placeholders ? placeholders.length : 0 11 | return format.apply(null, [pattern].concat(values.slice(0, count))) 12 | } 13 | 14 | function getChunk(values, totalChunks, chunkIndex) { 15 | // split all items into N chunks and take just a single chunk 16 | if (totalChunks < 0) { 17 | throw new Error('totalChunks must be >= 0') 18 | } 19 | 20 | if (chunkIndex < 0 || chunkIndex >= totalChunks) { 21 | throw new Error( 22 | `Invalid chunk index ${chunkIndex} vs all chunks ${totalChunks}`, 23 | ) 24 | } 25 | 26 | const chunkSize = Math.ceil(values.length / totalChunks) 27 | const chunkStart = chunkIndex * chunkSize 28 | const chunkEnd = chunkStart + chunkSize 29 | const chunk = values.slice(chunkStart, chunkEnd) 30 | return chunk 31 | } 32 | 33 | function makeTitle(titlePattern, value, k, values) { 34 | if (typeof titlePattern === 'string') { 35 | let testTitle = titlePattern 36 | .replace('%k', k) 37 | .replace('%K', k + 1) 38 | .replace('%N', values.length) 39 | if (Array.isArray(value)) { 40 | // apply any positional arguments 41 | // https://github.com/bahmutov/cypress-each/issues/50 42 | testTitle = testTitle 43 | .replace('%0', value[0]) 44 | .replace('%1', value[1]) 45 | .replace('%2', value[2]) 46 | return formatTitle(testTitle, ...value) 47 | } else { 48 | return formatTitle(testTitle, value) 49 | } 50 | } else if (typeof titlePattern === 'function') { 51 | return titlePattern(value, k, values) 52 | } 53 | 54 | throw new Error('titlePattern must be a string or function') 55 | } 56 | 57 | if (!it.each) { 58 | it.each = function (values, totalChunks, chunkIndex) { 59 | if (typeof values === 'number') { 60 | // the user wants to repeat the same test N times 61 | if (values < 1) { 62 | throw new Error('Number of test repetitions must be >= 1') 63 | } 64 | values = Cypress._.range(0, values) 65 | } 66 | 67 | if (typeof totalChunks === 'number') { 68 | if (!Array.isArray(values)) { 69 | throw new Error('cypress-each: values must be an array') 70 | } 71 | } 72 | 73 | return function (titlePattern, testCallback) { 74 | if (typeof totalChunks === 'number' && typeof chunkIndex === 'number') { 75 | // split all items into N chunks and take just a single chunk 76 | values = getChunk(values, totalChunks, chunkIndex) 77 | } else if ( 78 | typeof totalChunks === 'number' && 79 | typeof chunkIndex === 'undefined' 80 | ) { 81 | // take every Nth item 82 | values = values.filter((_, k) => k % totalChunks === 0) 83 | } else if (typeof totalChunks === 'function') { 84 | // filter using the given predicate 85 | values = values.filter(totalChunks) 86 | } 87 | 88 | if (Cypress._.isPlainObject(values)) { 89 | testCallback = titlePattern 90 | if (typeof testCallback !== 'function') { 91 | throw new Error( 92 | 'When using a single test case object, cannot provide title pattern', 93 | ) 94 | } 95 | 96 | const pairs = Cypress._.toPairs(values) 97 | pairs.forEach(function (pair) { 98 | const [title, value] = pair 99 | // define a test for each value 100 | if (Array.isArray(value)) { 101 | it(title, function itArrayCallback() { 102 | return testCallback.apply(this, value) 103 | }) 104 | } else { 105 | it(title, function itCallback() { 106 | return testCallback.call(this, value) 107 | }) 108 | } 109 | }, this) 110 | } else if (Array.isArray(values)) { 111 | values.forEach(function (value, k) { 112 | const title = makeTitle(titlePattern, value, k, values) 113 | if (!title) { 114 | throw new Error( 115 | `Could not compute the test title ${k} for value ${value}`, 116 | ) 117 | } 118 | 119 | // define a test for each value 120 | if (Array.isArray(value)) { 121 | it(title, function itArrayCallback() { 122 | return testCallback.apply(this, value, k) 123 | }) 124 | } else { 125 | it(title, function itCallback() { 126 | return testCallback.call(this, value, k) 127 | }) 128 | } 129 | }, this) 130 | 131 | // returns the number of created tests 132 | return values.length 133 | } else { 134 | console.error(values) 135 | throw new Error( 136 | 'Do not know how to create tests from the values array / object. See DevTools console', 137 | ) 138 | } 139 | } 140 | } 141 | } 142 | 143 | if (!describe.each) { 144 | context.each = describe.each = function (values) { 145 | if (typeof values === 'number') { 146 | // the user wants to repeat the same suite N times 147 | if (values < 1) { 148 | throw new Error('Number of suite repetitions must be >= 1') 149 | } 150 | values = Cypress._.range(0, values) 151 | } 152 | 153 | if (!Array.isArray(values)) { 154 | throw new Error('cypress-each: values must be an array') 155 | } 156 | 157 | if (typeof totalChunks === 'number' && typeof chunkIndex === 'number') { 158 | // split all items into N chunks and take just a single chunk 159 | values = getChunk(values, totalChunks, chunkIndex) 160 | } 161 | 162 | return function describeEach(titlePattern, testCallback) { 163 | // define a test for each value 164 | values.forEach((value, k) => { 165 | const title = makeTitle(titlePattern, value, k, values) 166 | 167 | if (!title) { 168 | throw new Error( 169 | `Could not compute the suite title ${k} for value ${value}`, 170 | ) 171 | } 172 | 173 | if (Array.isArray(value)) { 174 | // const title = formatTitle(testTitle, ...value) 175 | describe(title, testCallback.bind(null, ...value)) 176 | } else { 177 | // const title = formatTitle(testTitle, value) 178 | describe(title, testCallback.bind(null, value)) 179 | } 180 | }) 181 | 182 | // returns the number of created suites 183 | return values.length 184 | } 185 | } 186 | } 187 | 188 | module.exports = { formatTitle, makeTitle, getChunk } 189 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | "noEmit": true /* Disable emitting files from a compilation. */, 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 74 | 75 | /* Type Checking */ 76 | "strict": true /* Enable all strict type-checking options. */, 77 | // "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | 96 | /* Completeness */ 97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | --------------------------------------------------------------------------------