Integrasi

Cypress + CaptchaAI: Pengujian E2E dengan Penanganan CAPTCHA

CAPTCHA memblokir pengujian E2E otomatis. Menonaktifkannya di staging menciptakan perbedaan lingkungan — bug yang hanya muncul di produksi saat CAPTCHA aktif. CaptchaAI memungkinkan tes Cypress Anda berinteraksi dengan CAPTCHA asli, menjaga lingkungan pengujian tetap identik dengan produksi.


Mengapa Tidak Menonaktifkan CAPTCHA Saja dalam Pengujian?

Pendekatan Risiko
Nonaktifkan CAPTCHA di staging Melewatkan bug integrasi, membentuk perbedaan alur
Gunakan test key (selalu lulus) Tidak menguji inject token, penanganan callback
Solve dengan CaptchaAI Paritas penuh dengan produksi

Pengaturan

npm install cypress --save-dev

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

Task Handler CaptchaAI

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

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

Perintah Khusus

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

Contoh Tes E2E

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

Alur Pendaftaran

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

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

Retry dan Error Handling

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

Integrasi CI/CD

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

Tes Integrasi Jest

// Untuk tim yang juga menggunakan Jest untuk tes CAPTCHA di level API
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);

Pemecahan Masalah

Masalah Penyebab Perbaikan
cy.task timed out Solve CAPTCHA terlalu lama Tingkatkan taskTimeout di config
Token ditolak Kedaluwarsa sebelum inject Kurangi delay antara solve dan submit
data-sitekey tidak ditemukan CAPTCHA dimuat secara dinamis Tambahkan cy.wait() eksplisit atau intercept
Callback tidak terpicu Nama callback kustom Periksa ___grecaptcha_cfg di DevTools
CI gagal, lokal oke Environment variable tidak ada Tambahkan CAPTCHAAI_KEY ke secret CI

Pertanyaan Umum

Apakah ini akan memperlambat test suite saya?

Setiap solve CAPTCHA menambah 15–30 detik. Jalankan tes CAPTCHA di suite terpisah atau paralelkan dengan Cypress Cloud.

Bisakah saya menggunakan ini dengan Cypress component testing?

Tidak — component testing tidak memuat halaman nyata. Gunakan ini hanya untuk E2E test yang mengakses URL halaman penuh dengan CAPTCHA asli.

Haruskah saya menguji dengan CAPTCHA asli atau mock?

Uji dengan CAPTCHA asli di staging E2E. Gunakan mock di unit test. Ini memastikan paritas penuh dengan produksi.

Apakah CaptchaAI berfungsi dengan paralelisasi Cypress Cloud?

Ya. Setiap mesin paralel memanggil API key yang sama. CaptchaAI menangani request concurrent.


Panduan Terkait

  • Puppeteer + CaptchaAI untuk QA
  • CaptchaAI Quickstart
  • Cara Solve reCAPTCHA v2 dengan API
  • Cara Solve Cloudflare Turnstile dengan API
Komentar dinonaktifkan untuk artikel ini.