Learn Cypress - Custom Plugins & Extensions
Episode 18 of 23

Learn Cypress - Custom Plugins & Extensions

This episode covers using Cypress plugins and community modules, building custom plugins for test needs, extending custom commands and task APIs, and reusable plugin patterns across projects.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Every project has needs that Cypress does not cover out of the box. Episode 18 covers the plugin ecosystem: using community plugins, building custom plugins, extending commands and task APIs, and putting together reusable plugin patterns across projects.

Plugins are the line between a rigid suite and a suite built around the team's needs.

Using Cypress Plugins and Community Modules

Finding and Installing Plugins

The Cypress plugin ecosystem is catalogued on the official site and npm. The installation pattern is consistent:

Installing a community plugin
npm install -D cypress-file-upload

npm install -D cypress-file-upload adds the upload plugin. Some plugins are commands imported in support; others are Node modules registered in the configuration.

Registering a Plugin in the Configuration

Node-level plugins are registered through the setupNodeEvents block:

JSRegistering a plugin in setupNodeEvents
module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      require("cypress-watch-and-reload")(on, config);
      return config;
    },
  },
});

setupNodeEvents(on, config) is the entry point for Node-side plugins. The on function is used by plugins to register event handlers; the config object may be modified and returned.

Building a Custom Plugin

A Node-Side Plugin with the Task API

The most common custom plugin uses the task API — calling Node code from the browser:

JSRegistering a custom task
module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on) {
      on("task", {
        seedDatabase() {
          return seedDatabase();
        },
      });
    },
  },
});

on("task", { seedDatabase() { ... } }) registers a task named seedDatabase. This task can be called from tests with cy.task("seedDatabase") as we touched on in episode 10.

A Simple Useful Task

Here is a task you can use right away: returning a consistent value from the Node side:

JSTask for test data
on("task", {
  timestampSekarang() {
    return Date.now();
  },
});

timestampSekarang() returns the server time. Tasks like this let tests use a consistent value from the Node side, rather than from a browser that can be reset.

Extending Commands and Task APIs

Extending Custom Commands

Cypress commands and the task API complement each other: commands run in the browser, tasks in Node. Combine both for powerful helpers:

JSA command that calls a task
Cypress.Commands.add("seedDanKunjungi", (fixtureName) => {
  cy.task("seedDatabase", fixtureName);
  cy.visit("/");
});

cy.task("seedDatabase", fixtureName) sends an argument to the Node task. The seedDanKunjungi command packages the set up data then open the page pattern into a single call — a composition of command and task.

Keeping Tasks Safe

Tasks run in Node with access to the file system and environment. Limit what tasks can do — never accept arbitrary paths or commands from a spec:

JSTask with a whitelist
on("task", {
  readFixture(name) {
    const whitelist = ["produk", "users"];
    if (!whitelist.includes(name)) {
      throw new Error("fixture tidak diizinkan");
    }
    return readFile(`cypress/fixtures/${name}.json`, "utf-8");
  },
});

whitelist.includes(name) limits the fixture names that may be read. Validating task input prevents a compromised spec from running dangerous operations in Node.

Reusable Plugin Patterns

A Reusable Plugin Structure

A good plugin can move between projects. Separate it as its own module:

JSReusable plugin structure
const { defineConfig } = require("cypress");
const seedPlugin = require("./plugins/seed");
 
module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      seedPlugin(on, config);
      return config;
    },
  },
});

require("./plugins/seed") loads a separate plugin module. seedPlugin(on, config) registers tasks from that module — this pattern makes plugins shareable and testable on their own.

Documentation and Versioning

Give custom plugins a short documentation: what they register, the arguments they accept, and usage examples. If a plugin is used across projects, release it as an npm package with its own versioning so changes do not surprise other teams.

Info

Before writing a custom plugin, check whether the need already exists in a maintained community plugin. Writing your own is worthwhile for specific needs, but a mature general-purpose plugin saves time and maintenance.

Closing

Episode 18 opened the door to the ecosystem: using community plugins, building custom plugins through the task API and setupNodeEvents, extending commands and tasks, and putting together reusable plugin patterns with validation and documentation.

Key takeaways:

  • Command plugins are imported in support; Node plugins are registered in setupNodeEvents.
  • The task API calls Node code from the browser via cy.task.
  • Tasks can accept arguments; combine them with custom commands.
  • Validate task input to keep Node operations safe.
  • Separate plugins as modules so they can be reused across projects.

In the next episode, episode 19, we will cover operational readiness and runbooks — runbooks for flaky tests and environment drift, incident response for failing CI runs, maintaining test coverage and health, and team ownership and maintenance routines.

Learn Cypress - Custom Plugins & Extensions | Learn Cypress