Day 3

Welcome to Day 3, y'all!

As usual, if you're new to the Cypress Weekly Workshop, check out Day 0 to get started.

The sessions going forward might be shorter, but the concepts – like dealing with network requests – are important, so you should take the extra time to understand the concepts.

In the last session we learned about the advantages of not hardcoding your test data by defining a data object. Applying this principle to the Sign In test is straightforward:

describe('sign in', () => {
  const data = {
    username: 'Allie2',
    password: 's3cret'
  }
  it('sign in with username and password', () => {
    cy.visit('/signin')
    cy.get('#username')
      .type(data.username)
    cy.contains('button', 'Sign In')
      .should('be.disabled')
      .and('have.class', 'Mui-disabled')
    cy.get('#password')
      .type(data.password)
    ...
  })
})

You were also tasked with replacing cy.url with cy.location. The real objective was to simplify our assertion by eliminating the need for a baseUrl. For example, after signing in:

cy.url()
  .should('equal', baseUrl + '/')

According to the Cypress docs, cy.location yields just / when passed pathname, so we can replace the above with this:

cy.location('pathname')
  .should('equal', '/')

On the Sign In page, a user can click a link below the Sign In button to Sign Up. How should we assert that the link is there and takes the user to the /signup page?

One way is to click on the link and assert the path like this:

cy.contains('a', 'Don\'t have an account? Sign Up')
  .click()
cy.location('pathname')
  .should('equal', '/')

There are a couple of drawbacks to this approach. Firstly, we're navigating away from the page so we would have to navigate back. Secondly, we're mostly testing that clicking a link goes to the page specified by the link's href attribute. There's a better way to test that by simply asserting the link has the correct href value:

  cy.contains('a', 'Don\'t have an account? Sign Up')
    .should('have.attr', 'href', '/signup')

This is adequate because we can make a safe assumption that the browser will work as intended when a user clicks a link.

The remaining Take-home Challenges were around network requests. Previously we learned how to declare aliased routes and pass each alias to a cy.wait() to ensure that the request is made and a response is received before executing the next command.

Declaring and aliasing routes in the Sign In test is the same (sans createUser):

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')

Waiting for the routes at the end of the test is exactly the same:

cy.wait('@login')
cy.location('pathname')
  .should('equal', '/')
cy.wait([
    '@getBankAccounts',
    '@getPublicTransactions',
    '@getNotifications'
])

OK, I lied. See the difference? When waiting on rapid-fire XHRs like those last three, you can pass an array of aliases to cy.wait().

You might be wondering why we should bother waiting for those last three requests since we're not performing any actions after. While this is true, by waiting on a request, we're implicitly asserting that our app makes the request. So if our app fails to make the request, we might never know if we didn't wait on it.


Before we move on, let's get the latest and greatest:

$ git checkout Day-3
$ npm install
$ npm start

When the user clicks Sign In button, our Real World App makes the request POST /login. Typically a POST includes some payload, right? We could assume that it includes the username and password, but let's inspect that request:

[image]

Clicking on an XHR in the Command Log will output details in the Console which includes Request containing body:

{
  username: "Allie2",
  password: "s3cret",
  remember: true,
  type: "LOGIN"
}

How do we assert this? First we need to ask, what does cy.wait yield?

There are a few places you can find the answer: the Cypress docs, your code editor (if it's configured) or the console output of cy.wait() in the runner. By selecting the command in the Command Log we see that it yields an object containing request.body:

[image]

We learned previously that should is used to make an assertion on its subject - what's yielded by the previous command - by passing in a chainers like this:

cy.get('checkbox')
  .should('be.enabled')

So in the same fashion we can chain should, but only after grabbing request.body with its like so:

cy.wait('@login')
  .its('request.body')
  .should('deep.equal', {
    username: data.username,
    password: data.password,
    remember: true,
    type: 'LOGIN'
  })

The object yielded by cy.wait() also contains response.body, so we could make an assertion on that if we wanted to.

Our Sign In test is now functionally complete (yay!), but some of you might agree that the setup code (for the aliased routes) looks out of place in the it block. I firmly believe that the it block should only contain commands for executing the test. Using the mocha pre-hook, before, we can improve the structure. Beauty is in the eyes of the beholder, but I think our test looks even better:

describe('sign in', () => {
  const data = {
    username: 'Allie2',
    password: 's3cret'
  }
  before('setup routes', () => {
    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')
  })
  it('sign in with username and password', () => {
    cy.visit('/signin')
    cy.get('#username')
      .type(data.username)
    cy.contains('button', 'Sign In')
      .should('be.disabled')
      .and('have.class', 'Mui-disabled')
    cy.get('#password')
      .type(data.password)
    cy.get('[data-test="signin-remember-me"]')
      .should('not.have.class', 'Mui-checked')
    cy.get('[data-test="signin-remember-me"] input')
      .should('not.be.checked')
      .check()
    cy.get('[data-test="signin-remember-me"]')
      .should('have.class', 'Mui-checked')
    cy.contains('a', 'Don\'t have an account? Sign Up')
      .should('have.attr', 'href', '/signup')
    cy.contains('button', 'Sign In')
      .should('not.be.disabled')
      .and('not.have.class', 'Mui-disabled')
      .click()
    cy.wait('@login')
      .its('request.body')
      .should('deep.equal', {
        username: data.username,
        password: data.password,
        remember: true,
        type: 'LOGIN'
      })
    cy.location('pathname')
      .should('equal', '/')
    cy.wait([
      '@getBankAccounts',
      '@getPublicTransactions',
      '@getNotifications'
    ])
  })
})

To finish our Sign Up test, let's complete the Get Started with Real World App wizard.

We'll use our existing data object for the three field values:

const data = {
  firstName: 'Mike',
  lastName: 'C',
  username: Date.now(),
  password: 'password',
  baseUrl: Cypress.config('baseUrl'),
  bankName: 'Farrs Wellgo',
  routingNumber: '123456789',
  accountNumber: '123456789'
}

There's also a couple of XHRs we need to wait for, one of which isn't aliased yet.

cy.route('POST', '/bankAccounts')
  .as('createBankAccount')

To complete the wizard, the user clicks Next, saves bank information (which triggers two requests to the /bankAccounts endpoint) and clicks Done (which closes the wizard dialog).

cy.contains('button', 'Next')
  .click()
cy.get('#bankaccount-bankName-input')
  .type(data.bankName)
cy.get('#bankaccount-routingNumber-input')
  .type(data.routingNumber)
cy.get('#bankaccount-accountNumber-input')
  .type(data.accountNumber)
cy.contains('button', 'Save')
  .click()
cy.wait([
  '@createBankAccount',
  '@getBankAccounts'
])
cy.contains('button', 'Done')
  .click()
cy.get('[role=dialog]')
  .should('not.exist')

[image]

Now that Sign In and Sign Up are tested, we can test Home, which is at the index route (/).

We'll start simple by testing Logout, by visiting / and clicking on the Logout button. Since we're testing a different feature/route, we should create a new spec, home.spec.js. The top-level describe will contain an it block for testing Logout:

describe('home', () => {
  it('log out', () => {
    cy.visit('/')
    cy.contains('Logout')
      .click()
  })
})

After creating home.spec.js, we'll open the runner (npx cypress open and select home.spec.js to execute it. Here's the result:

[image]

So rather than navigating to Home, we were navigated to /signin. Why?

For every new it block, all browser data – like the authentication cookie set by logging in – is cleared. By design, RWA redirects unauthenticated users to /signin. So what do we do?

We could go through the same process as the other specs. We could even put the code in a before hook to keep it clean:

describe('home', () => {
  const data = {
    username: 'Allie2',
    password: 's3cret'
  }
  before('setup routes', () => {
    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')
  })
  before('sign in', () => {
    cy.visit('/signin')
    cy.get('#username')
      .type(data.username)
    cy.get('#password')
      .type(data.password)
    cy.contains('button', 'Sign In')
      .click()
    cy.wait('@login')
    cy.location('pathname')
      .should('equal', '/')
    cy.wait([
      '@getBankAccounts',
      '@getPublicTransactions',
      '@getNotifications'
    ])
  })
  it('log out', () => {
    cy.contains('Logout')
      .click()
  })
})

[image]

This is fine, but in general we want to avoid interacting with the DOM as much as possible because it's slower and more prone to failure than the alternatives.

Take-home Challenges

  • Assert the response body from the POST /login request
    The body should contain the expected username
  • Assert components on the Home page work as expected
    At minimum, test that the left nav components navigate correctly, defining all routes and waiting on them.

GO TOP

🎉 You've successfully subscribed to iheartjs!
OK