Day 1

We're communicating with machines here so this is not the first day! See Day 0 if you're new here or if that's what you're looking for.

Welcome to Day 1!

To recap Day 0, we learned how to install Cypress, run it for the first time, and write our first test, which visits the Sign In page of our Real World App. Then you were presented the challenge of filling out the username and password fields and clicking on the Sign In button.


How should we go about finding the selectors to query for the inputs? We could start with the Selector Playground (more on that later), but a more common tool is built into Chrome – open DevTools and inspect the DOM for an ideal selector and pass that into cy.get():

describe('login', () => {
  it('login with username and password', () => {
    cy.visit('localhost:3000/signin')
    cy.get('#username').type('Allie2')
    cy.get('#password').type('s3cret')
  })
})

If the Document Object Model (DOM) is foreign to you or you need a refresher, see Introduction to the DOM.
If you don't have a basic understanding of DOM element selection (with or without jQuery), this might get confusing. I would suggest checking out these resources:

jQuery Selector Types and Usage Explained
w3schools jQuery Selectors Reference (and their jQuery Selector Tester)


We still have to click Sign In, so let's inspect that button...

Here are some observations you might make:

  • this is the only button on the DOM
  • it contains the text, Sign In
  • it has attributes type="submit" and data-test="signin-submit"
  • it has several (somewhat convoluted) classes: MuiButtonBase-root, MuiButton-root, MuiButton-contained, makeStyles-submit-5, MuiButton-containedPrimary, MuiButton-fullWidth

There are a number of ways we can target and click on this element:

  • cy.get('button').click()
    simple but it contains no context; breaks when another button is added
  • cy.get('.MuiButtonBase-root').click()
    ambiguous and heavily coupled to style; breaks when style changes
  • cy.get('[type="submit"]').click()
    doesn't suffer from the same drawbacks; the best so far, but not a repeatable strategy
  • cy.get('[data-test="signin-submit"]').click()
    least brittle as it's intentionally and exclusively coupled to tests
  • cy.get('button:contains("Sign In")').click()
    simple like the first, but with necessary context; easy to read and use

I would recommend selecting by text content, falling back to a data-test attribute when that doesn't work or isn't an option.

A user interacts with our app based on textual content first so that's how we should write our tests. Doing so will not only make them more readable but also give us more confidence. If the content changes, the user could see that as a bug and documentation (including test cases) might need to be updated, so the test should break when content changes:

describe('login', () => {
  it('login with username and password', () => {
    cy.visit('localhost:3000/signin')
    cy.get('#username').type('Allie2')
    cy.get('#password').type('s3cret')
    cy.get('button:contains("Sign In")').click()
  })
})

We're assuming that changes to textual content are being avoided, otherwise user experience will suffer, but where these changes are made too often, fall back to data-test.

See also the Selecting Elements section in Cypress' own Best Practices guide.


Hands-on

Before we proceed, you'll need to get the Day-1 branch of our Real World App:

$ git checkout Day-1
$ npm install

Every new branch includes code that I demonstrated previously, hands-on work we did together and the solution to the previous Take-home Challenge.

In this new branch, you'll notice our login spec is missing the triple-slash directive at the top and the formatting is different. I updated the configuration for Typescript, ESLint and Prettier.

describe('sign in', () => {
  it('sign in with username and password', () => {
    cy.visit('localhost:3000/signin')
    cy.get('#username')
      .type('Allie2')
    cy.get('#password')
      .type('s3cret')
    cy.get('button:contains("Sign In")')
      .click()
  })
})

I also updated the Cypress configuration with a baseUrl (more on that later).


The Selector Playground is an interactive tool to help you find a unique selector for a given element, sometimes without having to open DevTools.

While testing our Real World App, we can open the Selector Playground and type the selector we chose for the Sign In button, button:contains("Sign In"). Seeing that it's highlighted tells us it's working and the 1 at the top tells us there's only one match.

Hovering such that entire button is highlighted like it was before and clicking on it gives us a confirmation that our data-test fallback would also work:

The two buttons to the right are for copying the entire cy.get command and for printing information to the console - both handy features you should try out for yourself.


So far, we've needed to specify the full URL when visiting our app:

cy.visit('localhost:3000/signin')

...but our project contains a cypress.json file where we can specify a baseUrl:

{
  "baseUrl": "http://localhost:3000"
}

Now we only need to specify the path or route:

cy.visit('/signin')

cy.contains() gets DOM elements that contain the given text content:

cy.contains('Real World App')

We can also provide a selector to return a specific element:

cy.contains('.title', 'Real World App')

And chaining it off cy.get() limits the context to whatever it returns:

cy.get('dialog').contains('.title', 'Real World App')

Let's update our test to use our newfound command:

describe('sign in', () => {
  it('sign in with username and password', () => {
    cy.visit('/signin')
    cy.get('#username')
      .type('Allie2')
    cy.get('#password')
      .type('s3cret')
    cy.contains('button', 'Sign In')
      .click()
  })
})

A test isn't a test without assertions, right? Even though our test doesn't appear to, it makes some assertions implicitly:

  • cy.visit('/signin')
    asserts that the page load was successful
  • cy.get('#username').type('Allie2') cy.get('#password').type('s3cret')
    asserts that the desired element exists on the DOM and accepts text input
  • cy.contains('button', 'Sign In').click()
    asserts that the button isn't covered by another element and can be clicked on

That's just a sample of all the Default Assertions Cypress makes implicitly.

Making assertions explicitly is done with .should() and the built-in Chai assertions. You observant types might have noticed that the Sign In button is disabled under certain conditions. We can observe that by pausing after filling in the Username by injecting a cy.pause() into our test and running it:

describe('sign in', () => {
  it('sign in with username and password', () => {
    cy.visit('/signin')
    cy.get('#username')
      .type('Allie2')
    cy.pause()
    cy.get('#password')
      .type('s3cret')
    cy.contains('button', 'Sign In')
      .click()
  })
})

Checking that the Sign In button is disabled is a worthwhile assertion:

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

.should() takes the what's returned (yielded) by the previous command (the subject), makes an assertion on that and then yields the same subject to whatever is chained off of it, like another .should() – or if you prefer, its alias .and():

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

Bonus Activities

  • Try the Selector Playground by pausing and finding the Remember Me checkbox
  • Check the Remember Me box before signing in
  • Make more assertions on the Sign In page
  • Test navigating to the to Sign Up page

Take-home Challenge

  • Test the Sign Up feature
    1. Fill out and submit the form
    2. Sign in as the new user
    3. Assert the url is correct
    4. Assert the Get Started with Real World App dialog is visible
  • Extra Credit: Sign up a unique user each time the test is executed
  • Extra Credit: Setup intelligent code completion (instructions here)

GO TOP

🎉 You've successfully subscribed to iheartjs!
OK