DEV Community 👩‍💻👨‍💻

Murat K Ozcan
Murat K Ozcan

Posted on • Updated on

How to create internal test plugins for your team in TS, implement custom commands, and use other Cypress plugins in them

Assume you are in a large organization with many services and applications. You are cranking out Cypress ui-e2e and api-e2e tests. Suddenly you realize lots of code is being duplicated between the teams for common tasks; setup, teardown, authentication, or anything pertaining to your domain.

One solution is to be in a monorepo. Another is to have the tests in a repo of its own. While monorepo can be done right - I have not been in any team that has succeeded with it - the latter is an anti-pattern you want to stay away from for many reasons. Leaving that discussion aside, we will assume you are a wise engineer, you want individual repositories and you want the tests living near the source code.

You know how to create Cypress commands, and you wish you knew how to publish these internally as a Cypress plugin, so that other teams do not duplicate any test code and easily abstract away the complexities. But, you are a large org and may be using Typescript - which makes it all more difficult - and you may be using many other Cypress plugins in your Cypress commands, because plugins are great.

You have a situation of plugins within plugins, plans within plans, problems within problems.

Image description

The example used is a hypothetical test suite for an authentication service. The tests are api tests. You can find the repo at https://github.com/muratkeremozcan/cypress-test-plugin-example and use it as a template for your future needs. It is also a good example for setting up industry best practices like eslint, prettier, husky, Renovate, badges etc.

At Extend we have created multiple internal test plugins following these patterns.

Create & configure the repo

You can replicate most of these settings from the sample repo.

npm init -y

Set these package.json properties:

"main": "src",
"types": "src",
"files": [ "**/*.js", "index.d.ts" ],
Enter fullscreen mode Exit fullscreen mode

Add scripts, dependencies, devDependencies.

Setup generic repo configuration: eslintrc.js, .nvmrc, prettier, husky, .vscode/settings.json, renovate.json.

Rename master to main:

git branch -m master main
# at github/.../settings/branches, switch default branch to main
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Install & open Cypress, implement the commands, tests,cypress.json,cypress.env.json, plugins/index.js like usual. Start with the default typescript settings. Add secrets to github if using cypress.env.json.

Setup badges

Renovate badges:

Add anywhere on your readme, usually the bottom.

[renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg
[renovate-app]: https://renovateapp.com/
Enter fullscreen mode Exit fullscreen mode

Badges for your package.json dependencies

npx -p just installs it on the fly_

npx -p dependency-version-badge update-badge <name-of-package> <other-package>

Badge for the repo itself

You can use Github to create a badge: Actions > Workflows > click on a workflow > upper right ... > Create Status badge. This will make a badge out of the status of a yml file. Add the query param ?branch=main&event=push to it to make it only report on main branch's status

[![yml-file-name](url-to-worfklow-yml-file/badge.svg?branch=main&event=push)](url-to-worfklow-yml-file)

[![cypress-test-plugin-example](https://github.com/muratkeremozcan/cypress-test-plugin-example/actions/workflows/cypress-test-plugin-example.yml/badge.svg?branch=main&event=push)](https://github.com/muratkeremozcan/cypress-test-plugin-example/actions/workflows/cypress-test-plugin-example.yml)
Enter fullscreen mode Exit fullscreen mode

Badge for cypress dashboard (only works with public dashboards)

[![any-name-usually-your-package-dashboard](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/<your-project-id>/main&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/<your-project-id>/runs)
Enter fullscreen mode Exit fullscreen mode

Command Resource Refactoring

If all is working well at this point, Cypress commands have been implemented, you have specs, the repo is setup to your liking, badges are good, push the changes up then proceed.

At the repo root, create a src folder. Move cypress/support/ folder contents to it. Also move the type definitions at cypress/index.d.ts to the root of the src folder.

cypress/support/index.js

Create this file with a one-liner import. This will import the top level package folder. We previously set package.json "main" to "src" for this reason.

import '../..'
Enter fullscreen mode Exit fullscreen mode

Typescript settings

./src/tsconfig.json

Create a src/tsconfig.json.
Include all the plugins types you are using in the command files, and include the folder itself with "./"

"compilerOptions": {
  "types": ["@bahmutov/cy-api", "./"]
},
"include": ["**/*.ts"],
"extends": "../tsconfig.json"
Enter fullscreen mode Exit fullscreen mode

./src/index.ts

Rename the index.js file to index.ts. Import all the plugins here.

Convert all the Cypress commands to functions, export the ones that will be used as commands.

Import them at index.ts , and wrap them in Cypress commands.

Not only this gives us cleaner looking functions, but also makes our api more obvious.

import "@bahmutov/cy-api/support";
import "cypress-data-session";

import { getToken, me, createUser } from "./commands/generic";
import { maybeGetToken } from "./commands/data-session";

Cypress.Commands.add("getToken", getToken);
Cypress.Commands.add("me", me);
Cypress.Commands.add("createUser", createUser);
Cypress.Commands.add("maybeGetToken", maybeGetToken);
Enter fullscreen mode Exit fullscreen mode

./cypress/tsconfig.json

The spec files will use Cypress types, and the types from the ./src. We might also need additional types for independent usage of the plugins in the specs, for example cypress-data-session is used to clear the data session in the specs so we need the types here.

  "compilerOptions": {
    "types": ["cypress", "cypress-data-session", "../src/"]
  },
  "include": ["**/*.ts"],
  "extends": "../tsconfig.json"
Enter fullscreen mode Exit fullscreen mode

./tsconfig.json

Update tsconfig.json at the project root to include the types from ./src.

In order to run typecheck, you may need to have a subset of the types at the root tsconfig.json. Copy the types from one of the other 2 jsons and follow a process of elimination.

"types": [
  "cypress",
  "node",
  "@bahmutov/cy-api",
  "cypress-data-session",
  "./src"
]
Enter fullscreen mode Exit fullscreen mode

Release

Build the app (dist folder)

We need to complie the TS into JS.

tsc --skipLibCheck
Enter fullscreen mode Exit fullscreen mode

Update the package version at ./package.json

yarn version
Enter fullscreen mode Exit fullscreen mode

Create a ./package.json copy in dist folder, also copy the types and dependency types to the dist folder

For this we will write a script at ./scripts/release.js

const fs = require("fs-extra");
const path = require("path");
const packageJson = require("../package.json");

const newPackage = {
  ...packageJson,
};

// whether dist/package.json exists or not, get it created and synced with root package.json
fs.outputFileSync(
  path.resolve(__dirname, "..", "dist", "package.json"),
  JSON.stringify(newPackage, null, 2)
);

// copy the types and dependency types
fs.copy(
  path.resolve(__dirname, "..", "src", "index.d.ts"),
  path.resolve(__dirname, "..", "dist", "index.d.ts")
);

// if your types need additional files, copy them too
fs.copy(
  path.resolve(__dirname, "..", "src", "api-version.ts"),
  path.resolve(__dirname, "..", "dist", "api-version.ts")
);
Enter fullscreen mode Exit fullscreen mode

Publish

Publish step is different per environment, because of internal registry needs.

Internally we use AWS CodeArtifact, and all it takes to release is yarn publish dist. For publishing to npm, take a look at Gleb's instructions.

Create a ./package.json script out of these commands

"build": "tsc --skipLibCheck",
"release": "yarn build && yarn version && node scripts/release.js && yarn publish dist"
Enter fullscreen mode Exit fullscreen mode

Run the script

# builds the app (dist folder)
# updates package version in the root package.json
# creates a package.json copy at dist folder
# copies the types and dependency types to the dist folder
# publishes to AWS CodeArtifact
yarn release
Enter fullscreen mode Exit fullscreen mode

This plugin has not been released anywhere at the time of writing. The first person who releases to a registry gets to do it there for the first time!

Install the plugin at another repo

yarn add -D cypress-test-plugin-example
# or
npm i -D cypress-test-plugin-example
Enter fullscreen mode Exit fullscreen mode

Setup the types at cypress/tsconfig.json :

{
  "compilerOptions": {
    "types": ["cypress", "cypress-test-plugin-example"],
    "target": "esnext",
    "lib": ["esnext", "dom"],
    "allowJs": true,
    "resolveJsonModule": true
  },
  "include": ["**/*.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Import the package at cypress/support/index.js :

import cypress-test-plugin-example
Enter fullscreen mode Exit fullscreen mode

Try out the commands! You can copy the specs from this repo's only spec and see how they work. The commands you have implemented in your plugin should work just the same way.

References

https://glebbahmutov.com/blog/publishing-cypress-command/ - Gleb Bahmutov

https://github.com/bahmutov/cypress-get-by-label JS JS - Gleb Bahmutov

https://github.com/dmtrKovalenko/cypress-real-events TS - Dmitry Kovalenko

Top comments (0)

12 Rarely Used Javascript APIs You Need

>> Check out this classic DEV post <<