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 visit
ing /
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 expectedusername
- 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.