All the automation tools provide a set of commands which perform a designated action and helps in simulating a user behavior. Following the same, Cypress also provides a set of commands which affect the user actions. But it also provides an added functionality that you can define a command of your own. These commands are Cypress custom commands. These commands can be created to give a different behavior to the existing in-build Cypress command or can be used to create a new command altogether. Let's understand the details and implementation of the Cypress custom command by briefing the details under the following sections:
- What are custom commands in Cypress?
- How to add a new custom command in Cypress?
- Understanding the usage of custom commands with examples.
- What are the recommended best practices for Custom commands in Cypress?
- How to add Custom commands documentation in Cypress Intellisense?
What are custom commands in Cypress?
A custom command in Cypress is nothing more than a regular Cypress command. The only difference is it is defined by the user, as opposed to the default commands that Cypress supplies out of the box. Custom commands are beneficial for automating a workflow you repeat in your tests over and over. Let's see how we can add a custom command in Cypress:
How to add a new custom command in Cypress?
Cypress provides an API for creating a new custom command or overwriting existing commands to change their implementation. The built-in Cypress commands also use the API. It can be represented syntactically as follows:
// Add a new command
Cypress.Commands.add(name, callbackFn)
Cypress.Commands.add(name, options, callbackFn)
//Overwrite an existing command
Cypress.Commands.overwrite(name, callbackFn)
Cypress.Commands.overwrite(name, options, callbackFn)
Where,
- name is the name of the command that you are creating or overwriting.
- callbackFn is a function that receives arguments passed to the command.
- The options parameter is an object which defines the implicit behavior of the custom command.
Option | Accepts | Default | Description |
---|---|---|---|
prevSubject | Boolean, String or Array | false | Specify handling the previous command subject. |
The best place to define the custom commands is in the cypress/support/commands.js file, as it loads before any of the test-script files. Let's understand how we can add various kinds of custom commands in Cypress:
How to add a parent custom command in Cypress?
As we know, Cypress provides various commands such as visit(), get(), request(), etc., which always begin a new chain of commands and are "Parent Commands". Similarly, we can add a new custom command, which will serve as a parent command and will never be dependent on the subject generated by the previous command in the command chain. These commands can be directly invoked on the cy object and are syntactically represented as follows:
// Adding a parent custom command
Cypress.Commands.add(name, callbackFn)
If we don't pass any value for the optional "prevSubject", the new command will automatically be considered as "Parent command" and can be invoked directly on the "cy" object. Let's try to understand the same with the help of the following example:
Suppose our application has a scenario where we need to click on various buttons that can be accessed using the labels of the buttons. So, instead of invoking the get() and click() commands, we can add a new custom command which accepts the label of the button and performs of these options in a single command. So, to achieve the same, we can declare a new custom command as follows:
Cypress.Commands.add('clickButton', (buttonLabel) => {
cy.get('button').contains(buttonLabel).click();
})
Now, we can invoke this command in our test scripts as follows:
cy.clickButton('Next Article');
So, it will search for a button labeled "Next Article" and will click on the button. So, this way, it will make the code clean and clear and will provide a new and easy function to perform various complex operations.
How to add a child custom command in Cypress?
Similar to "Parent commands", Cypress also provides various child commands such as click(), find(), should(), which are dependent on the subject yielded by the previous command in the command chain. Using custom commands, we can add new "Child commands" also, which will always perform the action on the subject yielded by the previous command and will always be invoked as a chained command only. Let's try to understand the same with the help of the following example:
Suppose, automation has a need that we need to get the text of various UI elements, but Cypress doesn't provide any in-built command to get the text of an element. One can only achieve this with the help of "JQuery". Now to avoid writing the same JQuery code again and again for getting the text, we can add a child custom command, which will get the text of the parent element on which the command will invoke. It implements as follows:
Cypress.Commands.add('getText', { prevSubject: 'element' },
($element: JQuery<HTMLElement>) => {
cy.wrap($element).scrollIntoView()
return cy.wrap($element).invoke('text')
}
)
As we can see, the prevSubject parameter passes as the element on which the command will invoke. Now, we can use the "getText()" command to get the text of the any of the HTML elements as shown:
cy.get('#loginID').getText();
So, the above command will return the text of the HTML element with id as "loginID".
How to add a dual custom command in Cypress?
The dual commands are kind of hybrid command which lie between the Parent and the Child commands. These can either be called directly on the cy object or can chain with other commands also. Let's try to understand the same with the help of the following example:
Suppose, while automating our application, we need a getLink function which gets the href attribute of the element. Now, the getLink function can be invoked either at the parent level, where it will get the attribute of the first element in the document. Or it can invoke in chained command, where it will get the attribute of the elements under the parent element. It can implement as follows:
Cypress.Commands.add('getLink', {
prevSubject: 'optional'
}, (subject) => {
if (subject) {
cy.get(subject).get('a').its('href');
} else {
cy.get('a').its('href');
}
})
As we can check in the above code-snippet, the prevSubject is marked as optional. So, the above command can invoke directly on the cy object or on a parent command, as shown below:
cy.getLink() // no subject
cy.get('#dialog').getLink() // with subject
So, the above command will serve as a dual command and can be invoked both as a parent as well s child command.
Let's now understand the usage of custom commands with a practical example:
Understanding the usage of custom commands with examples:
Consider that we have to automate the following scenario:
- Open the URL, which is My Account page of E-commerce demo website - http://shop.demoqa.com/my-account/.
- Then do the registration using a valid username, email, and password.
- Verify if the registration was successful or not.
- Search for a Shirt and select two products as per the data provided in parameters.
So we will be targeting for searching the shirt, and selecting two products defined as a test data is the fixture files.
Now let's write a test that can traverse through all shirt search results and add one to the cart, which we want to add or which we have passed as a parameter to the test, and we will store this data in fixtures directory. So, in this case, we have to write a generic function that takes the product name as an input and does the rest of the work for us. We can achieve this by writing a generic method in Cypress under cypress/support/commands.js.
Now before moving to create a custom command, let's store the data of product name first in example.json file under fixture directory, which includes the product name, size, and color. It includes a 2*2 data which has all the details of the product in a variable productName.
{
"Username": "aashishk7",
"Email":"[email protected]",
"Password": "cypresstutorials",
"NewPassword":"cypresstutorials77",
"productName":[["blue denim","34","Black"],
["playboy","40","Grey"]]
}
Now next step is to create a custom command which does the rest of the work for us. It searches for the shirt and then accordingly searches for the product and keeps on adding to the cart after reading the data from the fixtures file.
Now, let's add a custom command "selectProduct", which will search for the product and add it to the cart. Add the following code in the file cypress/support/commands.js.
Cypress.Commands.add("selectProduct", (productName, size , color) => {
// Doing the search part for Shirts.
cy.get('.noo-search').click()
cy.get('.form-control').type('Shirt');
cy.get('.form-control').type('{enter}')
// Searching for product mentioned in fixtures file
cy.get('.noo-product-inner h3').each(($el , index , $list) => {
if($el.text().includes(productName)) {
cy.get($el).click();
}
})
// Selecting the size and color and then adding to cart button.
pcy.get('#pa_color').select(color);
cy.get('#pa_size').select(size);
cy.get('.single_add_to_cart_button').click();
})
So now, we have our data defined in the fixtures directory, and we have created a custom command as well. Now the only bit left is to use this custom command in our test, which is just like using any other cypress command, so let's add a new test as shown below and invoke the custom command to perform the needed action.
// type definitions for Cypress object "cy"
// <reference types="cypress" />
describe('Cypress Custom Commands', function() {
//Mostly used for Setup Part
before(function(){
cy.fixture('example').then(function(data)
{
this.data=data ;
})
})
it('Cypress Test Case', function() {
//Registration on the site
cy.visit('https://shop.demoqa.com/my-account/');
cy.get('#reg_username').type(this.data.Username);
cy.get('#reg_email').type(this.data.Email);
cy.get('#reg_password').type(this.data.NewPassword);
cy.get('.woocommerce-Button').click();
//Checking whether the Registration is successful and whether UserName is populated under login section
cy.get('#username').should('have.value',this.data.Username);
})
// For Loop for Accessing productName array from Features File and Using the custom command
this.data.productName.forEach(function(element){
// Invoke the Custom command selectProduct
cy.selectProduct(element[0],element[1],element[2]);
})
})
As we can see in the above code snippet, it invokes the custom command "selectProduct" inside a for loop, which is taking the inputs from the fixture file and adding all those products in the cart. Save the test files as "CypressTest8.js" and execute it. It will show a sample output, as shown below:
As is evident from the above screenshot, marker 1 and 2 shows the addition of products mentioned in fixtures files, and marker 3 shows the successful addition of two products. So our custom commands implementation was successful.
What are the recommended best practices for it?
Even though there are no restrictions on what we can add as a custom command, there are still some best practices that ensure the correct usage of custom commands. Few of them are:
- Don't make everything a custom command: With the kind of ease and cleanliness custom commands provide, users tend to add each and every function as a custom command, which makes the maintenance very cumbersome. So, until we use the needed functionality across multiple spec files, try to make it as an independent function, instead of a global custom command.
- Don't do too much in a single command: Don't try to wrap everything under a single command. The custom commands are just wrapper over the in-build commands. Therefore, wrapping everything inside a single command, make the overall functionality less visible to all the stakeholders. So, ideally, try to avoid wrapping everything under a single custom command and keep them simple.
- Skip UI actions in Custom commands: Custom commands are a wrapper that encapsulates the built-in commands. So if we will have multiple UI actions inside the custom commands, then invoking the custom commands will make the overall test execution very slow. So, try to avoid UI actions as much as feasible in the custom commands.
How to add Custom commands documentation in Cypress Intellisense?
If you are using any of the modern IDE such as the Visual Studio Code for your test development, IntelliSense is one of the critical functions. It makes the development very easy by providing support by showing the documentation and signature of the commands to use. So, the expectation with the custom commands will be that it always benefits the colleagues to understand. Additionally, it should use the custom commands if IntelliSense can show the signature and documentation for the custom commands. Let's understand how we can implement the same for our custom commands:
IntelliSense uses the TypeScript to know and show the syntax and usage of various commands. We describe the signature of the custom command in file cypress/support/index.d.ts as shown below. We do it to let the TypeScript compiler know that we have added a custom command and have IntelliSense working.
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable<Subject> {
/**
* Select and add product to Cart
* @example
* cy.selectProduct(productName, size , color)
*/
selectProduct(productName: String, size: String , color: String): Chainable<any>
}
}
Now, to make the spec file aware that there is a typescript defined which we can refer for the definition of the commands, as the following code on top of a test script:
// type definitions for custom commands like "createDefaultTodos"
// will resolve to "cypress/support/index.d.ts"
// <reference types="../support" />
Now, whenever a user will type the cy.selectProduct(), Intellisense will show the help of the command, as shown below:
Therefore, by providing the details in the typescript, we can enable the IntelliSense support for all of our custom commands.
Key Takeaways
- Custom commands help by providing an easy interface for various repetitive tasks.
- Additionally, custom commands can add as Parent, Child, and Dual commands.
- We shouldn't overuse the custom commands and should abide by the best practices suggested.
- You can add support for Custom commands in IDE IntelliSense by adding the needed details in the typescript.
As we have now understood the Cypress's custom commands in detail. Let's move to the next article to understand how we can design the "Page Object Pattern" in Cypress.
If you want to learn more you can look at this video series: ++Cypress video series++