4

So I think this is probably me mixing up sync/async code (Mainly because Cypress has told me so) but I have a function within a page object within Cypress that is searching for customer data. I need to use this data later on in my test case to confirm the values.

Here is my function:

searchCustomer(searchText: string) {
  this.customerInput.type(searchText)
  this.searchButton.click()
  cy.wait('@{AliasedCustomerRequest}').then(intercept => {
    const data = intercept.response.body.data
    console.log('Response Data: \n')
    console.log(data)
    if (data.length > 0) {
      {Click some drop downdowns }
      return data < ----I think here is the problem
    } else {
      {Do other stuff }
    }
  })
}

and in my test case itself:

let customerData = searchAndSelectCustomerIfExist('Joe Schmoe')
//Do some stuff with customerData (Probably fill in some form fields and confirm values)

So You can see what I am trying to do, if we search and find a customer I need to store that data for my test case (so I can then run some cy.validate commands and check if the values exist/etc....)

Cypress basically told me I was wrong via the error message:

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().

So obviously I am mixing up async/sync code. But since the return was within the .then() I was thinking this would work. But I assume in my test case that doesn't work since the commands run synchronously I assume?

Fody
  • 23,754
  • 3
  • 20
  • 37
msmith1114
  • 2,717
  • 3
  • 33
  • 84
  • Can you add the cypress commands in your `if` statement? I think your initial assumption is right, in which case we'll just have to add that `return` in a `.then()`, and return the entire command chain – agoff Oct 26 '22 at 15:45
  • This isn't a cypress command but simply just a function within a page object. – msmith1114 Oct 26 '22 at 15:57
  • Do those functions in the page object use cypress commands? – agoff Oct 26 '22 at 17:03
  • They do. The function I posted above is within the page object @agoff – msmith1114 Oct 27 '22 at 16:20

2 Answers2

3

Since you have Cypress commands inside the function, you need to return the chain and use .then() on the returned value.

Also you need to return something from the else branch that's not going to break the code that uses the method, e.g an empty array.

searchCustomer(searchText: string): Chainable<any[]> {

  this.customerInput.type(searchText)
  this.searchButton.click()

  return cy.wait('@{AliasedCustomerRequest}').then(intercept => {

    const data = intercept.response.body.data
    console.log('Response Data: \n')
    console.log(data)
    if (data.length) {
      {Click some drop downdowns }
      return data                              
    } else {
      {Do other stuff }
      return []
    }

  })
}

// using 
searchCustomer('my-customer').then((data: any[]) => {
  if (data.length) {

  }
})

Finally "Click some drop downdowns" is asynchronous code, and you may get headaches calling that inside the search.

It would be better to do those actions after the result is passed back. That also makes your code cleaner (easier to understand) since searchCustomer() does only that, has no side effects.

Fody
  • 23,754
  • 3
  • 20
  • 37
  • Im a bit confused on your last statement. Do you have an example? IE: Are you saying I should do the dropdown clicking inside my test case (and outside of the function). I think im not used to returning a function either (That is a closure right?). Im still rusty on JS – msmith1114 Oct 27 '22 at 16:23
  • Basically, yes. I was thinking of the [Single Responsibility Principle](https://stackify.com/solid-design-principles/). What exactly is a Single Responsibility is open to interpretation, you may decide clicking dropdowns is actually part of `searchCustomer`. On a practical side, if you separate the querying part (getting the data) from the action part (clicking the dropdown) it's easier to test your test code because `searchCustomer` has no side effects, it just searches and returns the data. – Fody Oct 27 '22 at 18:32
  • The part about returning a function - it's asynchronous code so as an analogy it's like connecting up pipes. The data isn't flowing yet when you return from `searchCustomer`, so you return the end of the "pipe" and in the test connect it to the code inside the `.then()`. – Fody Oct 27 '22 at 18:40
  • Would I still need the `return data` within? And the returning a function thing works because it's a "promise" like function right? (Within Cypress I mean) – msmith1114 Oct 28 '22 at 13:37
  • Yes you always need to return something, either `return data` or `return []` in the else part if data is undefined. And yes, returning the `cy.wait()` command is promise-like - so you are returning an object that "promises" to give you a value in the future. – Fody Oct 28 '22 at 20:24
  • This worked, fwiw I had to still `cy.wrap(data)` since it was complaining again about mixing async and sync values :shrug: – msmith1114 Oct 31 '22 at 00:22
1

you just need to add return before the cy.wait

here's a bare-bones example

it("test", () => {
  function searchCustomer() {
    return cy.wait(100).then(intercept => {
      const data = {text: "my data"}
      return data
    })
  }

  const myCustomer = searchCustomer()
  myCustomer.should("have.key", "text")
  myCustomer.its("text").should("eq", "my data")
});
Daniel
  • 34,125
  • 17
  • 102
  • 150
  • This is actually what I tried originally but I still received an undefined value when trying to assign the return value of the function. – msmith1114 Oct 27 '22 at 16:22
  • I didn't notice that the wait didn't have a return. The `cy.wrap` is not needed, just the return. Tested with the example and I think it does what you're looking for. – Daniel Oct 27 '22 at 17:38
  • So do we need the return within the cy.wait as well? I guess im confused since we have a return on the function and within the cy.wait as wel – msmith1114 Oct 28 '22 at 13:07
  • Also do you mind explaining why/what the return before cy.wait does/is? Im not used to seeing it. – msmith1114 Oct 28 '22 at 13:40
  • 1
    you have two (nested) functions and both need to return something for you to be able to use a the returned value. `cy.wait` is asynchronous and will allow you to chain with `then` or `should` etc... Then you need to return the actual data, which the other return handles. To understand better why the `return cy.wait` is needed you can look into how promises and async/await works which is analogous. cypress uses a `Chainer` which is like a Promise – Daniel Oct 28 '22 at 15:26
  • So if Im understanding correctly. With `cy.wait` aliased im waiting for a request to essentially resolve. So with the first return im returning the actual "promise" (or cy.wait being a promise-ish type thing) to something else (in this case `myCustomer` variable) and then within than return object/promise once it's resolved im returning the actual data to the object (with the inner `return`)? Is that sort of correct? – msmith1114 Oct 28 '22 at 15:44
  • that sounds about right. If you didn't return anything from the `searchCustomer` function you can't operate on it (returns undefined). and if you don't return a chainable you can't use the assertions on it, but either way cypress sees this as returning a synchronous value and throws the error you are seeing: _In your callback function you invoked 1 or more cy commands but then returned a synchronous value._ – Daniel Oct 28 '22 at 18:31
  • Sorry last question. Why don't we need to return `cy.wrap` with the data? I thought that was needed when returning cypress values from functions/cypress commands (custom ones) – msmith1114 Oct 31 '22 at 00:14