Las specs de Cypress se ejecutan dentro del navegador, pero un cliente HTTP que crea bandejas y sondea el correo pertenece a Node — así que el patrón aquí es una cy.task() que envuelve otpbox-sdk, llamada desde una spec por lo demás normal. Sin proveedor de correo simulado, sin una bandeja compartida por la que compitan dos archivos de specs en paralelo.
cypress-plugin/otpbox-cypress (un wrapper de cy.task/comandos ya listo), pero todavía no está publicado en npm. Esta guía usa otpbox-sdk directamente, que ya está en npm y es todo lo que necesita una cy.task.cy.request()cy.request() puede llamar a la API REST de OTPBox directamente y funcionaría para crear una bandeja una sola vez. Pero esperar un correo implica sondeo, y una cy.task() que se ejecuta en Node te permite usar waitForOtp() de otpbox-sdk tal cual, en lugar de escribir a mano un bucle de reintentos en la spec del lado del navegador. También mantiene tu clave de API completamente fuera del contexto del navegador.
npm install otpbox-sdk
cypress.config.tsimport { defineConfig } from 'cypress';
import { OTPBox } from 'otpbox-sdk';
export default defineConfig({
e2e: {
setupNodeEvents(on) {
const client = new OTPBox({ apiKey: process.env.OTPBOX_KEY! });
let currentInboxId: string | null = null;
on('task', {
async otpboxCreateInbox() {
const inbox = await client.createInbox();
currentInboxId = inbox.id;
return inbox; // { id, address, domain, expiresAt }
},
async otpboxWaitForOtp(inboxId: string) {
return client.waitForOtp(inboxId, { timeoutMs: 20_000 });
},
async otpboxDeleteInbox(inboxId: string) {
await client.deleteInbox(inboxId);
return null;
},
});
},
},
});
Define OTPBOX_KEY en tu shell, en el almacén de secretos de CI, o en un cypress.env.json ignorado por git — nunca lo subas al repositorio. ¿Sin clave? otpbox-sdk no tiene un ayudante de generación automática propio; genera una con curl -X POST https://otpbox.io/api/v1/keys/free (ver Obtener una clave) y expórtala, o usa una clave de organización desde el panel para CI.
describe('sign up', () => {
it('verifies with a real OTP', () => {
cy.task('otpboxCreateInbox').then((inbox: any) => {
cy.wrap(inbox.id).as('inboxId');
cy.visit('https://your-app.example.com/signup');
cy.get('[name="email"]').type(inbox.address);
cy.get('button[type="submit"]').click();
// Blocks Node-side until the SDK sees a message with an extracted code
cy.task('otpboxWaitForOtp', inbox.id, { timeout: 25000 }).then((code) => {
expect(code).to.be.a('string');
cy.get('[name="otp"]').type(code as string);
cy.get('button[type="submit"]').click();
cy.contains('Welcome').should('be.visible');
});
});
});
afterEach(function () {
if (this.inboxId) cy.task('otpboxDeleteInbox', this.inboxId);
});
});
Dos particularidades de Cypress que vale la pena mencionar: pasa una opción timeout por task a cy.task() que sea mayor que el timeoutMs propio del SDK (el timeout de task por defecto de Cypress es 60 segundos, pero conviene ser explícito), y usa this.inboxId/cy.wrap().as() en lugar de una variable a nivel de módulo, ya que los comandos de Cypress son asíncronos y se encolan en lugar de esperarse (await) directamente.
Algunos flujos de registro envían un enlace de confirmación en lugar de un código de 6 dígitos. Añade una task más junto a las demás, y luego visita la URL que devuelve:
// cypress.config.ts, inside setupNodeEvents
on('task', {
// ...otpboxCreateInbox, otpboxDeleteInbox as above...
async otpboxWaitForLink(inboxId: string) {
const message = await client.waitForEmail(inboxId, { timeoutMs: 20_000 });
if (!message) return null;
const full = await client.getMessage(message.id);
return full.link?.url ?? null;
},
});
cy.task('otpboxWaitForLink', inbox.id).then((url: any) => {
expect(url).to.be.a('string');
cy.visit(url);
cy.contains('Verified').should('be.visible');
});
Una vez que hayas escrito el mismo par cy.task('otpboxCreateInbox') / cy.task('otpboxWaitForOtp', ...) en más de una spec, vale la pena convertirlos en comandos personalizados en cypress/support/commands.ts para que las specs se lean como inglés (o español) llano:
Cypress.Commands.add('otpboxSignUp', (email: string) => {
cy.get('[name="email"]').type(email);
cy.get('button[type="submit"]').click();
});
Cypress.Commands.add('otpboxEnterCode', (inboxId: string) => {
cy.task('otpboxWaitForOtp', inboxId, { timeout: 25000 }).then((code: any) => {
cy.get('[name="otp"]').type(code);
cy.get('button[type="submit"]').click();
});
});
Esto es solo una capa fina de conveniencia sobre las dos mismas tasks — nada cambia en el lado de OTPBox, es puramente para mantener los archivos de specs legibles una vez que varias pruebas necesitan la misma secuencia de registro y verificación.
OTPBOX_KEY como variable de CI enmascarada/protegida — ver CI/CD para ejemplos con GitHub Actions, GitLab CI, Jenkins y CircleCI (los mismos pasos de inyección de variables aplican sin importar qué runner de pruebas consuma la clave).429 quota_exceeded.otpboxCreateInbox con una clave de idempotencia si una red de CI inestable hace que tu task reintente — ver Claves de idempotencia — pasando una opción idempotencyKey como segundo argumento de client.createInbox().afterEach/after como se muestra arriba; de todos modos, lo que quede se limpia con el cron de expiración de 15 minutos.¿Listo para probarlo en tu propia app? Crea una cuenta gratuita
← Volver a OTPBox