Search code examples
cypresscypress-intercept

Test that an API call does NOT happen in Cypress


I've implemented API data caching in my app so that if data is already present it is not re-fetched.

I can intercept the initial fetch

cy.intercept('**/api/things').as('api');
cy.visit('/things')                      
cy.wait('@api')                         // passes

To test the cache is working I'd like to explicitly test the opposite.

How can I modify the cy.wait() behavior similar to the way .should('not.exist') modifies cy.get() to allow the negative logic to pass?

// data is cached from first route, how do I assert no call occurs?
cy.visit('/things2')                      
cy.wait('@api')                    
  .should('not.have.been.called')   // fails with "no calls were made"

Minimal reproducible example

<body>
  <script>
    setTimeout(() => 
      fetch('https://jsonplaceholder.typicode.com/todos/1')
    }, 300)
  </script>
</body>

Since we test a negative, it's useful to first make the test fail. Serve the above HTML and use it to confirm the test fails, then remove the fetch() and the test should pass.


Solution

  • The add-on package cypress-if can change default command behavior.

    cy.get(selector)
      .if('exist').log('exists')
      .else().log('does.not.exist')
    

    Assume your API calls are made within 1 second of the action that would trigger them - the cy.visit().

    cy.visit('/things2')
    cy.wait('@alias', {timeout:1100})
      .if(result => {
        expect(result.name).to.eq('CypressError')    // confirm error was thrown
      })  
    

    You will need to overwrite the cy.wait() command to check for chained .if() command.

    Cypress.Commands.overwrite('wait', (waitFn, subject, selector, options) => {
    
      // Standard behavior for numeric waits
      if (typeof selector === 'number') {
        return waitFn(subject, selector, options)
      }
    
      // Modified alias wait with following if()
      if (cy.state('current').attributes.next?.attributes.name === 'if') {
        return waitFn(subject, selector, options).then((pass) => pass, (fail) => fail)
      }
    
      // Standard alias wait
      return waitFn(subject, selector, options)
    })
    

    As yet only cy.get() and cy.contains() are overwritten by default.


    Custom Command for same logic

    If the if() syntax doesn't feel right, the same logic can be used in a custom command

    Cypress.Commands.add('maybeWaitAlias', (selector, options) => {
      const waitFn = Cypress.Commands._commands.wait.fn
    
      // waitFn returns a Promise
      // which Cypress resolves to the `pass` or `fail` values
      // depending on which callback is invoked
    
      return waitFn(cy.currentSubject(), selector, options)
        .then((pass) => pass, (fail) => fail)
    
      // by returning the `pass` or `fail` value
      // we are stopping the "normal" test failure mechanism
      // and allowing downstream commands to deal with the outcome
    })
    
    cy.visit('/things2')
    cy.maybeWaitAlias('@alias', {timeout:1000})
      .should(result => {
        expect(result.name).to.eq('CypressError')    // confirm error was thrown
      })