Day 6

Welcome to the 7th and final day of the workshop! As always, feel free to check out Day 0 if this is your first time.

Last week, you were challenged with adding a cy.setupRoutes command and use it in the spec for My Account and add it to a global beforeEach just like we did with cy.signIn. Lastly, you were supposed to update all the other tests to use global hooks for signing in and setting up routes.

At this point we already have the function, setupRoutes at the bottom of our my-account.spec.js. To make it a command, we need to add it or import it in support/commands.js.

Cypress.Commands.add('setupRoutes', () => {
  cy.server()
  cy.route('POST', '/login')
    .as('login')
  cy.route('GET', '/bankAccounts')
    .as('getBankAccounts')
  cy.route('GET', '/transactions/public')
    .as('getPublicTransactions')
  cy.route('GET', '/notifications')
    .as('getNotifications')
  cy.route('POST', '/logout')
    .as('logout')
  cy.route('PATCH', '/users/*')
    .as('updateUser')
  cy.route('GET', '/checkAuth')
    .as('checkAuth')
})

Now that it's a command, we can remove the function from our spec and call it with cy.setupRoutes(), but you were challenged with setting up routes for all specs with a global hook. In support/index.js, there's already a beforeEach for signing in, so now we just have to add another one like so:

beforeEach('set up routes', () => { 
  cy.setupRoutes()
})

The code for signing in and setting up routes consumes over 50 lines. For some perspective, my-account.spec.js is about 25 lines now. So more than twice the number of lines has been abstracted from our spec, making it much easier to reason about.


In home.spec.js, we have two beforeEachs that are already being called globally. We can easily remove the one for setting up routes, but the one for signing in includes a visit to Home, we're going to modify that one:

  beforeEach('navigate to Home', () => {
    cy.visit('/')
  })

As for the data object, we no longer need password, but username, which is now defined in cypress.json, needs to be updated accordingly:

  const data = {
    username: Cypress.env('username'),
    fullName: 'Kaylin H'
  }

That's fine, but as I've said before - we can do better. What if username changes (via configuration)? Will the test break because fullName changed, too? More on this later.


In both sign-in.spec.js and sign-up.spec.js, we can remove the before hooks for setting up routes, but with or without the change, these specs fail. Let's investigate.

While authenticated, navigate to /signin and /signup. You'll notice that the app redirects you to the Home page because you've already signed in or signed up. What do we do? We'll likely have more tests that require authentication than don't, so removing the global hook is not ideal. Here are our options:

1. Modify the hook so that sign in doesn't apply to these two specs
2. Log out of our app in these two specs

Let's explore the first option.

In our beforeEach, we can wrap the call to cy.signIn in a conditional like this:

beforeEach('sign in', () => {
  if (isSuiteNotExcluded) {
    cy.signIn(Cypress.env('username'), Cypress.env('password'))
  }
})

We're working backwards here, starting with an ideal solution. Now we need to define isSuiteNotExcluded.

beforeEach('sign in', () => {
  const excludedSpecs = ['sign-in', 'sign-up']
  const isSuiteNotExcluded = !excludedSpecs.includes(currentSpecName)
  if (isSuiteNotExcluded) {
    cy.signIn(Cypress.env('username'), Cypress.env('password'))
  }
})

Here, we're taking an array of excluded specs and checking if it doesn't include currentSpecName. We now need to figure out how to get the current spec name.

Run any test and open the browser console. Type Cypress and press Enter. In this global is lots of goodies, much of which can be accessed synchronously. Now find the spec object. One of the properties is name. Perfect!

We'll need to trim off the .spec.js so let's write a utility function for that and use it in getCurrentSpecName:

function getCurrentSpecName(Cypress) {
  const fileName = Cypress.spec.name
  return trimFileExtension(fileName)
}

function trimFileExtension(fileName) {
  return fileName
    .split('.')
    .shift()
}

Now currentSpecName is simply getCurrentSpecName(Cypress).

While Cypress is a global that we don't have to pass in, I think it's a good habit to pass in everything we need for a function (a form of dependency injection).

Finally, here's our updated beforeEach:

beforeEach('sign in', () => {
  const currentSpecName = getCurrentSpecName(Cypress)
  const excludedSpecs = ['sign-in', 'sign-up']
  const isSuiteNotExcluded = !excludedSpecs.includes(currentSpecName)
  if (isSuiteNotExcluded) {
    cy.signIn(Cypress.env('username'), Cypress.env('password'))
  }
})

If you run all the specs now you'll see that only the excluded ones bypass cy.signIn() and the visit no longer redirects to Home.

The second option is to log out, but I'll let you explore that one on your own!


In home.spec.js we're checking that the user's first name and last initial are displayed in the side nav:

      cy.get('[data-test=sidenav-user-full-name]')
        .should('be.visible')
        .and('have.text', data.fullName)

While the text we're asserting is not hard-coded in the test, it's still dependent on the username environment config, which could change and cause the test to fail. So how do we get fullName in any case?

Again we have a couple of options:

1. Get it from the API at the test level
2. Get it from the API at the plugin level

In either case, we need to know where we can get the full name. Head over to the auth support module and set log back to true and run a test. If you click on the request in the command log and inspect the response body, you'll see it contains firstName and lastName, but not the last initial. We can work with this! Let's just create a function to get what we need:

function getFullName(firstName, lastName) {
  return `${firstName} ${lastName.charAt(0)}`
}

But how do we get the response from a request in a hook all the way to the test? First, we'll need to store it with an alias. Since the signIn command returns the user from the response body, just chain .as('user') to cy.signIn() in your hook.

cy.signIn(Cypress.env('username'), Cypress.env('password'))
  .as('user')

Now we need to read the user alias, pass the first and last name into getFullName and assign that to data.fullName

There are two methods to get the alias - cy.get('@user') or this.user.

it('name, handle and avatar are visible', () => {
  cy.get('@user')
    .then(user => {
      const data = {
        username: Cypress.env('username'),
        fullName: getFullName(user.firstName, user.lastName)
      }
  cy.get('[data-test=sidenav-user-full-name]')
    .should('be.visible')
    .and('have.text', data.fullName)
  cy.get('[data-test=sidenav-username]')
    .should('be.visible')
    .and('contain.text', data.username)
  })
})
it('name, handle and avatar are visible', function() {
  const data = {
    username: Cypress.env('username'),
    fullName: getFullName(this.user.firstName, this.user.lastName)
  }
  cy.get('[data-test=sidenav-user-full-name]')
    .should('be.visible')
    .and('have.text', data.fullName)
  cy.get('[data-test=sidenav-username]')
    .should('be.visible')
    .and('contain.text', data.username)
 })

Take note of a couple of important details. First, reading an alias is restricted to tests, so we needed to move the data object to the test block. And second, the value of this is not defined in an arrow function, so we had to revert to pre-ES6 syntax.

GO TOP

🎉 You've successfully subscribed to iheartjs!
OK