Integrations

Cypress + CaptchaAI:使用验证码处理进行 E2E 测试

跑 Cypress 回归测试时最怕的不是断言写错,而是测试卡在验证码保护的登录页或结账页——本地能过,CI 里直接超时。多数团队的应对方式是在 staging 关掉验证码,测试是绿了,可 token 提交、回调触发这些真实路径从没被测过,问题只会在上线后暴露。

CaptchaAI 能让 Cypress 直接识别真实的 reCAPTCHA v2 和 Cloudflare Turnstile,把 token 提交、回调触发纳入测试范围。这篇指南给出可直接用的任务处理函数、自定义命令、三个端到端场景,以及 CI/CD 常见坑点。


测试环境要不要保留真实验证码?

团队一般会在三种做法里选一种。

方案一:staging 直接关闭验证码

测试很快就能跑绿,但也把表单流程里最容易出 bug 的一段测没了——生产环境验证码是开着的,行为和 staging 不一致。

方案二:用永远通过的测试密钥

表面上看是真实流程,其实还是没覆盖 token 提交和回调触发这两步,出了问题照样发现不了。

方案三:用 CaptchaAI 识别真实验证码

测试环境和生产走同一套验证码逻辑,token 提交、回调触发全部覆盖,是三种做法里唯一能做到生产平价的一种。


安装与初始化

npm install cypress --save-dev

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",
  },
});

CaptchaAI 任务处理函数

验证码识别写成 Cypress 的 task

调用 CaptchaAI API 提交任务、轮询结果,把 token 交回测试。

// 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 };

接入 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);
        },
      });
    },
  },
});

封装成自定义命令

把提交、轮询、注入 token 封装成 cy.solveCaptcha()cy.solveTurnstile()

测试里只需调用一行。

// 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;
        });
      });
    });
  });
});

三个 E2E 场景

场景一:reCAPTCHA 登录

// 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");
  });
});

场景二:注册流程

// 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("[email protected]");
    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");
  });
});

场景三:Turnstile 结账

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");
  });
});

重试与失败处理

偶尔会因网络抖动失败一次。

cy.task 加层重试比直接标红划算。

// 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);
});

CI/CD 集成

国内自建 runner 常踩的坑

reCAPTCHA 从 Google 域名加载脚本,CI runner 如果部署在国内网络(比如自建的私有 runner),大概率访问不到该域名,任务会一直卡到超时——报错看起来像 CaptchaAI 没识别成功,其实是页面没加载出验证码组件。换成 GitHub Actions 默认的海外托管 runner 通常能解决,这是网络可达性问题,与识别逻辑无关。

GitHub Actions 配置

CI 里只需把 API Key 放进 secrets。

其余配置不变。

小贴士:本地 .env 和 CI secrets 用同一个 CAPTCHAAI_KEY,不需要为每个 worker 单独申请密钥。

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

Jest 集成测试

同时用 Jest 做 API 层验证码测试的团队,不用另外写一套。

直接复用 solveCaptchaTask 即可。

// 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);

常见报错排查

常见的几类报错和处理方式:

cy.task timed out:识别耗时超过默认超时,调大 taskTimeout

token 被拒绝:注入前已过期,缩短识别到提交的间隔。

找不到 data-sitekey:验证码动态加载,加 cy.wait() 或拦截请求。

回调没有触发:自定义回调名,去 DevTools 检查 ___grecaptcha_cfg

CI 失败、本地能过:CI 缺环境变量,把 CAPTCHAAI_KEY 加进 secrets。


常见问题

为什么本地测试能过,CI 里 reCAPTCHA 却经常加载不出来?

多数情况是 CI runner 访问不到 Google 域名,不是 CaptchaAI 识别失败。

先确认 runner 能不能访问 reCAPTCHA 的加载域名,能访问再排查 token 提交逻辑。

CI 并行跑多个 worker,CaptchaAI 该选多少线程?

线程数按并行 worker 数估算。

5 个 worker 用 BASIC($15/月,5 线程)刚好够,worker 更多就上 STANDARD($30/月,15 线程)。

token 提交后偶尔还是失败,要先查什么?

先确认 token 是否在拿到后几秒内就提交了——多数 token 有效期很短。

再检查回调是否标准(___grecaptcha_cfgcf-turnstile-response)。

组件测试里能不能直接调用 solveCaptcha?

不建议。Cypress 组件测试不会加载完整页面,[data-sitekey] 大概率取不到,也没有真实回调可触发。

把带验证码的用例放进 E2E suite 里跑。

taskTimeout 要不要单独设置?

识别耗时通常在 15–30 秒,默认的 defaultCommandTimeout 未必够用。

建议给带验证码的用例单独调大 taskTimeout,避免和普通用例共用同一个超时阈值。


相关指南



后续阅读

该文章已禁用评论。