Search code examples
javascriptcypresse2e-testing

Cypress waiting for a spy


I'm writing Cypress tests and our app has a bunch of forms and they all follow a specific pattern of, first, local validation, then on submit, server validation. If there's a server error it calls setCustomValidity and reportValidity()

The problem I'm running into is I want to write a test that looks like this:

cy.intercept('POST', '/api**').as('apiRequest')

// ...test code to fill in fields

// Here I'm clearing the field I care about
cy.wrap($input).clear()

// The actual input I care about
cy.get('[name=zip]').type('00000')

cy.get('form [type=submit]').click()

cy.wait('@apiRequest').then((interception) => {
  cy.get('[name=zip]').then(($input) => {
    expect($input[0].validationMessage).to.eq(errorMessage)
  })
})

The problem, however, is that the expect($input[0].validationMessage).to.eq(errorMessage) line is called before the onComplete callback in the actual application code. It looks something like:

function submitHandler() {
   // This is what is intercepted by Cypress
   makeAnApiCall({
    variables: {
      input: {
        // ...
      },
    },
    onCompleted(response) {
      const addressResponse = response.createAddress
      
      // ... the rest of the handling logic. If there are errors...
      // This runs AFTER the cy.intercept callback is run so tests fail
      addressResponse?.messages.forEach((message) => {
        elementArray[message.field]?.setCustomValidity(message.help)
        elementArray[message.field]?.reportValidity()
      })
    },
    onError(error) {
      // ...
    },
  })
}

Above you can see the report call is calling after the API call is finished. This makes sense in that Cypress is probably wrapping it to spy on it and the others waiting for it to finish are called after the Spy callback.

My only idea was to listen for the spy to complete like:

cy.spy($input[0], 'reportValidity').as('validitySpy')
cy.wait('@validitySpy').then(() => {/* check validation here */})

But that doesnt work or isnt supported. Am I doing this wrong in general or how should I go about this? It's preferable to not inject data attributes into the DOM to track the status of API requests.


Solution

  • You probably just need to change the .then() to a .should(). This will give you retry on the expectation, and enough time for the onCompleted handler to fire.

    cy.wait('@apiRequest').then((interception) => {
      cy.get('[name=zip]').should(($input) => {
        expect($input[0].validationMessage).to.eq(errorMessage)
      })
    })
    

    You also don't need to do the assertion inside the callback, the intercept wait command will wait for the intercept.

    cy.wait('@apiRequest')
    
    cy.get('[name=zip]').should(($input) => {
      expect($input[0].validationMessage).to.eq(errorMessage)
    })
    

    One more suggestion (but it's not really an improvement), you can use a fluid syntax

    cy.wait('@apiRequest')
    
    cy.get('[name=zip]')
      .its('validationMessage')
      .should('eq', errorMessage)