Guides

Test verification emails with Cypress

Drive email verification flows from Cypress specs with cy.task and the SDK.

Cypress runs spec code inside the browser, so the Node SDK cannot be imported in a spec file. Register a few cy.task handlers in setupNodeEvents instead: the tasks own the SDK in the config's Node process, and specs call them with plain values.

Start the services

Start InboxTap and the application before the Cypress run, locally and in CI:

npx inboxtap &
npm run dev &
npx cypress run

Configure the application under test to send SMTP to localhost:1025.

Register the tasks

// cypress.config.ts
import { defineConfig } from "cypress";
import { InboxTapClient, TestInbox } from "inboxtap/client";

const inboxTap = new InboxTapClient();

interface WaitArgs {
  to: string;
  subject?: string;
  contains?: string;
  pattern?: string;
  timeoutMs?: number;
}

export default defineConfig({
  e2e: {
    setupNodeEvents(on) {
      on("task", {
        "inboxtap:createInbox": async (alias: string) =>
          (await inboxTap.createInbox({ alias })).address,
        "inboxtap:waitForLink": ({ to, ...options }: WaitArgs) =>
          new TestInbox(inboxTap, to).waitForLink(options),
        "inboxtap:waitForCode": ({ to, ...options }: WaitArgs) =>
          new TestInbox(inboxTap, to).waitForCode(options),
      });
    },
  },
});

The create task returns the inbox's address as a string, and each wait task rebuilds a TestInbox from the address it receives.

Write the test

describe("sign up", () => {
  it("verifies a new account", () => {
    cy.task<string>("inboxtap:createInbox", "signup").then((address) => {
      cy.visit("/signup");
      cy.get("input[name=email]").type(address);
      cy.contains("button", "Create account").click();

      cy.task<string>(
        "inboxtap:waitForLink",
        { to: address, subject: "verify your email", contains: "/verify", timeoutMs: 20_000 },
        { timeout: 30_000 },
      ).then((url) => {
        cy.visit(url);
        cy.contains("Email verified").should("be.visible");
      });
    });
  });
});

Give the Cypress timeout option more time than timeoutMs, so a slow email fails inside the task with InboxTap's error message rather than as a generic task timeout. Use the inboxtap:waitForCode task when the application asks for an OTP instead of a link.

Parallel isolation

Create the inbox inside each test, never once for the whole suite. Every task call generates a fresh recipient address, so parallel specs can share one InboxTap server without reading one another's messages. The bounded store evicts old messages on its own; add a clear task only if immediate cleanup helps local debugging.

Direct HTTP access

cy.request executes from Cypress's Node-side proxy, so it can reach the loopback API directly with no task registration:

cy.request(
  "http://localhost:8025/api/emails/wait?to=signup-1@local.test&timeoutMs=20000",
)
  .its("body.email.links")
  .should("not.be.empty");

This trades away the SDK's multi-message scanning and value extraction, so prefer the tasks for anything beyond a one-off check. The full endpoint list is in the HTTP API reference.