Integrations

Cypress + CaptchaAI: E2E-тестирование с обработкой CAPTCHA

Реальная CAPTCHA в форме логина или на кассе — частая причина, почему E2E-тесты в Cypress либо просто падают, либо тестируют не то, что окажется в продакшене. Самый быстрый обходной путь — отключить CAPTCHA в staging — на практике прячет баги: токен не долетает до бэкенда, обратный вызов reCAPTCHA не срабатывает, а форма в продакшене ломается именно там, где тесты были зелёными.

CaptchaAI решает эту проблему иначе: тест Cypress получает настоящий токен CAPTCHA через API и подставляет его в форму, а staging остаётся полностью идентичным продакшену. Из этого руководства вы получите:

  • обработчик задач (cy.task), который решает reCAPTCHA v2 и Cloudflare Turnstile через CaptchaAI;
  • кастомные команды cy.solveCaptcha() / cy.solveTurnstile() для форм логина, регистрации и оформления заказа;
  • шаблон retry с задержкой на случай нестабильной сети CI;
  • готовый workflow для GitHub Actions.

Почему нельзя просто отключить CAPTCHA в тестах

Подход Риск
Отключить CAPTCHA в staging Пропускает баги интеграции, тестовый флоу расходится с продакшеном
Тестовые ключи Google (always-pass) Не проверяет доставку токена на бэкенд и обработку callback
Решить через CaptchaAI Полный паритет с продакшеном

Третий вариант дороже по времени прогона — 15–30 секунд на каждое решение. Зато именно он ловит баги, которые всплывают только в продакшене:

  • неверное имя поля токена на бэкенде;
  • отсутствующий обработчик callback;
  • гонку между рендером виджета и отправкой формы.

Установка и настройка

npm install cypress --save-dev

Конфигурация Cypress

Таймауты по умолчанию в Cypress рассчитаны на обычный DOM, а не на ожидание внешнего API. Решение CAPTCHA через cy.task может занять больше 4 секунд, которые Cypress отводит на команду по умолчанию, поэтому defaultCommandTimeout и responseTimeout увеличены до 120 секунд:

// 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 выполняются в браузерном контексте, а прямые сетевые запросы к внешнему API оттуда небезопасны и часто блокируются CORS. Поэтому решение CAPTCHA выносится в задачу (task) — код, который выполняется в node-процессе Cypress, а не в браузере. Ниже — модуль, который отправляет задачу в CaptchaAI и опрашивает res.php, пока не придёт готовый токен:

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

Параметр type определяет сразу две вещи в in.php: метод (userrecaptcha для reCAPTCHA, turnstile для Cloudflare Turnstile) и поле с sitekey (googlekey против sitekey). Перепутать их — самая частая причина, почему submitResp.status возвращает ошибку сразу после отправки, ещё до опроса res.php.

Подключение обработчика в 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);
        },
      });
    },
  },
});

Кастомные команды Cypress

Задачу удобнее вызывать не напрямую, а через собственные команды. Каждая из них:

  • находит sitekey на странице и вызывает cy.task("solveCaptcha", …);
  • подставляет полученный токен в видимое и все скрытые поля [name="g-recaptcha-response"];
  • вызывает клиентский callback виджета reCAPTCHA, если страница его ожидает.
// 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-тестов с CAPTCHA

Ниже три сценария на одной и той же паре команд cy.solveCaptcha() / cy.solveTurnstile(): логин, регистрация и оформление заказа. Разница только в типе CAPTCHA и в том, какую форму нужно заполнить перед вызовом команды.

Тест входа с 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");
  });
});

В тестах регистрации и логина используйте только синтетические данные ([email protected], тестовые пароли), а не реальные email или ФИО живых людей — даже в закрытом staging. Для команд, чьи staging-стенды хранят такие данные дольше цикла тестов, это ещё и требование 152-ФЗ «О персональных данных» для российских проектов и GDPR-диллидженс для распределённых команд. CaptchaAI в этой схеме получает только sitekey и адрес страницы — данные из полей формы к нему не уходят.

Оформление заказа с Cloudflare 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");
  });
});

Номер карты 4242424242424242 — тестовый номер платёжного шлюза (Stripe), а не реальные платёжные данные; используйте эквивалентный тестовый номер вашего провайдера.


Повтор попыток и обработка ошибок

Сеть до ocr.captchaai.com иногда отдаёт пустой токен из-за таймаута апстрима — в CI это заметнее, чем локально, особенно на раннерах с нестабильной сетью. Оберните вызов задачи в повтор с паузой, а не в жёсткий fail:

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

GitHub Actions

Ключ CaptchaAI хранится в секретах CI, а не в репозитории — так же, как любой другой API-ключ:

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

Если раннеры CI размещены в другом регионе, чем ваша команда (например, EU-раннеры при разработке из Казахстана или Беларуси), добавьте небольшой запас к taskTimeout сверх обычных 120 секунд: сетевая задержка до ocr.captchaai.com складывается с временем решения CAPTCHA.

Интеграция с Jest

Тот же обработчик задач можно переиспользовать вне Cypress — например, для API-теста на Jest, который проверяет только связку CaptchaAI + бэкенд, без запуска браузера:

// 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 Решение CAPTCHA заняло больше времени, чем таймаут Cypress Увеличьте taskTimeout в конфигурации
Токен отклонён бэкендом Токен истёк до отправки формы Сократите задержку между решением и submit
data-sitekey не найден CAPTCHA рендерится динамически Добавьте явный cy.wait() или перехватите запрос через cy.intercept()
Обратный вызов не сработал На странице нестандартное имя callback-функции Проверьте ___grecaptcha_cfg в DevTools
В CI падает, локально проходит Не задана переменная CAPTCHAAI_KEY Добавьте CAPTCHAAI_KEY в секреты CI

Часто задаваемые вопросы

Вопросы ниже собраны из тикетов QA-команд, которые уже подключали CaptchaAI к Cypress.

Насколько решение CAPTCHA замедлит прогон тестов?

Каждое решение добавляет 15–30 секунд. Вынесите тесты с CAPTCHA в отдельный набор (cypress/e2e/captcha/**) и запускайте его отдельно от быстрых UI-тестов или параллельте через Cypress Cloud.

Можно ли использовать это в component-тестах Cypress?

Нет. Component-тесты монтируют компонент напрямую и не грузят реальную страницу с виджетом CAPTCHA. Обработчик задач имеет смысл только для E2E-тестов, которые открывают полноценный URL.

Обязательно ли гонять реальные CAPTCHA в каждом pull request, или достаточно nightly?

Быстрые PR-прогоны можно оставить на моках CAPTCHA-виджета, а реальное решение через CaptchaAI подключать в nightly-сборке или перед релизом — так CI остаётся быстрым, но регрессии в интеграции токена всё равно ловятся до продакшена.

Сколько потоков CaptchaAI нужно для параллельных машин в Cypress Cloud?

Один поток обрабатывает одно решение одновременно, так что для 5–10 параллельных раннеров обычно достаточно тарифа ADVANCE ($90/мес, 50 потоков) — с запасом на пиковые прогоны. Считайте по числу одновременных CAPTCHA-тестов, а не по общему числу тестов в наборе.

Что делать с персональными данными, которые проходят через форму с CAPTCHA?

  • используйте только синтетические тестовые данные, никогда реальные email/номера карт;
  • не логируйте значения полей формы в отчётах Cypress и видеозаписях прогонов;
  • помните, что CaptchaAI получает исключительно sitekey и pageurl — данные из полей формы в запрос к API не попадают.

Итог

  • Не отключайте CAPTCHA в staging — это прячет ровно те баги, которые всплывут в продакшене.
  • Решение выносится в cy.task, потому что прямой вызов API CaptchaAI из браузерного контекста теста заблокирует CORS.
  • Кастомные команды (cy.solveCaptcha(), cy.solveTurnstile()) избавляют от копипасты между тестами логина, регистрации и оформления заказа.
  • Добавьте retry с задержкой и запас по taskTimeout в CI — сеть до ocr.captchaai.com в пайплайне менее стабильна, чем локально.

Похожие материалы



Следующие шаги

Комментарии для этой статьи отключены.