Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to send a trigger inside .then() to exit an outer loop prematurely? #2710

Closed
ldiary opened this issue Nov 2, 2018 · 6 comments
Closed

Comments

@ldiary
Copy link

ldiary commented Nov 2, 2018

I tried to ask in gitter first but didn't get any suggestions. I realized I may not get any advise in gitter though, since this is somehow related to #1417 . I could have solved this problem in minutes if only Cypress have async/await, however as I am banging my head now for days, I wonder if someone could suggest me a workaround.

Here's what I want to achieve - scan an API endpoint using pagination and stop (or exit the loop) once I get a product with more than 2 images.

    let hasMultipleImages = false
    const getProducts = (page) => {
      cy.request({
        url: `https://myapi.com/1/products?page=${page}`,
        method: "GET",
        timeout: 70000,
        failOnStatusCode: false
      }).then(response => {
        response.body.response.forEach(p => {
          if (p.images.length > 2){
            cy.log(p.images)
            hasMultipleImages = true
          }
        })
      })
    }
    for (let i = 1; i < 49; i++) {
      getProducts(i)
      if (hasMultipleImages) break
    }

I want to exit the loop when hasMultipleImages is set to true. However, although .then() can access the hasMultipleImages, whatever value is set by .then() gets ignored and the if(hasMultipleImages) break never gets executed because inside the if statement hasMultipleImages always evaluate to the initial value false.

I tried a lot of backflipping codes including this one, but nothing really worked.

    const getProducts = (page = 1) => {
       cy.request({
        url: `https://myapi.com/1/products?page=${page}`,
        method: "GET",
        timeout: 70000,
        failOnStatusCode: false
      }).then(response => {
        let hasImage = false
         return new Cypress.Promise((resolve, reject) => {
           response.body.response.forEach(p => {
             cy.log(p.images.length)
             if (p.images.length > 2 || i > 48) {
               cy.log("Found: ", p)
               hasImage = true
             }
           })
           resolve(hasImage)
         })
       })
         .then(found => {
           if (! found) {
             page = page + 1
             return getProducts(page)
           }
         })
    }
    getProducts()

Anyone has any suggestion for a workaround, please?

@Herteby
Copy link

Herteby commented Nov 2, 2018

A quick and dirty fix is to write your code using async/await, then paste it into https://babeljs.io/en/repl to convert it (maybe keep your original code as a comment in the file).

@jennifer-shehane
Copy link
Member

@ldiary I'm not totally sure what you want to do exactly when the 2 images is not found and I'd like to see the code above in the context of an it block - where the test comes into play.

Here is an example to making the test fail when the conditions of the request are not met in a loop of 49 pages. I like to reproduce the examples locally, so I've used a public API endpoint instead of your exact example. Is this helpful?

  cy.wrap([...Array(49).keys()]).each((page) => {
    cy.request({
      url: `https://jsonplaceholder.typicode.com/comments/${page + 1}`,
      method: 'GET',
    }).then((response) => {
      if (response.body.id === 10) {
        cy.log(10)
        throw new Error('not enough images here')
      }
    })
  })
})

@ldiary
Copy link
Author

ldiary commented Nov 2, 2018

@Herteby I was thinking if only async/await can be used with cy.request()

@jennifer-shehane thanks for response.
I have two APIs, a monolith REST and another a GraphQL. I am trying to test the GraphQL endpoint which will eventually replace the REST.

After reading your example, I found out that each can exit the loop prematurely - which is exactly what I want. However, now Cypress is complaining if I try to use return false in order to exit the loop.

  it("Should provide products with multiple images", () => {
    cy.wrap([...Array(49).keys()]).each((page) => {
      return cy.request({
        url: `https://myapi.com/1/products?page=${page + 1}`,
        method: 'GET',
        timeout: 70000,
        failOnStatusCode: false
      }).then((response) => {
        let hasMulitpleImages = false
        response.body.response.forEach(product => {
          if (product.images.length > 2) {
            // We found a product we can use for testing
            // Use this product info to query another API endpoint (which is actually GraphQL)
            // Assert that the second API endpoint returned correct values
            hasMulitpleImages = true
          }
        })
        if (hasMulitpleImages) return false // test completed successfully, let's exit the loop
        if (page === 48 && hasMulitpleImages === false) {
          // We never found a usable product, fail the test here
        }
      })
    })
  })

Cypress is giving me this error:

CypressError: cy.then() failed because you are mixing up async and sync code.

In your callback function you invoked 1 or more cy commands but then returned a synchronous value.

Cypress commands are asynchronous and it doesn't make sense to queue cy commands and yet return a synchronous value.

You likely forgot to properly chain the cy commands using another cy.then().

The value you synchronously returned was: 'false'

@ldiary
Copy link
Author

ldiary commented Nov 20, 2018

I finally found a workaround by re-structuring the test and using before and alias.

describe("Issue 2710: Workaround on exiting outer loop prematurely", () => {

  before("Preparation", () => {
    const findItemWithMultipleImages = (page) => {
      cy.request({
        url: `https://myapi.com/1/products?page=${page}`,
        method: "GET",
        timeout: 70000,
        failOnStatusCode: false
      }).then(response => {
        const found = response.body.response.find(p => {
          if (p.images.length > 2){
            cy.wrap(p).as('foundItem')
            return true
          }
          return false
        })
        if (!found && page < 49) {
          findItemWithMultipleImages(page + 1)
        }
      })
    }
    cy.wrap(findItemWithMultipleImages(1)).as("Find an item with multiple images.")
  })

  it.only("Test for item with multiple images.", () => {
    cy.get('@foundItem').then(item => {
      // Start testing here
      cy.log(item)
    })
  })

})

@arnonax
Copy link

arnonax commented Sep 4, 2019

AFAIU, @ldiary found a workaround that works for his case by moving some of code into the before function. But I have a similar issue (trying to return false from inside each which uses cy commands, and get the error cy.then() failed because you are mixing up async and sync code), but I don't think that this workaround can work for me.
IMHO the bug still holds and should be reopened, or you should provide a more general workaround.

@talayellet
Copy link

I finally found a workaround by re-structuring the test and using before and alias.

describe("Issue 2710: Workaround on exiting outer loop prematurely", () => {

  before("Preparation", () => {
    const findItemWithMultipleImages = (page) => {
      cy.request({
        url: `https://myapi.com/1/products?page=${page}`,
        method: "GET",
        timeout: 70000,
        failOnStatusCode: false
      }).then(response => {
        const found = response.body.response.find(p => {
          if (p.images.length > 2){
            cy.wrap(p).as('foundItem')
            return true
          }
          return false
        })
        if (!found && page < 49) {
          findItemWithMultipleImages(page + 1)
        }
      })
    }
    cy.wrap(findItemWithMultipleImages(1)).as("Find an item with multiple images.")
  })

  it.only("Test for item with multiple images.", () => {
    cy.get('@foundItem').then(item => {
      // Start testing here
      cy.log(item)
    })
  })

})

THANK U SOOOO MUCH for this, I was struggling for days with the same issue till I found this reference!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants