DEV Community

Cover image for The Quirky Guide to Crafting and Publishing Your Cypress NPM Plugin
Sebastian Clavijo Suero
Sebastian Clavijo Suero

Posted on

The Quirky Guide to Crafting and Publishing Your Cypress NPM Plugin

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!

 

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.

Image description

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.

Image description

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.

Image description

 

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.

Image description

Image description

Image description

If the login is successful, you will see your GitHub user reflected in VS Code.

Image description

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".

Image description

Image description

Image description

If the repository is cloned successfully, a new VS Code window will open, displaying the contents of your GitHub repository in the Explorer panel.

Image description

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

Image description

If everything proceeds as expected, a file named package.json will be created at the root of our project, containing all the information provided.

Image description

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.

Image description

Cypress Setup: So Slick, It's Almost Sneaky

To start configuring Cypress, run the command npx cypress open in the terminal.

Image description

Afterward, simply follow and accept the prompts in the configuration screens:

Image description

Image description

This process will create the cypress.config.js file and the cypress folder, which contains several subfolders and configuration files.

Image description

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.

Image description

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/.

Image description

 

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
                }
            },
        })
    }
)
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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)
        })
    }
)
Enter fullscreen mode Exit fullscreen mode

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
                }
            },
        })
    }
)

Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

And this is what our plugin looks like now:

Image description

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"] }
}
Enter fullscreen mode Exit fullscreen mode

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')
    });
});
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.' } }
        )
    });
});
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.

Image description

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.

Image description

Image description

And with that, our Cypress plugin has made its grand entrance on GitHub, ready for action.

Image description

 

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/.

Image description

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).

Image description

Image description

Image description

If everything went well, you should see the message "Logged in on https://registry.npmjs.org/".

Image description

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:

Image description

Navigate to your NPM account in the browser, and you should see your freshly minted package proudly listed there:

Image description

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

Image description

 


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)

Collapse
 
joydeep100 profile image
Joydeep D

Very well written guide, Thank you was looking for this. Should be quite helpful.
I have some questions.

  • I see that you have imported some of the src into spec file, is it possible to import in a single place like command.js or plugins/index.js and not be required to import in every spec file.
  • secondly, how do we go about if we were to write a new cy.task() and use that.
Collapse
 
sebastianclavijo profile image
Sebastian Clavijo Suero

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!