In my previous blog post Writing a Custom Cypress Command I have shown a simple custom Cypress custom command for finding a form element by its label. In this blog post I will show how to correctly publish this custom command to the NPM registry.
- The command
- Continuous integration
- Command source refactoring
- Publishing to NPM
- Better registration
- Using an example
- Types
- ES6 import and export
- Examples
🔎 You can find the source code for this blog post in bahmutov/cypress-get-by-label repo.
The command
I usually start by developing the code in-place and writing the custom command in the spec file. Given an HTML file:
1 | <body> |
The following spec file tests the custom command
1 | // enables intelligent code completion for Cypress commands |
The spec passes.
Continuous integration
As soon as we have the first test, I set up continuous integration pipeline. I can pick amongst various CI providers, but perhaps GitHub Actions is the simplest to set up right now. We can run Cypress via cypress-io/github-action with ease:
1 | name: ci |
The test passes on CI.
Command source refactoring
Now let's refactor our command to be usable from any spec. Our users should be able to register the custom command in a particular spec or in all specs by importing the command from their support file. Let's move the command source code into its own file src/index.js
1 | Cypress.Commands.add('getByLabel', (label) => { |
The spec file requires the src/index.js
file
1 | require('../../src') |
Great, but when we distribute this NPM package, our users will require it by name. Thus instead of require('../../src/index')
let's require the top level package folder. We can set the main
field to point at the src/index
in our package.json
file. While we are there, let's set the list of files to be published to NPM, our users only will need the src
folder.
1 | { |
1 | require('../..') |
Time to publish!
Publishing to NPM
Let's verify the files we are about to publish to NPM
1 | $ npm pack --dry |
So we are going to include three files in the distributable archive: the README file, the package.json
, and src/index.js
. Seems right.
As always I will use semantic-release following How I publish to NPM approach. Since GitHub Actions will have GH token set, we can grab a new NPM token using the command:
1 | $ semantic-release-cli setup --gh-token foo |
Set the NPM_TOKEN
as a secret when running the GitHub Action
To do semantic release, I like using cycjimmy/semantic-release-action. The ci.yml
file is now:
1 | name: ci |
In order to actually release, let's update our README file. We need to explain how to use this package and commit with a feature or fix release.
1 | $ git status |
The GitHub action runs and publishes "cypress-get-by-label v1.0.0", which we can find at www.npmjs.com/package/cypress-get-by-label.
Better registration
Currently we have published our command that registers itself immediately whenever some runs require('cypress-get-by-label')
code. What if we want to customize the custom command? What if we want to register it under a different name? It makes sense to export a registration function, instead of immediately modifying the global state by running Cypress.Commands.add('getByLabel', ...)
function.
Let's modify our package.
1 | const registerCommand = (name = 'getByLabel') => { |
Let's update our spec file to test this:
1 | const { registerCommand } = require('../..') |
The commands work, notice both "getByLabel" and "getFormField" log messages.
Because we have changed the public API of our package, we have to publish this change as a breaking change. Since we use the semantic versioning that analyzes the commit messages, we can simply do:
1 | $ git commit |
Semantic release publishes v2
Oops, we forgot to update the README when we published the new release.
## use
Include from your Cypress support file or individual spec
const { registerCommand } = require('cypress-get-by-label')
registerCommand()
// or we could register under a different name
registerCommand('getFormField')
Then use the command `cy.getByLabel` (default) or the custom name
// if used registerCommand()
cy.getByLabel('First name:')
// if used registerCommand('getFormField')
cy.getFormField('First name:')
We should commit this change with fix: ...
prefix. For example
1 | $ git commit -m "fix: update readme for v2" |
Which publishes v2.0.1 of the package.
Using an example
At this point I like creating a separate repo to show the custom command in action and to verify everything works when a user tries to install and use cypress-use-by-label
. You can find my example in the repo bahmutov/cypress-get-by-label-example.
When installing the new package, I recommend following the README file, just to "test" the installation instructions.
1 | $ npm i -D cypress-get-by-label |
In the spec file (I have copied the spec and example from cypress-get-by-label
) I will use the name of the package.
1 | // cypress-get-by-label-example |
The example works. We can set up the CI, RenovateBot, and even self-updating README badges to keep this example repo up-to-date.
I then link the new repo from the cypress-get-by-label README file
Types
When the user uses the cy.getByLabel
command, there is no IntelliSense information, unlike cy.visit
and other built-in Cypress commands.
Note: we are only going to provide type for the default cy.getByLabel
command, and we also assume command has been registered by the user's test code. If the user uses a different command name, they could write a similar typescript file locally.
Let's follow Cypress TypeScript guide and add src/index.d.ts
file
1 | // load type definitions that come with Cypress module |
Now we need to point at this types file from other specs and distribute this file with the source code.
Types in the project
To tell your specs about your new custom command, instead of loading the reference types="cypress"
from the spec file.
1 | - /// <reference types="cypress" /> |
Notice how we switched from loading types from node_modules/cypress
folder to a relative path using reference path=...
.
Types for other projects
In order to describe the types for the default cy.getByLabel
command we need to include types file with our NPM package. In the package.json
file add the "types" field pointing at the src/index.d.ts
file
1 | { |
Publish the new version of the package - it is a new feature, so it becomes v2.1.0, and the example project should use the types by loading them like this:
Global plus exports
Sometimes the module registers custom Cypress commands AND exports functions. To make sure it is working and to avoid file index.d.ts is not a module
error, I had to create two .d.ts
files
1 | // import the custom Cypress commands provided by this module |
The globals.d.ts
file contains the Cypress namespace where we can add the custom commands
1 | // load type definitions that come with Cypress module |
See the plugin cypress-slow-down
ES6 import and export
If we register the custom command, and try to avoid modifying the global state using require
, we might as well declare our registration function using the ES6 export
keyword.
1 | export const registerCommand = (name = 'getByLabel') => { |
The cypress-get-by-label
project still works like before. Let's publish our change, it becomes v2.2.0.
Let's upgrade cypress-get-by-label-example
to use v2.2.0 - all working the same. The top-level export
keyword did not affect the project. We can also replace the require
with import
keyword in the user project
1 | // const { registerCommand } = require('cypress-get-by-label') |
Even if we refactor the internals of cypress-get-by-label
to use import
and export
keywords, the user package should be able to bundle them correctly using Cypress v6+.
Examples
You can find example Cypress commands in the following modules: