التكاملات

اختبار Cypress من النوع E2E مع حلّ CAPTCHA عبر CaptchaAI

تصطدم اختبارات Cypress الآلية بجدار واحد على الدوام: نموذج محمي بـ CAPTCHA لا يستطيع أي سكربت اجتيازه من تلقاء نفسه. الحل ليس تعطيل الحماية في بيئة التدريج—فذلك يفتح فجوة بين الاختبار والإنتاج تُخفي أخطاءً لا تظهر إلا بعد النشر—بل تمرير التحدي إلى CaptchaAI الذي يحلّه ويعيد رمزاً صالحاً تحقنه في الصفحة، فتبقى بيئة الاختبار مطابقة للإنتاج تماماً. في هذا الدليل تبني هذا المسار خطوة بخطوة: معالج مهام، أوامر مخصصة، أمثلة اختبار جاهزة، ثم تكامل مع خط CI/CD.


لماذا لا يكفي تعطيل CAPTCHA في الاختبارات؟

الإغراء الأول أمام أي فريق QA هو إطفاء CAPTCHA في بيئة الاختبار كي تمرّ المجموعة بسرعة. المشكلة أن ذلك يختبر مساراً لا وجود له في الإنتاج؛ فثلاثة أشياء تُغفَل بالكامل:

  • التحقق الفعلي على صفحة تسجيل الدخول، فتصبح بلا حارس.
  • حقن الرمز في الحقل الخفي g-recaptcha-response.
  • دالة رد النداء (Callback) التي يعتمد عليها reCAPTCHA بعد الحلّ.

النتيجة اختبارات خضراء تُطمئن الفريق زوراً، بينما ينكسر التدفق الحقيقي عند أول مستخدم.

النهج الخطر
تعطيل CAPTCHA في بيئة التدريج يتجاهل أخطاء التكامل واختلافات تدفق النموذج
مفاتيح الاختبار (تمرّ دائماً) لا يختبر حقن الرمز ولا معالجة رد النداء
الحلّ عبر CaptchaAI اختبار بتكافؤ كامل مع الإنتاج

الخيار الثالث وحده يمرّ بالمسار نفسه الذي يسلكه المستخدم الحقيقي: تحدٍّ فعلي، رمز صالح، وحقلٌ يُملأ كما يحدث في الإنتاج.


تثبيت Cypress وتهيئة المشروع

ابدأ بإضافة Cypress إلى تبعيات التطوير:

npm install cypress --save-dev

إعداد ملف تهيئة Cypress

الفكرة الأساسية أن حلّ CAPTCHA عملية شبكية طويلة نسبياً، لذا نُشغّلها داخل setupNodeEvents كمهمة على طرف Node لا داخل المتصفح، ونرفع المهلات حتى لا يفشل الاختبار أثناء انتظار الرمز:

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

هذا المعالج هو قلب التكامل: يرسل التحدي إلى نقطة النهاية in.php، ثم يستطلع النتيجة دورياً عبر res.php حتى يعود الرمز جاهزاً. النموذج قائم على الـ Thread، فكل تحدٍّ قيد الحل يشغل خيطاً واحداً حتى ينتهي، ولا توجد رسوم لكل عملية حلّ داخل خطتك.

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

لاحظ اختلاف اسم الحقل بحسب النوع: reCAPTCHA يستخدم googlekey مع الطريقة userrecaptcha، بينما يستخدم Turnstile الحقل sitekey مع الطريقة turnstile.

اربط المعالج بملف cypress.config.js

استورد الدالة داخل التهيئة كي يستطيع الأمر cy.task استدعاءها من أي اختبار:

// 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 مخصصة لحقن الرمز

بدل تكرار منطق الحقن في كل اختبار، اجمعه في أمرين قابلين لإعادة الاستخدام. الأمر الأول للـ reCAPTCHA يقرأ data-sitekey من الصفحة، يستدعي المهمة، ثم يحقن الرمز في #g-recaptcha-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;
        });
      });
    });
  });
});

الأمر الثاني للـ Turnstile يحقن الرمز في cf-turnstile-response. بعد تعريف الأمرين تصبح كل خطوة CAPTCHA في اختباراتك سطراً واحداً: cy.solveCaptcha().


أمثلة اختبار E2E جاهزة

تخيّل منصة تجارة إلكترونية في منطقة الخليج تحمي تسجيل الدخول بـ reCAPTCHA v2 وتحمي صفحة الدفع بـ Cloudflare Turnstile. فريق QA يحتاج أن يختبر المسار كاملاً—من الدخول حتى تأكيد الطلب—على البيئة نفسها التي يراها العميل. الأمثلة التالية تغطي هذا السيناريو مباشرة.

تدفق تسجيل الدخول المحمي بـ 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("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");
  });
});

إتمام الشراء المحمي بـ 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.solveTurnstile() بدل cy.solveCaptcha()—أما بقية التدفق فيبقى كما يكتبه أي مطوّر Cypress عادي.


إعادة المحاولة ومعالجة الأخطاء

الشبكة تتذبذب، وقد يعود تحدٍّ فارغاً من حين لآخر. بدل أن ينكسر الاختبار كله، غلّف الاستدعاء بمنطق إعادة محاولة يعيد الكرّة عدة مرات قبل الاستسلام:

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

بهذا يصبح اختبارك مقاوماً للتذبذبات العابرة (flaky) بدل أن يفشل خط CI لسبب لا علاقة له بالكود المُختبَر.


تشغيل الاختبارات داخل CI/CD

الهدف النهائي أن تعمل هذه الاختبارات آلياً مع كل دفعة كود. مرّر مفتاح API عبر أسرار المستودع لا داخل الملفات، ثم شغّل Cypress في خط الأنابيب:

GitHub Actions

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

بعض الفرق تفضّل التحقق من طبقة الـ API مباشرةً خارج المتصفح. يمكنك إعادة استخدام المعالج نفسه داخل 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);

استكشاف الأخطاء وإصلاحها

معظم المشكلات تعود إلى المهلات أو صلاحية الرمز أو غياب متغير البيئة في CI. الجدول التالي يلخّص أكثرها شيوعاً وحلّها المباشر:

المشكلة السبب الإجراء
cy.task timed out استغرق حلّ CAPTCHA وقتاً أطول من المتوقع ارفع taskTimeout في التهيئة
رفض الرمز انتهت صلاحيته قبل الحقن قلّل التأخير بين الحلّ والإرسال
تعذّر العثور على data-sitekey تحميل CAPTCHA ديناميكياً أضف cy.wait() صريحاً أو اعتراضاً للطلب
عدم تشغيل رد النداء اسم رد نداء مخصص افحص ___grecaptcha_cfg في DevTools
فشل CI ونجاح محلي متغير بيئة مفقود أضف CAPTCHAAI_KEY إلى أسرار CI

الأسئلة الشائعة

ما أنواع CAPTCHA التي يغطيها هذا التكامل؟

الأمثلة هنا تعالج reCAPTCHA v2 وCloudflare Turnstile، وكلاهما مدعوم بشكل كامل. المعالج نفسه يمتد إلى بقية الأنواع المدعومة مثل reCAPTCHA v3 وGeeTest v3 وCloudflare Challenge بتغيير حقل method. لاحظ أن hCaptcha وFunCaptcha غير مدعومَين حالياً، فلا تبنِ اختباراً يفترض حلّهما.

كيف أوازن بين تكلفة الحلّ في CI وسرعة المجموعة؟

لأن CaptchaAI يحاسب على الـ Thread المتزامن لا على كل عملية حلّ، تختار الخطة بحسب عدد الاختبارات المتوازية لا بحسب عددها الكلي:

  • BASIC ($15 شهرياً، 5 Threads): يكفي خط CI صغيراً بعدد محدود من الاختبارات المتوازية.
  • STANDARD ($30 شهرياً، 15 Thread): يناسب الفرق التي تشغّل مجموعات متوازية أوسع.

لا توجد رسوم لكل تحدٍّ داخل الخطة، فيبقى ثمن الشهر ثابتاً مهما تكرّرت عمليات الحلّ.

هل يمكن تشغيل هذا مع اختبار مكوّنات Cypress؟

لا. اختبارات المكوّنات لا تحمّل صفحات حقيقية، ولا يظهر فيها تحدي CAPTCHA أصلاً. استخدم هذا الأسلوب حصراً في اختبارات E2E التي تفتح عناوين URL كاملة تحوي تحدياً فعلياً.

هل التكامل مناسب لأتمتة اختبار الجودة المعتمدة؟

نعم، هذا هو الاستخدام المقصود بالضبط: اختبار تدفقات تطبيقك أنت على بيئة تطابق الإنتاج، دون تعطيل الحماية. يبقى الأمر ضمن نطاق اختبار الجودة على أنظمة تملك صلاحية اختبارها.


أدلة ذات صلة



الخطوات التالية

التعليقات غير مفعّلة لهذا المقال.