ZERODROP
← INTEGRATIONSDOCS
CypressClerkGitHub Actions

Testing Clerk Email Flows with Cypress

Clerk sends OTP codes for email verification. Testing these flows end-to-end in Cypress CI requires catching real emails. Here's the complete setup — including the Clerk OTP input trick.

The problem

Clerk's OTP UI renders individual number inputs — one per digit. Most Cypress guides skip this flow entirely or bypass it with Clerk's backend API. ZeroDrop gives you the real email so you can test the actual UI without bypassing anything.

Install

npm install zerodrop-client

Testing Clerk OTP in Cypress

The key trick: Clerk's OTP renders as input[name="code-0"] through input[name="code-5"]. Type each digit individually.

// cypress/e2e/clerk-auth.cy.js
import { ZeroDrop } from 'zerodrop-client';

const mail = new ZeroDrop();

describe('Clerk OTP verification', () => {
  it('user can sign up and verify email', () => {
    const inbox = Cypress.env('TEST_INBOX') ?? mail.generateInbox();

    // 1. Navigate to sign-up
    cy.visit('/sign-up');

    // 2. Fill email — Clerk uses "identifier" field
    cy.get('input[name="identifier"]').type(inbox);
    cy.contains('button', 'Continue').click();

    // 3. Wait for OTP input to appear
    cy.get('input[name="code-0"]').should('be.visible');

    // 4. Catch the OTP email
    cy.wrap(mail.waitForLatest(inbox, { timeout: 30000 }))
      .then((email) => {
        expect(email.otp).to.not.be.null;

        // 5. Type each digit into Clerk's individual inputs
        const digits = email.otp.split('');
        digits.forEach((digit, index) => {
          cy.get(`input[name="code-${index}"]`).type(digit);
        });

        // 6. Should be signed in
        cy.url().should('include', '/dashboard');
      });
  });
});

Testing Clerk magic link in Cypress

it('user can sign in with magic link', () => {
  const inbox = Cypress.env('TEST_INBOX') ?? mail.generateInbox();

  cy.visit('/sign-in');
  cy.get('input[name="identifier"]').type(inbox);
  cy.contains('button', 'Continue').click();

  cy.wrap(mail.waitForLatest(inbox, { timeout: 30000 }))
    .then((email) => {
      expect(email.magicLink).to.not.be.null;

      // Visit the magic link
      cy.visit(email.magicLink);

      // Should be signed in
      cy.url().should('include', '/dashboard');
    });
});

cypress.config.js

const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    defaultCommandTimeout: 10000,
    pageLoadTimeout: 60000,
    env: {
      // TEST_INBOX injected by GitHub Action
    },
  },
});

GitHub Actions workflow

name: E2E Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci

      - name: Generate test inbox
        id: inbox
        uses: zerodrop-dev/create-inbox@8706a59 # v1.0.0

      - name: Run Cypress tests
        run: npx cypress run
        env:
          CYPRESS_TEST_INBOX: ${{ steps.inbox.outputs.inbox }}
          NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
          CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
          CYPRESS_BASE_URL: ${{ secrets.STAGING_URL }}

What you're actually testing

Clerk's email delivery is working in your environment
The OTP code arrives and is correctly formatted
Clerk's individual digit inputs accept the code
The session is created after OTP verification
The user is redirected to the correct page

Ready to test your Clerk flows in Cypress?

Free tier. No signup. No Docker. Works in CI in 5 minutes.

OPEN INBOX →

RELATED GUIDES

Playwright + ClerkPlaywright + NextAuthPlaywright + Auth0Full Cypress guide