This blog post teaches you how to write a reusable Cypress Custom Command.
- Simple custom command
- Simple command limitation
- Custom command with retry-ability
- Final thoughts
- Update 1: publish command to NPM
- Update 2: add local types
- Read next
- Examples
Let's take a page with a form. There are a couple of input fields. Every input field has a label.
1 | <form> |
🔎 You can find the source code for this blog post in the "Recipes" section of glebbahmutov.com/cypress-examples.
It is simple to select and element by its attribute, like id or name using cy.get command.
1 | // we can find the input field by id and name using the standard cy.get |
What if we want to find the input element by the label's text? We could first find the label using the cy.contains command, then grab the for
attribute, and then pass it to the cy.get
command.
1 | cy.contains('label', 'First name:') |
If you have to find multiple input elements by the label, you can make the above code fragment reusable by making it into a function.
1 | // we return the Cypress chain so the test can attach more commands |
Note the getInputByLabel
returns the entire Cypress command chain, thus we can add more commands or assertions. A reusable function is simple to write and use, and is my "go to" method for factoring out the common Cypress code.
Simple custom command
You can also place common testing code into a custom command. In the simplest case, the command might simple contain the exactly the same code as the reusable function.
1 | Cypress.Commands.add('getByLabel', (label) => { |
Notice that we did not have to return the command chain from the custom command, this is done automatically for every command.
Simple command limitation
The limitation of this simple command is its lack of retry-ability. The final input element yielded by the cy.get('#' + id)
command should pass the assertion .should('have.value', '')
. What happens if the input element has some other value? Let's see:
Notice that because we have HTML markup <input type="text" id="fname" name="fname" value="Gleb" />
the last command cy.get ...
was retried automatically, since the assertion was failing. So the custom command automatically retries the last command inside of it. What if we want to retry the entire command starting with cy.contains
?
Here is an example when it is necessary. Imagine the application starts with some static HTML that later hydrates. The initial element IDs might be invalidated - because the entire or part of the page is re-rendered. Let's say the initial HTML is simple:
1 | <form> |
A second after the page loads, the app re-renders the form. Notice how the IDs are now realistic.
1 | function hydrate() { |
What happens to our custom command? Well, it is still tries to find an element with ID 'fname' using cy.get('#' + id)
code.
So what can we do it retry the cy.contains
and other commands inside the custom command?
Custom command with retry-ability
We need to write a custom command that has logic to find the input element, then checks all upcoming assertions. If the assertions fail, then it retries the entire logic from the start again. Let's write this command in parts.
First, we need to write a command placeholder. We will log the text passed to the command:
1 | Cypress.Commands.add('getByLabel2', (label, options = {}) => { |
Let's write the logic to find an element by its text. We need to search the application's document
object. To access it we can use the undocumented cy.state
command, and then we can use jQuery commands to find the element.
1 | Cypress.Commands.add('getByLabel2', (label, options = {}) => { |
The function getValue
is ready to be tried again and again. Let's finish our custom command. We need to call getValue
, get the element and then verify the upcoming assertions. Here is the end of the custom command
1 | return Cypress.Promise.try(getValue).then(($el) => { |
The above fragment inside the custom command does the trick. One last tip: the finished command can place the found element into the log object.
1 | ... |
Let's verify our custom command finds the input by the label's text in the hydration example.
If you click on the command getByLabel2
in the Command Log, you will find the details and the element itself printed to the console.
Note: if you try to use Cypress commands like cy.contains
inside getValue
function, Cypress detects it and throws an error.
Final thoughts
- only the last Cypress command is retried when the assertions that follow it fail
- you can write the logic to find the element using
cy.state('document')
and jQuery / DOM commands - to verify the upcoming assertions and retry your logic to find the element, use
cy.verifyUpcomingAssertions
1 | const resolveValue = () => { |
Happy testing!
Update 1: publish command to NPM
Read How to Publish Custom Cypress Command on NPM where I describe how to correctly publish the custom command via NPM registry, including loading with custom command type.
Update 2: add local types
To provide intelligent code completion for our new custom command, I wrote src/index.d.ts
file
1 | // load type definitions that come with Cypress module |
Typically in a JavaScript project, every spec file includes the following line to load global Cypress definitions
1 | /// <reference types="cypress"> |
In our case, the specs exercise the new cy.getByLabel
command. Thus they need to "know" about it. In the cypress/integration/spec.js
I replaced the reference types=cypress
comment with reference path=...
pointing at the src/index.d.ts
file:
1 | /// <reference path="../../src/index.d.ts" /> |
Note that we no longer need the reference types="cypress"
line - because it is loaded by src/index.d.ts
file.
Next, you can add the "types" property to the "package.json" file, pointing at the index.d.ts
file or a folder with it. For example:
1 | src/ |
Inside "package.json" point at the "src/index.d.ts" file
1 | { |
Then you can omit the </// reference ...>
comment lines - the types should be read from the "src/index.d.ts". You can even verify the types by adding typescript
as a dev dependency and adding a lint step:
1 | $ npm i -D typescript |
In the "package.json" add a "lint" step
1 | { |
For example, see cypress-data-session repo.
Read next
Examples
cy.getByLabel
in bahmutov/cypress-get-by-labelcy.dataSession
in bahmutov/cypress-data-session