Integraciones

Cypress + CaptchaAI: Pruebas E2E con manejo de CAPTCHA

Para probar de extremo a extremo un formulario protegido por CAPTCHA sin apagar esa protección, necesitas que Cypress obtenga un token válido en tiempo de ejecución y lo inyecte antes de enviar. CaptchaAI resuelve reCAPTCHA v2 y Cloudflare Turnstile por API y te devuelve ese token, así tu suite E2E corre contra el mismo flujo que verá el usuario final. El resultado: staging deja de divergir de producción y desaparecen los bugs que solo aparecían con el CAPTCHA activo.


¿Por qué no basta con desactivar los CAPTCHA en las pruebas?

Es la salida rápida y también la que oculta fallos reales: al quitar el CAPTCHA, tus pruebas dejan de ejercitar la inyección de token y el callback que sí corren en producción, así que el flujo que validas no es el que despliegas.

Enfoque Riesgo
Deshabilitar CAPTCHA en staging Omite errores de integración y diferencias en el flujo de formularios
Usar claves de prueba (siempre pasa) No prueba la inyección de token ni el manejo del callback
Resolver con CaptchaAI Prueba completa con paridad de producción

Piensa en el equipo de QA de una fintech en Bogotá que valida su checkout antes de cada release: con el CAPTCHA apagado, el primer sitio donde se ejercita de verdad la validación del token es producción. Resolverlo en la suite E2E mueve ese descubrimiento a la pull request.

Al resolver el CAPTCHA de verdad, tu suite ejercita tres piezas que las claves de prueba nunca tocan:

  • La inyección del token en #g-recaptcha-response y en los campos ocultos del formulario.
  • El callback de ___grecaptcha_cfg, que muchos formularios usan para habilitar el botón de envío.
  • La validación en el backend, que rechaza tokens caducados o emitidos para otro sitekey.

Instalación y configuración inicial

Empieza por añadir Cypress como dependencia de desarrollo en tu proyecto:

npm install cypress --save-dev

Configuración de Cypress

// cypress.config.js
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    baseUrl: "https://your-app.com",
    defaultCommandTimeout: 120000,
    responseTimeout: 120000,
    setupNodeEvents(on, config) {
      on("task", {
        solveCaptcha({ siteUrl, sitekey, type }) {
          return solveCaptchaTask(siteUrl, sitekey, type);
        },
      });
      return config;
    },
  },
  env: {
    CAPTCHAAI_KEY: "YOUR_API_KEY",
  },
});

Los timeouts largos (120000 ms) no son opcionales: la resolución tarda segundos.


El manejador de tareas de CaptchaAI

Por las restricciones de CORS, la llamada a la API vive en una task de Node. Este módulo envía el trabajo a in.php, sondea res.php hasta que el token está listo y lo devuelve:

// cypress/plugins/captcha-solver.js
const https = require("https");

function httpPost(url, data) {
  return new Promise((resolve, reject) => {
    const params = new URLSearchParams(data).toString();
    const options = {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
    };
    const req = https.request(url, options, (res) => {
      let body = "";
      res.on("data", (c) => (body += c));
      res.on("end", () => resolve(JSON.parse(body)));
    });
    req.on("error", reject);
    req.write(params);
    req.end();
  });
}

function httpGet(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let body = "";
      res.on("data", (c) => (body += c));
      res.on("end", () => resolve(JSON.parse(body)));
    }).on("error", reject);
  });
}

async function solveCaptchaTask(siteUrl, sitekey, type = "recaptcha_v2") {
  const API = "https://ocr.captchaai.com";
  const key = process.env.CAPTCHAAI_KEY || "YOUR_API_KEY";

  const submitData = {
    key,
    pageurl: siteUrl,
    json: "1",
  };

  if (type === "turnstile") {
    submitData.method = "turnstile";
    submitData.sitekey = sitekey;
  } else {
    submitData.method = "userrecaptcha";
    submitData.googlekey = sitekey;
  }

  const submitResp = await httpPost(`${API}/in.php`, submitData);

  if (submitResp.status !== 1) {
    throw new Error(`Submit failed: ${submitResp.request}`);
  }

  const taskId = submitResp.request;

  // Poll for result
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));

    const params = new URLSearchParams({
      key,
      action: "get",
      id: taskId,
      json: "1",
    });

    const result = await httpGet(`${API}/res.php?${params}`);

    if (result.request === "CAPCHA_NOT_READY") continue;
    if (result.status !== 1) throw new Error(`Solve failed: ${result.request}`);

    return result.request; // The CAPTCHA token
  }

  throw new Error("CAPTCHA solve timeout");
}

module.exports = { solveCaptchaTask };

Conéctalo a cypress.config.js

// cypress.config.js
const { solveCaptchaTask } = require("./cypress/plugins/captcha-solver");

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on("task", {
        solveCaptcha({ siteUrl, sitekey, type }) {
          return solveCaptchaTask(siteUrl, sitekey, type);
        },
      });
    },
  },
});

Comandos personalizados de Cypress

Con la task registrada, envuélvela en comandos reutilizables. solveCaptcha lee el sitekey del DOM, pide el token, lo escribe en #g-recaptcha-response y dispara el callback de ___grecaptcha_cfg. solveTurnstile hace lo mismo con cf-turnstile-response:

// cypress/support/commands.js

Cypress.Commands.add("solveCaptcha", (options = {}) => {
  cy.get("[data-sitekey]", { timeout: 10000 }).then(($el) => {
    const sitekey = options.sitekey || $el.attr("data-sitekey");
    const siteUrl = options.siteUrl || cy.url();

    cy.url().then((url) => {
      cy.task("solveCaptcha", {
        siteUrl: url,
        sitekey,
        type: options.type || "recaptcha_v2",
      }).then((token) => {
        // Inject token
        cy.window().then((win) => {
          const responseEl = win.document.querySelector(
            "#g-recaptcha-response"
          );
          if (responseEl) {
            responseEl.value = token;
          }

          // Set all hidden response fields
          win.document
            .querySelectorAll('[name="g-recaptcha-response"]')
            .forEach((el) => {
              el.value = token;
            });

          // Trigger callback if exists
          if (win.___grecaptcha_cfg) {
            const clients = win.___grecaptcha_cfg.clients;
            for (const key in clients) {
              const client = clients[key];
              if (client && typeof client.callback === "function") {
                client.callback(token);
              }
            }
          }
        });
      });
    });
  });
});

Cypress.Commands.add("solveTurnstile", (options = {}) => {
  cy.get("[data-sitekey]", { timeout: 10000 }).then(($el) => {
    const sitekey = options.sitekey || $el.attr("data-sitekey");

    cy.url().then((url) => {
      cy.task("solveCaptcha", {
        siteUrl: url,
        sitekey,
        type: "turnstile",
      }).then((token) => {
        cy.window().then((win) => {
          const input = win.document.querySelector(
            'input[name="cf-turnstile-response"]'
          );
          if (input) input.value = token;
        });
      });
    });
  });
});

Ejemplos de pruebas E2E

Estas pruebas reutilizan los comandos anteriores sobre los flujos más habituales.

Inicio de sesión con reCAPTCHA v2

El caso más común es un formulario de login protegido con reCAPTCHA v2: resuelve el CAPTCHA justo antes de enviar las credenciales.

// cypress/e2e/login.cy.js
describe("Login with reCAPTCHA", () => {
  it("should log in through a CAPTCHA-protected form", () => {
    cy.visit("/login");

    cy.get("#username").type("testuser");
    cy.get("#password").type("securepassword123");

    // Solve the CAPTCHA
    cy.solveCaptcha();

    // Submit
    cy.get('button[type="submit"]').click();

    // Verify login success
    cy.url().should("include", "/dashboard");
    cy.get(".welcome-message").should("contain", "Welcome, testuser");
  });
});

Flujo de registro

// cypress/e2e/register.cy.js
describe("Registration with CAPTCHA", () => {
  it("completes registration with all fields + CAPTCHA", () => {
    cy.visit("/register");

    cy.get("#first-name").type("Test");
    cy.get("#last-name").type("User");
    cy.get("#email").type("test@example.com");
    cy.get("#password").type("StrongPass!123");
    cy.get("#confirm-password").type("StrongPass!123");

    cy.solveCaptcha();

    cy.get("#register-btn").click();
    cy.url().should("include", "/verify-email");
  });
});

Checkout protegido con Turnstile

Cuando el formulario usa Cloudflare Turnstile, solveTurnstile escribe el token en cf-turnstile-response en lugar del campo de reCAPTCHA.

describe("Checkout with Turnstile", () => {
  it("processes payment through Turnstile-protected checkout", () => {
    cy.visit("/cart");

    cy.get(".checkout-btn").click();
    cy.get("#card-number").type("4242424242424242");
    cy.get("#expiry").type("12/26");
    cy.get("#cvc").type("123");

    cy.solveTurnstile();

    cy.get("#pay-now").click();
    cy.get(".confirmation").should("contain", "Order confirmed");
  });
});

Reintentos y manejo de errores

La resolución depende de la red, así que conviene envolverla en reintentos:

// cypress/support/commands.js

Cypress.Commands.add("solveCaptchaWithRetry", (options = {}) => {
  const maxRetries = options.retries || 3;

  function attempt(retryCount) {
    return cy.task("solveCaptcha", {
      siteUrl: options.siteUrl,
      sitekey: options.sitekey,
      type: options.type || "recaptcha_v2",
    }).then((token) => {
      if (!token && retryCount < maxRetries) {
        cy.log(`CAPTCHA retry ${retryCount + 1}/${maxRetries}`);
        cy.wait(2000);
        return attempt(retryCount + 1);
      }
      return token;
    });
  }

  return attempt(0);
});

Ajusta maxRetries según la estabilidad de tu red: tres intentos suelen bastar en CI, y cada reintento vuelve a pedir un token nuevo en lugar de reutilizar uno ya caducado.


Integración con CI/CD

GitHub Actions

Guarda tu clave como secreto (CAPTCHAAI_KEY) y pásala por variable de entorno; nunca la escribas en el repositorio:

name: E2E Tests
on: [push, pull_request]

jobs:
  cypress:
    runs-on: ubuntu-latest
    steps:

      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci

      - name: Run Cypress tests
        uses: cypress-io/github-action@v6
        env:
          CAPTCHAAI_KEY: ${{ secrets.CAPTCHAAI_KEY }}
        with:
          wait-on: "http://localhost:3000"
          start: npm start

Como CaptchaAI factura por thread concurrente y no por resolución, correr esta suite en cada push tiene un costo predecible: el plan BASIC ($15/mes, 5 threads) cubre varias máquinas de CI en paralelo con un costo fijo en USD.

Prueba de integración con Jest

// For teams that also use Jest for API-level CAPTCHA tests
const { solveCaptchaTask } = require("../cypress/plugins/captcha-solver");

test("CaptchaAI solves reCAPTCHA v2", async () => {
  const token = await solveCaptchaTask(
    "https://www.google.com/recaptcha/api2/demo",
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    "recaptcha_v2"
  );

  expect(token).toBeDefined();
  expect(token.length).toBeGreaterThan(50);
}, 120000);

Solución de problemas frecuentes

Problema Causa Solución
cy.task timed out La resolución de CAPTCHA tomó demasiado tiempo Aumenta taskTimeout en la configuración
Token rechazado Caducado antes de la inyección Reduce el retraso entre la resolución y el envío
data-sitekey no encontrado CAPTCHA se carga dinámicamente Agrega cy.wait() explícito o intercepta la carga
Callback no activado Nombre de callback personalizado Inspecciona ___grecaptcha_cfg en DevTools
CI falla, pasa en local Falta variable de entorno Agrega CAPTCHAAI_KEY a los secretos de CI

Preguntas frecuentes

¿Cuánto tiempo añade resolver un CAPTCHA a cada prueba?

Entre 15 y 30 segundos por resolución. Aísla las pruebas con CAPTCHA en una suite propia y paralelízalas con Cypress Cloud.

¿Qué tipos de CAPTCHA puedo resolver en mis pruebas de Cypress?

reCAPTCHA v2 y Cloudflare Turnstile en esta guía. La misma task sirve para el resto de tipos GA de CaptchaAI —reCAPTCHA v3, GeeTest v3, Cloudflare Challenge, imagen/OCR y grid— cambiando el method. hCaptcha y FunCaptcha no son compatibles.

¿Por qué mi token de reCAPTCHA se rechaza al inyectarlo?

Casi siempre por caducidad: los tokens tienen vida corta. Reduce el tiempo entre resolver y enviar, y confirma que el callback de ___grecaptcha_cfg se dispara.

¿Necesito un plan de pago para usarlo en integración continua?

Sí. Para CI parte del plan BASIC ($15/mes, 5 threads), con concurrencia suficiente para varias máquinas en paralelo; sube de thread cuando tu volumen crezca.

¿Funciona con la paralelización de Cypress Cloud?

Sí. Cada máquina paralela usa la misma clave API y CaptchaAI atiende las solicitudes simultáneas hasta el límite de threads de tu plan.


Guías relacionadas

Los comentarios están deshabilitados para este artículo.