Introduction to Cypress
What you'll learn
- How Cypress queries the DOM
- How Cypress manages subjects and chains of commands
- What assertions look like and how they work
- How timeouts are applied to commands
Cypress Can Be Simple (Sometimes)
describe('Post Resource', () => {
it('Creating a New Post', () => {
cy.visit('/posts/new') // 1.
cy.get("input.post-title") // 2.
.type("My First Post"); // 3.
cy.get("input.post-body") // 4.
.type("Hello, world!"); // 5.
cy.contains("Submit") // 6.
.click(); // 7.
cy.get("h1") // 8.
.should("contain", "My First Post");
});
});
describe('Post Resource', () => {
it('Creating a New Post', () => {
cy.mount(<PostBuilder />) // 1.
cy.get("input.post-title") // 2.
.type("My First Post"); // 3.
cy.get("input.post-body") // 4.
.type("Hello, world!"); // 5.
cy.contains("Submit") // 6.
.click(); // 7.
cy.get("h1") // 8.
.should("contain", "My First Post");
});
});
Querying Elements
Cypress is Like jQuery
In fact, Cypress bundles jQuery and exposes many of its DOM traversal methods to you so you can work with complex HTML structures with ease using APIs you're already familiar with
// Each Cypress query is equivalent to its jQuery counterpart.
cy.get('#main-content').find('.article').children('img[src^="/static"]').first()
Cypress is Not Like jQuery
cy
// cy.get() looks for '#element', repeating the query until...
.get('#element')
// ...it finds the element!
// You can now work with it by using .then
.then(($myElement) => {
doSomething($myElement)
})
cy
// cy.get() looks for '#element-does-not-exist', repeating the query until...
// ...it doesn't find the element before its timeout.
// Cypress halts and fails the test.
.get('#element-does-not-exist')
// ...this code is never run...
.then(($myElement) => {
doSomething($myElement)
})
Before, you'd be forced to write custom code to protect against any and all of these issues: a nasty mashup of arbitrary waits, conditional retries, and null checks littering your tests. Not in Cypress! With built-in retrying and customizable timeouts, Cypress sidesteps all of these flaky issues
Querying by Text Content
// Find an element in the document containing the text 'New Post'
cy.contains('New Post')
// Find an element within '.main' containing the text 'New Post'
cy.get('.main').contains('New Post')
When Elements Are Missing
// Give this element 10 seconds to appear
cy.get('.my-slow-selector', { timeout: 10000 })
Chains of Commands
It's very important to understand the mechanism Cypress uses to chain commands together. It manages a Promise chain on your behalf, with each command yielding a 'subject' to the next command, until the chain ends or an error is encountered
Interacting With Elements
Here are even more action commands Cypress provides to interact with your app:
.blur()
- Make a focused DOM element blur..focus()
- Focus on a DOM element..clear()
- Clear the value of an input or textarea..check()
- Check checkbox(es) or radio(s)..uncheck()
- Uncheck checkbox(es)..select()
- Select an<option>
within a<select>
..dblclick()
- Double-click a DOM element..rightclick()
- Right-click a DOM element.
For example, when writing a .click()
command, Cypress ensures that the element is able to be interacted with (like a real user would). It will automatically wait until the element reaches an "actionable" state by:
- Not being hidden
- Not being covered
- Not being disabled
- Not animating
Asserting About Elements
Assertions let you do things like ensuring an element is visible or has a particular attribute, CSS class, or state
cy.get(':checkbox').should('be.disabled')
cy.get('form').should('have.class', 'form-horizontal')
cy.get('input').should('not.have.value', 'US')
Subject Management
A new Cypress chain always starts with cy.[command]
, where what is yielded by the command establishes what other commands can be called next (chained)
Each command specifies what value it yields. For example,
cy.clearCookies()
yields null. You can chain off commands that yield null, as long as the next command doesn't expect to receive a subject.cy.contains()
yields a DOM element, allowing further commands to be chained (assuming they expect a DOM subject) like.click()
or evency.contains()
again..click()
yields the same subject it was originally given
Don't continue a chain after acting on the DOM While it's possible in Cypress to act on the DOM and then continue chaining, this is usually unsafe, and can lead to stale elements. See the Retry-ability Guide for more details.
But the rule of thumb is simple: If you perform an action, like navigating the page, clicking a button or scrolling the viewport, end the chain of commands there and start fresh from cy
.
Using Aliases to Refer to Previous Subjects
cy.get('.my-selector')
.as('myElement') // sets the alias
.click()
/* many more actions */
cy.get('@myElement') // re-queries the DOM as before
.click()
This lets us reuse our queries for more readable tests, and it automatically handles re-querying the DOM for us as it updates. This is particularly helpful when dealing with front end frameworks that do a lot of re-rendering
Commands Are Asynchronous
it('does not work as we expect', () => {
cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // Still nothing happening
.click() // Nope, nothing
.then(() => {
// placing this code inside the .then() ensures
// it runs after the cypress commands 'execute'
let el = Cypress.$('.new-el') // evaluates after .then()
if (el.length) {
cy.get('.another-selector')
} else {
cy.get('.optional-selector')
}
})
})
// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!
The Cypress Command Queue
While the API may look similar to Promises, with its then()
syntax, Cypress commands and queries are not promises - they are serial commands passed into a central queue, to be executed asynchronously at a later date
Assertions
What makes Cypress unique from other testing tools is that assertions automatically retry
Asserting in English
cy.get('button').click()
cy.get('button').should('have.class', 'active')
This above test will pass even if the .active
class is applied to the button asynchronously, after an indeterminate period of time or even if the button is removed from the DOM entirely for a while (replaced with a waiting spinner, for example).
Implicit Assertions
For instance:
cy.visit()
expects the page to send text/html content with a 200 status code.cy.request()
expects the remote server to exist and provide a response.cy.contains()
expects the element with content to eventually exist in the DOM.cy.get()
expects the element to eventually exist in the DOM..find()
also expects the element to eventually exist in the DOM..type()
expects the element to eventually be in a typeable state..click()
expects the element to eventually be in an actionable state..its()
expects to eventually find a property on the current subject
List of Assertions
Cypress bundles Chai, Chai-jQuery, and Sinon-Chai to provide built-in assertions
Writing Assertions
There are two ways to write assertions in Cypress:
- As Cypress Commands: Using
.should()
or.and()
. - As Mocha Assertions: Using
expect
.
Command Assertions
Using .should()
or .and()
commands is the preferred way of making assertions in Cypress
cy.get('#header a')
.should('have.class', 'active')
.and('have.attr', 'href', '/users')
Mocha Assertions
Using expect
allows you to assert on any JavaScript object, not just the current subject
// the explicit subject here is the boolean: true
expect(true).to.be.true
Mocha assertions are great when you want to:
- Perform custom logic prior to making the assertion.
- Make multiple assertions against the same subject
The .should()
assertion allows us to pass a callback function that takes the yielded subject as its first argument. This works like .then()
, except Cypress automatically waits and retries for everything inside of the callback function to pass
cy.get('p').should(($p) => {
// massage our subject from a DOM element
// into an array of texts from all of the p's
let texts = $p.map((i, el) => {
return Cypress.$(el).text()
})
// jQuery map returns jQuery object
// and .get() converts this to an array
texts = texts.get()
// array should have length of 3
expect(texts).to.have.length(3)
// with this specific content
expect(texts).to.deep.eq([
'Some text from first p',
'More text from second p',
'And even more text from third p',
])
})
Timeouts
Applying Timeouts
You can modify a commands's timeout. This timeout affects both its default assertions (if any) and any specific assertions you've added
// we've modified the timeout which affects the implicit
// assertions as well as all explicit ones.
cy.get('.mobile-nav', { timeout: 10000 })
.should('be.visible')
.and('contain', 'Home')
Notice that this timeout has flowed down to all assertions and Cypress will now wait up to 10 seconds total for all of them to pass
Default Values
We've set their default timeout durations based on how long we expect certain actions to take
For instance:
cy.visit()
loads a remote page and does not resolve until all of the external resources complete their loading phase. This may take awhile, so its default timeout is set to60000ms
.cy.exec()
runs a system command such as seeding a database. We expect this to potentially take a long time, and its default timeout is set to60000ms
.cy.wait()
actually uses 2 different timeouts. When waiting for a routing alias, we wait for a matching request for5000ms
, and then additionally for the server's response for30000ms
. We expect your application to make a matching request quickly, but we expect the server's response to potentially take much longer.
That leaves most other commands including all DOM queries to time out by default after 4000ms