We have a simple test here that creates our board.
beforeEach(() => {
cy.request('POST', '/api/reset')
})
it('Custom commands', () => {
cy
.visit('/');
cy
.get('[data-cy="create-board"]')
.click();
cy
.get('[data-cy=new-board-input]')
.type('new board{enter}');
});
This is an action that we already done a couple of times in our course.
It would be nice to have some kind of abstraction in place — so that if we ever face a situation that our application changes, we don't have to change multiple tests, but just make a simple change in one place.
Let's take this sequence of actions and create a custom command for it.
For creating custom commands, I'm going to use the Cypress.Commands
API. I'm going to add
a new command, and we'll name it “addBoard”.
The second parameter will be a function. And I will paste my command sequence inside.
Now in my test, I can use my new command addBoard
.
When I save it, you can see that it does exactly the same thing as before, but let's make it dynamic.
Let’s for example, make this part where we type in the new board name customizable.
In our function, we will add an input
parameter and in our type
command, we will pass this input
with our text.
Now, inside our text, if we want to give our board a custom name, I'll just type it in, “groceries”.
When I save my text now, you can see that it has used the input that I have given it.
We made our custom command, but so far it is available only inside our test.
Let's change that.
When you create a Cypress Project, there's a place for your custom commands. And that is inside our support folder.
We have a “commands.js” file and inside it, we can paste our new command.
When I now save both of my files, you'll see that everything seems to be working exactly as before.
With custom commands, we can create our own library of commands that we are going to be using for testing our app, but we can certainly make some improvements.
For example, when I hover over my visit
command, I can see a short documentation of what the command does.
But I get no such thing for my addBoard
command.
Also, when I type any Cypress command, I get all these nice suggestions.
But if I delete this and try to type addBoard
, I don't really get a suggestion — and there's a way to change that.
We can find the answer in the Cypress documentation. I'll open a new tab here, look into the chapter for types of custom commands.
Here, we have a suggestion that we can create a definitions file and expand our Cypress namespace with our custom commands.
Let's do that.
I will copy this example file (from the Cypress documentation) and use it to create our own definitions.
I'll open support folder, create a new file, and I'm going to call that “command.d.ts”. I will paste the code from documentation.
Instead of this “data-cy”, I will create my own and that will be our “addBoard”.
This is now telling our editor that inside our Cypress namespace, there's an interface
called Chainable
, and that has a function called addBoard
.
Now don't worry if you're confused. We are entering TypeScript world here, and that is a whole another chapter that could have a separate course on it.
For now, let's focus on what we want to achieve.
We want to make sure that when we start typing, our new function will appear. And from now on, that should be the reality. So, let's delete this (old example from test) and start typing.
And as you can see, our code editor is now suggesting our addBoard
function, and that's great.
But let's also add that mini documentation that we saw in the visit
command.
For that, we can use something that's called JSDoc. It is a special kind of comment that works exactly as you saw. When I hover over a function that has this comment in place, it will show up.
To create a JSDoc, I'll type slash and double asterisk (/**
). And my code editor is going to recognize that we are going to type JSDoc comment.
Now, here I can write a short description. For example, “Creates a new board via UI”.
And we can also suggest an example.
These keywords that you can see here are actually parts of JSDoc that you can use.
I'll create a simple example. So, cy .addBoard
, and inside we'll have a “board name”.
// in cypress/support/index.d.ts
// load type definitions that come with Cypress module
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
/**
* Creates a new board via UI
*
* @example
* cy
* .addBoard('board name')
*/
addBoard()
}
}
When I now save this, and hover over our command, you can see that our little documentation has appeared.
So, whoever reads this now, will now know how this command works.
Let's continue playing with our custom commands, I'm going to create another one.
When you look inside our application, you may notice that I have quite a few data-cy
attributes in my application.
Whenever I want to select them, I will type get('[data-cy={the value of the data-cy attribute}]')
.
(e.g: cy.get('[data-cy=board-item]')
)
Instead of typing all that, I want to create my custom command, where I will just input the value of the data-cy
attribute.
Let’s once again, use Cypress API (Cypress.Commands.add
) to create a new command, and instead of get
, I'm going to call this command take
.
It is going to take an input
, and inside the function, I will use that input.
Let's type cy.get
, and that will use a template string. So, it will be data-cy
equals, and it will equal our input.
Cypress.Commands.add('take', (input) => {
cy
.get(`[data-cy=${input}]`)
}
When I now use my take
command, I can just select create-board
.
When I save this now, it's going to select my element. So, that's great.
But we can go even further.
Let’s say for example, that I want my take
command to search inside the whole DOM.
But if I chain it off another get
command, that's going to yield an element. I want to search just within the context of that element. Let me show you what I mean.
I'm going to create a new board. I will create a “todo” list, and then a “done” list.
And inside each of my lists, I'm going to create a task. So, this will be “task 1”. And the other will be “task 2”.
If I use my Selector Playground, you can see that “task 1” has data selector with value of “task”. And the other one also has a data selector with a value of “task”.
What I want to achieve is to select one or the other, depending on whether my take
command is a child command or a parent command.
Let's close the Selector Playground, and I will delete this beforeEach
hook. And for my visit
command, I'm going to open my board.
Right now, if I use my take
command and type in “task”, it is going to select 2 elements.
Let's use our take
command to first select a “list”, and we will then use eq
command to select the first list.
cy
.take('list')
.eq(0)
.take('task')
When I save this now, you'll see that first I'm selecting both of my lists, then I'm filtering the first one.
And then when I use my take
command the second time, it's again going to select all the tasks inside the whole application.
But what I want to achieve is to select only the task that is within the context of this list.
What we can do with our command, is to give it another argument. That's going to be called previous subject (prevSubject
).
If I would set this to “true”, then my take
command would only work as a child command. So, it would require that there is some element before it.
When I save this now, you can see that my command is failing, and it is telling me that the child command must be chained after a parent command because it operates on a previous subject.
But I actually want to make this a dual command, so, the prevSubject
will be “optional”.
In this type of command, there will always be at least 1 parameter. It seems we already have 1 here, we're going to have 2. And the first one will be the “subject”.
Let's handle both cases in our command; I'm going to write a condition.
If we have a “subject”, then we are going to do 1 action. And if we don't, then basically our command is going to stay the same.
Now if we have a subject, we are first going to use wrap
command, to select our “subject”. And then we are going to look (.find
) for an element that is ancestor to this subject. And that is going to be our “data-cy” input.
Cypress.Commands.add('take', {prevSubject: 'optional'}, (subject, input) => {
if (subject) {
cy
.wrap(subject)
.find(`[data-cy=${input}]`)
} else {
cy
.get(`[data-cy=${input}]`)
}
})
it('Custom commands', () => {
cy
.visit('/board/77787127477');
cy
.take('list')
.eq(0)
.take('task')
});
Let's now save all this, and you can see that our first take
command is going to select all the lists.
We then filter the first element and our second take
command, it's actually going to wrap
our first element and then look for a task inside it.
This way we have created quite a powerful custom command.
Let's recap.
With custom commands, we can create a sequence of commands or a custom logic that we want to abstract. That way we can use it anywhere in our tests. Our custom commands can be stored inside support folder.
To make autocomplete work, we can create a definitions TypeScript file that will contain the name of our function, that we can use for expanding our Cypress namespace.
To create a custom documentation for our commands, we can use JSDoc. Inside this Doc, we can explain what the function does as well as provide an example of how that function can be used.
Also, we can create parent, child, or dual custom commands. If we want to make our command really dynamic, we can write a custom logic inside, that will handle different situations.