The "one-stop shop" for creating Cypress plugins that'll have everyone cheering and chuckling.
(Cover image from https://www.youtube.com/c/Cypressio and https://authy.com/guides/npm/npm-logo/)
ACT 1: EXPOSITION
When I set out to develop my organization's first Cypress npm plugin, I expected the process to be relatively straightforward. I anticipated finding a wealth of blogs and documentation that detailed the configuration, creation, and publishing of such plugins.
"Easy peasy", I told myself.
To my surprise, comprehensive resources that clearly explained the configuration and implementation of a Cypress plugin were scarce, and unraveling the publishing process turned out to be an adventure in itself. After all, the joy of crafting a plugin should lie in the creation of remarkable functionalities, not in the laborious task of building infrastructure and configuration, right?
This revelation made it clear to me that if I were to write a blog about Cypress, I would dedicate an entire post to demystifying the plugin creation process.
I want to set expectations from the start: the aim of this blog is not to craft the most revolutionary plugin in the Cypress ecosystem, so don't expect intricate, world-changing functionalities for Cypress to be implemented here. Instead, our focus will be on the nuts and bolts of how you can build one of these plugins.
I didn't want to contribute just another generic post. My ambition was to establish the definitive "one-stop shop" for QA Engineers seeking to create a Cypress npm plugin from the ground up—a reliable guide to get the job done, without unnecessary embellishments.
And I assure you, that's precisely what you'll find in this blog!
ACT 2: CONFRONTATION
Our plugin will consist of a couple of custom Cypress commands: one to compare the values of two aliases, and another to log information in different colors in the Cypress runner log, as well as provide additional information in the console.
After we have implemented our plugin, we will publish it on the public npm registry so that everyone can benefit from our "contribution to the world".
The name of the plugin will be 'how-to-create-a-cypress-plugin', which will be hosted on a GitHub repository. We will utilize Visual Studio Code (VS Code) as our Integrated Development Environment (IDE). For simplicity, all Git operations, such as cloning and committing, will be performed directly within VS Code.
So, get ready for a coding marathon. Here are the six essential steps to master the task:
- Create and configure the GitHub project
- Set up the project in the local terminal and IDE
- Install and configure Cypress in the project
- Develop the plugin's source code
- Write tests for the plugin
- Publish the plugin to the npm registry
In other words:
- 1. Git Set Go! Configuring Your GitHub Repository with Pizzazz!
- 2. Get Your Local Groove On: Setting Up the Project with Zest!
- 3. Green Carpet Gala: Ushering Cypress into Your Project!
- 4. Concocting the Code: Fun with Plugin Development!
- 5. Whipping Up Witty Cypress Tests for Your Plugin!
- 6. Launch Your Plugin Into the Wild: Publishing on npm with a Bang!
1. Git Set Go! Configuring Your GitHub Repository with Pizzazz!
The Prerequisite Party List
- Have a personal GitHub account.
GitHub Sign-In Shenanigans: Accessing the Geeky Gateway
Navigate to https://github.com
in your browser and log in using your personal account credentials.
Repo Rodeo: Wrangling a Fresh GitHub Repository into Existence
For simplicity, we will create the new repository with a main
branch.
Navigate to the 'Repositories' tab to create a new GitHub repository.
Then, provide the following repository details:
Repository Name:
how-to-create-a-cypress-plugin
Description:
How to Create a Cypress Plugin
Select repo type
Public
Checkmark
Add a README file
Chose a license:
MIT
You can choose to add a .gitignore
file now, or you can add it later (in this exercise, we will add it later). For the license type, we have chosen MIT
to ensure that everyone can use our plugin.
2. Get Your Local Groove On: Setting Up the Project with Zest!
Before We Roll: The Must-Haves Checklist
Ensure the following are installed on your local machine:
- Git (for Mac) / Git Bash (for Windows)
- Visual Studio Code (IDE)
Hitch a Ride into GitHub: The VS Code Login Hoedown
Log in to your GitHub account using a browser, then open VS Code (this will make it easier to log in to GitHub from VS Code).
Proceed to sign in to your GitHub account from within VS Code by authorizing access.
If the login is successful, you will see your GitHub user reflected in VS Code.
Repo Replication Rumba: Syncing Up with VS Code's Clone Dance
In VS Code Explorer panel, select "Clone Repository", enter the URL for your GitHub project (https://github.com/sclavijosuero/how-to-create-a-cypress-plugin.git
), then choose the folder on your local machine where you want to clone the repository, and select "Open".
If the repository is cloned successfully, a new VS Code window will open, displaying the contents of your GitHub repository in the Explorer panel.
Kickstarting Your npm Adventure: Initializing the Package with Flair
The next step is to initialize the npm package in our project. To do this, type npm init
in the terminal.
Then, provide the required package information:
- Package name:
how-to-create-a-cypress-plugin
- Version:
1.0.0
- Description:
Example of how to create a Cypress npm Plugin
- Entry Point:
src/index.js
- Test Command: (just leave empty)
- Git Repository:
https://github.com/sclavijosuero/how-to-create-a-cypress-plugin.git
- Keywords:
plugin
- Author:
Sebastian Clavijo Suero
- License:
MIT
If everything proceeds as expected, a file named package.json
will be created at the root of our project, containing all the information provided.
But let's pause for a moment, and I'd like you to focus on one particular property of the package: main
.
The main
property is initialized with the value we provided as the Entry Point when we created the package. It represents the relative path from the root of our project to the file that will be executed when our plugin is imported.
In our case, the file is src/index.js
, and we will revisit it later when we implement the source code of our plugin.
3. Green Carpet Gala: Ushering Cypress into Your Project!
Well... as top-notch SDETs or QA Engineers, we aim for our plugin to be as bulletproof as a superhero, right? So, let's gear up and install Cypress in our package to create and run later epic tests for our plugin.
Cypress Install: Chairs for All, No Running Required
In the terminal, simply type npm install --save-dev cypress
, and that's all. This command will add the latest version of Cypress to the devDependencies
in your package.json
file.
Cypress Setup: So Slick, It's Almost Sneaky
To start configuring Cypress, run the command npx cypress open
in the terminal.
Afterward, simply follow and accept the prompts in the configuration screens:
This process will create the cypress.config.js
file and the cypress
folder, which contains several subfolders and configuration files.
The next step is to complete the configuration of the cypress.config.js
file by specifying the viewportWidth
and viewportHeight
dimensions, setting the specPattern
to cypress/e2e/**/*.{js,jsx,ts,tsx}
, and defining the baseUrl
for our upcoming tests.
Lastly, we will create a .gitignore
file at the root of our project (if it hasn't been created already) and add the directories we want to exclude from our commits to GitHub: node_modules/
, cypress/screenshots/
, cypress/videos/
, and cypress/downloads/
.
4. Concocting the Code: Fun with Plugin Development!
Quick Recap: Here's the "power duo" that our plugin will feature:
- A custom Cypress command to deftly compare the values of two aliases.
- A custom Cypress command to colorfully log information in the Cypress runner and enrich the console with additional details.
And now the real fun begins!
Whipping Up Our Plugin: Simple, Swift, and Satisfying
Let's create the src
folder at the root of our project. This folder will house the source files for our plugin.
Next, we'll create the index.js
file inside the src
folder. This file will contain the source code for our two custom Cypress commands.
The first custom command we'll create is cy.compareAliases(chainers, alias1, alias2)
. This command takes three arguments: the first is any valid chainer from Chai, Chai-jQuery, or Sinon-Chai, and the second and third are the aliases that we wish to compare.
The second custom command will be cy.colorLog(message, hexColor, { displayName, $el, data })
. It accepts a message as the first argument to display in the Cypress runner log, a hexadecimal color as the second argument to style the message, and a third argument which is an object that can include up to three properties: displayName
for the Cypress runner log, $el
for associating a DOM element with the log message that will appear in the console, and data
, an object containing any additional data we want to display in the console.
/// <reference types="Cypress" />
import StyleHandler from './StyleHandler'
// Example:
// cy.compareAliases('deep.equal', '@expected', '@result')
Cypress.Commands.add('compareAliases',
(chainer, alias1, alias2, options) => {
cy.get(alias2).then(aliasValue2 => {
cy.get(alias1, options).should(chainer, aliasValue2)
})
}
)
// Example:
// cy.colorLog('Not matching expected result', '#FF0000',
// { displayName: "ERROR:", data: { comments: 'Wrong!', toDo: 'Need way more practice' } })
Cypress.Commands.add('colorLog',
(message, hexColor, { displayName, $el, data = {}}) => {
const name = StyleHandler.getStyleName(hexColor)
Cypress.log({
displayName,
message,
name,
$el,
consoleProps: () => {
// return an object which will
// print to dev tools console on click
return {
displayName,
message,
name,
$el,
...data
}
},
})
}
)
You might have observed that in the index.js
file, we import a separate file named StyleHandler.js
. This file contains a helper class dedicated to generating the vibrant color styles within our Cypress runner. Its static method, getStyleName(hexColor)
, accepts a hexadecimal color as input and creates a <style>
element on the page, caching it for efficiency if that particular color hasn't been used before.
export default class StyleHandler {
static cachedStyles = new Set()
static getStyleName = (hexColor = '#FFFFFF') => {
const styleName = `colorLog${hexColor.replace("#", "-")}`
if (!StyleHandler.cachedStyles.has(styleName)) {
StyleHandler._createStyle(styleName, hexColor) // Create style element in the document
StyleHandler.cachedStyles.add(styleName) // Cache the style name
}
return styleName
}
static _createStyle = (styleName, hexColor) => {
const style = document.createElement('style')
style.textContent = `
.command.command-name-${styleName} span.command-method {
color: ${hexColor} !important;
text-transform: uppercase;
font-weight: bold;
background-color: none;
border-color: none;
}
.command.command-name-${styleName} span.command-message{
color: ${hexColor} !important;
font-weight: normal;
background-color: none;
border-color: none;
}
.command.command-name-${styleName} span.command-message strong,
.command.command-name-${styleName} span.command-message em {
color: ${hexColor} !important;
background-color: none;
border-color: none;
}
`
Cypress.$(window.top.document.head).append(style)
}
}
Both custom commands are contained within the same file, index.js
, which is designated as the package's entry point by setting the "main": "src/index.js"
property in the package.json
file. Consequently, importing our package from npm with import 'how-to-create-a-cypress-plugin'
after it's published will load both commands.
This is what our plugin project looks like so far:
But these custom commands serve distinct purposes: one facilitates assertions, while the other improves the visibility of messages in the Cypress runner log. It's possible that your Cypress project may only need the command for comparing two aliases. In such cases, you may prefer not to load the command for log enhancement when importing the plugin for your tests.
So, how can we enhance our plugin to make it more modular?
Modular Magic: Piecing Together Our Plugin Puzzle with Panache
We can make our package more modular by separating the two custom commands into different files within the src
folder.
One file could be named assertions.js
, which would contain the custom command cy.compareAliases(chainers, alias1, alias2)
:
/// <reference types="Cypress" />
// Example:
// cy.compareAliases('deep.equal', '@expected', '@result')
Cypress.Commands.add('compareAliases',
(chainer, alias1, alias2, options) => {
cy.get(alias2).then(aliasValue2 => {
cy.get(alias1, options).should(chainer, aliasValue2)
})
}
)
Another file could be titled custom-log.js
, and it would house the custom command cy.colorLog(message, hexColor, { displayName, $el, data })
:
/// <reference types="Cypress" />
import StyleHandler from './StyleHandler'
// Example:
// cy.colorLog('You did not pass the test!', '#FF0000',
// { displayName: "ERROR:", data: { comments: 'Wrong!', toDo: 'Need way more practice.' } })
Cypress.Commands.add('colorLog',
(message, hexColor, { displayName, $el, data = {}}) => {
const name = StyleHandler.getStyleName(hexColor)
Cypress.log({
displayName,
message,
name,
$el,
consoleProps: () => {
// return an object which will
// print to dev tools console on click
return {
displayName,
message,
name,
$el,
...data
}
},
})
}
)
Next, we'll update our entry point file, index.js
, by clearing its current content and replacing it with two import statements—one for each of the new files that now individually contain our custom commands:
import './assertions'
import './custom-log'
And this is what our plugin looks like now:
With these updates, using import 'how-to-create-a-cypress-plugin'
will still load both custom commands. However, we now have the added flexibility of loading each command independently in our test suites using the following import statements:
import 'how-to-create-a-cypress-plugin/src/assertions'
and
import 'how-to-create-a-cypress-plugin/src/custom-log'
Additionally, should we decide to expand our plugin with more custom assertions or additional stylized log commands in the future, we can simply add them to the appropriate source file, keeping them organized according to their distinct functions.
5. Whipping Up Witty Cypress Tests for Your Plugin!
We just got ourselves a Cypress npm plugin!
But before this plugin hits the stage of our test suites, let's put it through its own audition. Think of it as quality control's quality control—before it scrutinizes our apps, we scrutinize it. It's our mission to test the testable, and this plugin is stepping up to the plate. Let's give it the green light only after it passes our rigorous backstage warm-up!
Fixtures Fit for Fame: Prepping the Star-Studded Files for Testing
When we set up Cypress, it automatically created a file called example.json
within the fixtures folder.
We will rename this file to test-data.json
and edit its content, replacing it with the set of fixture data that we will use for our plugin tests:
{
"keanuReeves": { "martial-arts-artist": true, "cool-like-a-cucumber": true, "martial-arts": ["Judo", "Brazilian Jiu Jitsu"] },
"johnWick": { "martial-arts-artist": true, "cool-like-a-cucumber": true, "martial-arts": ["Judo", "Brazilian Jiu Jitsu"] },
"winston": { "martial-arts-artist": true, "cool-like-a-cucumber": true, "martial-arts": ["Arnis"] }
}
Now we're ready to write our stunt test scripts!
Stunt Test Scripts: Cue the Action for Our Plugin's Big Scene
The first step is to create the e2e
folder within the cypress
directory. This folder will host the end-to-end test files for our plugin.
We will create two test files: test-assertions.js
for testing our cy.compareAliases()
command, and test-custom-logs.js
for testing our custom cy.colorLog()
command.
In the test suite test-assertions.js
, we are going to create two tests: one comparing two aliases that reference objects with the same value (keanuReeves
and johnWick
), and the second test will compare two objects with similar properties but different values (johnWick
and winstonScott
):
/// <reference types="Cypress"/>
import '../../src/assertions.js'
import { keanuReeves, johnWick, winstonScott } from '../fixtures/test-data.json'
describe('Suite to showcase cy.compareAliases() command', () => {
it('Test 1 - Keanu Reeves is same as John Wick', () => {
cy.wrap(keanuReeves).as('keanuReeves')
cy.wrap(johnWick).as('johnWick')
cy.compareAliases('deep.equal','@keanuReeves', '@johnWick')
});
it('Test 2 - John Wick is not the same as Winston Scott', () => {
cy.wrap(johnWick).as('johnWick')
cy.wrap(winstonScott).as('winstonScott')
cy.compareAliases('not.deep.equal','@johnWick', '@winstonScott')
});
});
At the start of the test file, we import the custom command cy.compareAliases()
from src/assertions.js for testing, along with the test-data.json
fixture that we previously created.
When we run the tests, we can confirm that both pass: one checks a positive assertion, and the other checks a negative assertion on the aliases.
Let's now turn our attention to our second test file, test-custom-logs.js
, which is designed to verify that our Cypress command cy.colorLog()
can display in the Cypress runner log messages with different colors.
/// <reference types="Cypress"/>
import '../../src/custom-log.js'
describe('Suite to showcase cy.colorLog() command', () => {
it('Test cy.colorLog() with different colors', () => {
cy.colorLog('You have crossed the wrong line!', '#FF0000', // Red
{ displayName: "⛔ - YOU MESSED UP:", data: { toDo: 'If I were you, I would start running away right now.' } }
)
cy.colorLog('Are you sure you want to go that route?', '#FFFF00', // Yellow
{ displayName: "⚠️ - BRACE YOURSELF:", data: { toDo: 'If I were you, I would think twice.' } }
)
cy.colorLog('We are in the clear... for now.', '#00FF00', // Green
{ displayName: "✔️ - WE ARE COOL:", data: { toDo: 'Turn around and enjoy the peace while it lasts.' } }
)
});
});
In this test suite, we start by importing our custom command cy.colorLog()
from src/custom-log.js
for testing.
Upon running the test, the Cypress runner log displays three messages in their respective colors—red, yellow, and green—based on the color specified when calling the cy.colorLog()
command. Additionally, when we click on one of the colored messages in the Cypress runner log, the console reveals the additional details that we provided to cy.colorLog()
. So visually, the test has passed.
Penning the Scrolls: A Legendary Guide to Our Plugin's Lore
And now we've reached that phase every QA Engineer has a love-hate relationship with: documentation.
Oh man! Writing documentation can be tedious, right? But if you want your plugin to be a shining star, you need to tell people how to use it (even if it's just for future you).
To my knowledge, there isn't a convention for documenting a Cypress npm plugin, so it's more a matter of art and craft, as well as how much time you're willing to dedicate to the task.
However, it is recommended to place your plugin documentation in the README.md
file at the root of your Cypress package, just as you would with any other npm package. This file is the centerpiece of any documentation and should be written in Markdown.
For a Cypress npm package, the README.md
file should include information tailored to users who will be integrating the package into their Cypress testing environment. Here's what you should consider including:
Package Name: The name of the Cypress plugin or extension.
Description: A brief explanation of what the package does, with an emphasis on its value for testing with Cypress.
Installation Instructions: Step-by-step instructions for installing the package within a Cypress environment, including any necessary npm or Yarn commands.
Compatibility: Information about which versions of Cypress the package is compatible with.
Configuration: Details on how to configure the package within Cypress, if applicable.
API Reference: Documentation of any commands, options, or methods provided by the package, along with their expected parameters and return values.
Usage: Clear examples of how to use the package in Cypress tests, including how to import and apply any custom commands or fixtures.
Examples: Sample test cases or scenarios demonstrating the package's functionality within a Cypress test suite.
Contributing: Guidelines for contributing to the package, including how to report issues, submit pull requests, and any contribution requirements.
License: The type of license under which the package is released, often with a link to the full license text.
Credits and Acknowledgments: Any credits to contributors, sponsors, or related projects.
Changelog: A log of changes, bug fixes, and updates for each version of the package.
Support and Contact Information: Directions for obtaining support, such as a link to the issue tracker, discussion forums, or contact details for the maintainers.
Including this information will help ensure that users can effectively integrate and utilize the Cypress npm package within their testing frameworks.
You can see what our package documentation looks like by checking the README.md file in the GitHub project for this blog post.
Beam Up the Code: Teleportation to the GitHub Mothership
We're rock solid and brimming with confidence in our plugin—plus, it's backed by some seriously epic documentation. Time to commit and catapult it into the GitHub cosmos!
We open the Source Control panel in VS Code, select "Commit & Push" our changes upstream.
And with that, our Cypress plugin has made its grand entrance on GitHub, ready for action.
6. Launch Your Plugin Into the Wild: Publishing on npm with a Bang!
We will publish our plugin using a terminal session within our project in VS Code.
The Zesty Prep List Before the Feast
- An npm Account
Registry Razzle-Dazzle: Tuning Your Machine to the npm Groove
To ensure a smooth publishing process for your plugin, you should first verify the current registry setting in your terminal. Run the command npm get registry
to check the registry.
If the output is not the public npm registry URL, which is https://registry.npmjs.org/
, then set it to the public registry by using the command npm set registry https://registry.npmjs.org/
.
Code Command Central: Punching In Your npm Login Credentials
From the same terminal, log into your npm account with the command npm login
, and enter your credentials or the one-time password (depending on your account configuration).
If everything went well, you should see the message "Logged in on https://registry.npmjs.org/
".
Launch Codes Ready: Blasting Your Plugin into the npm Orbit
And at last, in the terminal, run the command npm publish
to release your Cypress npm plugin to the public.
You can also run npm info package-name to see the details of your published package:
Navigate to your npm account in the browser, and you should see your freshly minted package proudly listed there:
Ta-Da! Score a Win in Your Success Diary!
And finally, to bask in the glory of our achievement, verify that you can access your Cypress npm plugin in the public registry:
https://www.npmjs.com/package/how-to-create-a-cypress-plugin
ACT3: RESOLUTION
Wow, what a ride! We've officially crafted the ultimate "one-stop shop" guide on creating and publishing an end-to-end Cypress npm plugin.
Crafting a blog post as thorough and detailed as this one has truly been an epic journey!
Dive into the fully functional plugin on GitHub at https://github.com/sclavijosuero/how-to-create-a-cypress-plugin, and on npm at https://www.npmjs.com/package/how-to-create-a-cypress-plugin.
Then, seamlessly integrate it into your project by running npm install how-to-create-a-cypress-plugin --save-dev
from npm.
Don't forget to leave a comment, give a thumbs up, or follow my Cypress blog if you found this post useful.
Happy reading!
Top comments (2)
Very well written guide, Thank you was looking for this. Should be quite helpful.
I have some questions.
Thank you very much @joydeep100 !
Regarding your first question: Yes, you can indeed import the commands in a single place, such as the e2e.js file or the plugins/index.js file (personally, I prefer using e2e.js when a plugin is used in all or nearly all test suites). For the purpose of the post, I deliberately separated the commands and imports to demonstrate a scenario where a plugin might offer very diverse functionality, and you may not require all of its features for every test suite.
Regarding your second question: I believe it's possible to define a number of tasks within your plugin and then utilize them in your tests. Although I haven't tried this myself, if I were to attempt it, my approach would resemble the solution outlined in this GitHub issue: github.com/cypress-io/cypress/issu.... You would place the tasks.js file containing your custom tasks in the src folder of your plugin.
I hope this information is helpful!