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.