Search code examples
javascripttestingcypress

Storing a variable for later use in a Cypress test without affecting the current chainable element


We're testing the forgotten password email flow for our application. It goes something like this:

.getLastEmailHtml()
.then((html) => {
  cy.document().invoke('write', html)
})
.contains('Reset password') // <- this is a button/anchor
// Cypress doesn't support multiple tabs, so let's remove the target="_blank" on the link before clicking
.invoke('removeAttr', 'target')
.click()

/* do stuff on the new password page... */

Here we're grabbing html of the email, writing it into a Cypress document and then clicking the link which takes us to a url with a token in the query for security. Once the new password has been set by the test, I'd like to check that the perviously visited url (the one containing the token) now shows that the token has expired after re-visiting it.

What's the cleanest way I can store the original url for use in a visit later. I'd preferably want something where I didn't have to break the current chainable, like:

.getLastEmailHtml()
.then((html) => {
  cy.document().invoke('write', html)
})
.contains('Reset password') // <- this is a button/anchor
// Cypress doesn't support multiple tabs, so let's remove the target="_blank" on the link before clicking
.invoke('removeAttr', 'target')
.invoke('attr', 'href').then(url => /* save the url for later use */)
.click()

/* do stuff on the new password page and then check token is expired... */

Of course the above breaks our chain and click() no longer works:

! CypressError

cy.click() failed because it requires a DOM element.


Solution

  • The subject is changing in your chain, it's no longer the anchor at the final click.

    const html = `
      <html>
        <body>
          <a href="https://example.com" target="_blank">Reset password</a>
        </body>
      <html>
    `
    cy.document().invoke('write', html)
    
    cy.contains('Reset password')
      .invoke('removeAttr', 'target')
      .should(subject => {
        expect(typeof subject).to.eq('object')
        const element = subject[0]
        expect(element.nodeName).to.eq('A')           // subject is <a>
      })
      .invoke('attr', 'href')
      .should(subject => {
        expect(typeof subject).to.eq('string')        // subject is href
      })
    

    Move .invoke('attr', 'href') inside the .then(), making sure to set the subject back to the anchor element.

    This depends on how you are doing /* save the url for later use */.

    For example:

    cy.contains('Reset password')
      .invoke('removeAttr', 'target')
      .should(subject => {
        expect(typeof subject).to.eq('object')
        const element = subject[0]
        expect(element.nodeName).to.eq('A')
      })
      .then($anchor => {
        cy.wrap($anchor)
          .invoke('attr', 'href')
          .as('href')
        cy.wrap($anchor)                   // set subject back to <a>
      })
      .should(subject => {
        expect(typeof subject).to.eq('object')
        const element = subject[0]
        expect(element.nodeName).to.eq('A')
      })
    
    cy.get('@href').should('eq', 'https://example.com')
    

    enter image description here

    Be careful about origin change if href is not localhost, because no explicit origin is set by cy.document().invoke('write', html).

    cy.origin() may be needed for further testing on the linked page:

    cy.get('@href').should('eq', 'https://example.com')
      .then(href => {
        cy.origin(href, () => {
          cy.get('h1').should('have.text', 'Example Domain')
        })
      })