A common mistake when using cy.session() (and how to fix it)

February 8, 2023

By Guest

This is a guest post from Ambassador Filip Hric!

Hello, I’m Filip. I teach testers about web development and developers about testing. I have spoken at conferences around the world, created multiple online courses, and host live workshops on e2e testing in Cypress.io. Find everything I do at https://filiphric.com.

In Cypress v12, the cy.session() command was released as generally available. Many teams have already added it to their projects to save minutes from their test runs.

I recently wrote a blogpost on how cy.session() can be used instead of abstracting login logic into a page object. As community members on my Discord implement cy.session() into their projects, I often see one problem pop up repeatedly. Let me give you an example.

Consider this login flow:

cy.visit('/login')

cy.get('[data-cy="login-email"]')
   .type('[email protected]')

cy.get('[data-cy="login-password"]')
   .type('Asdf.1234#{enter}', { log: false })

By applying the cy.session() API, we can cache the cookies, localStorage and sessionStorage state of our browser for this login flow like this:

cy.session('login', () => {

   cy.visit('/login')

   cy.get('[data-cy="login-email"]')
      .type('[email protected]')

   cy.get('[data-cy="login-password"]')
      .type('Asdf.1234#{enter}', { log: false})
})

There is one small problem with this type of login. As soon as Cypress completes the cy.type() function, it caches our browser state. However, our frontend might still be working on the login flow and some of the authentication data we need has not been set.

Let’s say our application login will send an http request to the server to authenticate the user, and then redirect to a home page, before saving cookies into the browser.

Given the example above, our session will not get properly cached because we didn’t not explicitly tell cy.session() authentication flow is still in progress. The easiest way we can fix that is to add a guarding assertion to our session.

cy.session('login', () => {

   cy.visit('/login')

   cy.get('[data-cy="login-email"]')
      .type('[email protected]')

   cy.get('[data-cy="login-password"]')
      .type('Asdf.1234#{enter}', { log: false })

   cy.location('pathname')
      .should('eq', '/')
})

This way we will ensure that Cypress will cache our browser at the right moment. Depending on our application, this might be right after we navigate to a new page or some time later. The exact point will be determined by the application we are testing.

Usually, when making a login action, a server responds with an authorization in the form of a token that gets saved as cookies or some other browser storage. Instead of checking for the location, we can check that storage item. The session might then look something like this:

cy.session('login', () => {

   cy.visit('/login')

   cy.get('[data-cy="login-email"]')
      .type('[email protected]')

   cy.get('[data-cy="login-password"]')
      .type('Asdf.1234#{enter}')

   cy.getCookie('auth_token')
      .should('exist')
})

There is a slight problem with this approach though. The cy.getCookie() command will execute immediately and will not retry. Instead of using cy.getCookie() command we will choose a slightly different approach.

beforeEach(() => {

   cy.session('login', () => {

      cy.visit('/login')

     cy.get('[data-cy="login-email"]')
        .type('[email protected]')

     cy.get('[data-cy="login-password"]')
        .type('Asdf.1234#{enter}')

     cy.document()
        .its('cookie')
        .should('contain', 'auth_token')
   })
});

This way we can ensure that we wait the proper time for the application to store our cookies in the browser. After our cookies are saved, we can safely cache our session and use it in our tests.

The validation of a successful login is important for the cy.session() command to work properly. In fact, when using cy.session() you can add the validation as a separate step. This is what the validate() function is for. Let’s see it in action:

beforeEach(() => {

  cy.session('login', () => {

    cy.visit('/login')

    cy.get('[data-cy="login-email"]')
      .type('[email protected]')

    cy.get('[data-cy="login-password"]')
      .type('Asdf.1234#{enter}')
}, {
    validate() {
      cy.document()
        .its('cookie')
        .should('contain', 'auth_token')
    }
  })
});

In this simple example, cy.session() will work pretty much the same way as before. But there is one upside to this approach. Our validate() function can do more than check the mere presence of the token. Instead, we could check for the validity of the token and make sure it does not expire during, e.g., a longer test run. The validate function can look something like this:

validate() {
      cy.document()
        .its('cookie')
        .should('contain', 'trello_token')
        .then(token => {
          cy.request({
            url: '/api/users',
            headers: {
              authorization: token
            }
          })
        })
    }

If the cy.request() returns an error code, it will mean that our authorization is no longer valid and we need to log in again. The validate() function will take care of this, and re-run our login flow.

This way we still keep login attempts to minimum, while making sure that the authorization does not expire during the test run.

Remember to always ensure that your cy.session() flow contains a “confirming assertion” that will guard the creation of the session to proper time.


About the Ambassador Program

The Cypress Ambassador program supports the top Cypress advocates around the world. Through this program, Ambassadors are offered speaking opportunities, a personalized hub, and visibility within our extensive network.To learn more about these wonderful ambassadors visit our Official Ambassador webpage.