Repository: bahmutov/cypress-map
Pull request #26 (2023-03-07 15:03:23)

feat: add cy.mapChain command


Patch:
diff --git a/README.md b/README.md
index 472a0ef..2f008a7 100644
--- a/README.md
+++ b/README.md
@@ -310,6 +310,21 @@ cy.wrap(list)
   .should('deep.equal', [6, 5, 7])
 ```
 
+## Misc
+
+### mapChain
+
+I have added another useful command (not a query!) to this package. It allows you to process items in the array subject one by one via synchronous, asynchronous, or `cy` command functions. This is because the common solution to fetch items using `cy.each`, for example does not work:
+
+```js
+// fetch the users from a list of ids
+// 🚨 DOES NOT WORK
+cy.get(ids).each(id => cy.request('/users/' + id)).then(users => ...)
+// Nope, the yielded "users" result is ... still the "ids" subject
+// ✅ CORRECT SOLUTION
+cy.get(ids).mapChain(id => cy.request('/users/' + id)).then(users => ...)
+```
+
 ## Types
 
 This package includes TypeScript command definitions for its custom commands in the file [commands/index.d.ts](./commands/index.d.ts). To use it from your JavaScript specs:
diff --git a/commands/index.d.ts b/commands/index.d.ts
index c474569..745d09f 100644
--- a/commands/index.d.ts
+++ b/commands/index.d.ts
@@ -19,6 +19,21 @@ declare namespace Cypress {
      */
     mapInvoke(propertyName: string, ...args: any[]): Chainable<any>
 
+    /**
+     * A regular cy command that takes every item from the current subject
+     * and calls the given function with the item and its index.
+     * The function could be synchronous, or async. The function could
+     * call other Cypress commands and yield the value. All async and
+     * Cypress commands are queued up and execute one at a time.
+     * The current subject should be an array.
+     * Yields the final list of results.
+     * @see https://github.com/bahmutov/cypress-map
+     * @example
+     *  // fetch the users from a list of ids
+     *  cy.get(ids).mapChain(id => cy.request('/users/' + id)).then(users => ...)
+     */
+    mapChain(fn: Function): Chainable<any>
+
     /**
      * A query command that can log the data without changing it. Useful
      * for debugging longer command chains.
diff --git a/commands/index.js b/commands/index.js
index d78a163..cf30ccf 100644
--- a/commands/index.js
+++ b/commands/index.js
@@ -14,3 +14,4 @@ require('./table')
 require('./invoke-once')
 require('./update')
 require('./partial')
+require('./map-chain')
diff --git a/commands/map-chain.js b/commands/map-chain.js
new file mode 100644
index 0000000..b1372f3
--- /dev/null
+++ b/commands/map-chain.js
@@ -0,0 +1,44 @@
+/// <reference types="cypress" />
+
+Cypress.Commands.add(
+  'mapChain',
+  { prevSubject: 'Array' },
+  (list, fn) => {
+    if (!Array.isArray(list)) {
+      throw new Error('Expected cy.mapChain subject to be an array')
+    }
+    const results = []
+
+    const produceValue = (k) => {
+      const r = fn(list[k], k)
+
+      // if the function returned a Promise
+      // or "cy" chainable
+      if (typeof r === 'object' && 'then' in r) {
+        return r.then((result) => {
+          // console.log(k, list[k], result)
+          results.push(result)
+          if (k >= list.length - 1) {
+            // done
+          } else {
+            return produceValue(k + 1)
+          }
+        })
+      } else {
+        // user function is plain
+        // console.log(k, list[k], r)
+        results.push(r)
+        if (k >= list.length - 1) {
+          // done
+        } else {
+          return produceValue(k + 1)
+        }
+      }
+    }
+
+    // make sure we put the possible promises into the command chain
+    cy.wrap(results, { log: false })
+      .then(() => produceValue(0))
+      .then(() => results)
+  },
+)


Test patch:
diff --git a/cypress/e2e/map-chain.cy.js b/cypress/e2e/map-chain.cy.js
new file mode 100644
index 0000000..a8e7afb
--- /dev/null
+++ b/cypress/e2e/map-chain.cy.js
@@ -0,0 +1,40 @@
+/// <reference types="cypress" />
+// @ts-check
+import '../../commands'
+
+const doubleIt = (n) => n + n
+
+async function asyncDouble(n) {
+  await Cypress.Promise.delay(500)
+  return n + n
+}
+
+it('maps each item using sync function', () => {
+  cy.wrap([1, 2, 3])
+    .mapChain(doubleIt)
+    .should('deep.equal', [2, 4, 6])
+})
+
+it('maps each item using async function', () => {
+  cy.wrap([1, 2, 3])
+    .mapChain(asyncDouble)
+    .should('deep.equal', [2, 4, 6])
+})
+
+it('maps each item to the cy yielded value', () => {
+  cy.wrap([1, 2, 3])
+    .mapChain((x) => cy.wrap(x).then(doubleIt))
+    .should('deep.equal', [2, 4, 6])
+})
+
+it('maps each item using a Promise', () => {
+  cy.wrap([1, 2, 3])
+    .mapChain((x) => {
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          resolve(doubleIt(x))
+        }, 500)
+      })
+    })
+    .should('deep.equal', [2, 4, 6])
+})