Integrations

Cypress + CaptchaAI: Pengujian E2E dengan Penanganan CAPTCHA

Tim QA yang menonaktifkan CAPTCHA di staging supaya CI cepat hijau sering kaget: bug baru muncul justru di production, persis saat reCAPTCHA atau Turnstile aktif. Cypress bisa menghindari itu — task handler Node.js mengirim challenge ke CaptchaAI, custom command menyuntikkan token ke DOM, dan suite E2E Anda menguji alur yang sama persis dengan yang dilihat pengguna.

Setelah mengikuti panduan ini, Anda akan punya:

  • Task handler Node.js yang bicara langsung ke API CaptchaAI dari setupNodeEvents
  • Custom command cy.solveCaptcha() dan cy.solveTurnstile() siap pakai di test suite mana pun
  • Retry logic dan pipeline CI/CD yang tidak flaky gara-gara CAPTCHA

Kenapa Menonaktifkan CAPTCHA di Staging Berisiko

Pendekatan Konsekuensi
Nonaktifkan CAPTCHA di staging Bug integrasi ketahuan setelah rilis
Pakai test key yang selalu lulus Token injection dan callback tidak teruji
Solve CAPTCHA asli via CaptchaAI Staging = production, byte demi byte

Instalasi dan Konfigurasi Awal

Prasyarat

Sebelum mulai, siapkan tiga hal ini:

  • Node.js versi LTS terbaru dan project Cypress yang sudah bisa dijalankan (npx cypress open)
  • API key CaptchaAI aktif — ambil dari dashboard akun Anda
  • Akses untuk menambahkan environment variable di CI (GitHub Actions atau setara)

Install Cypress seperti biasa:

npm install cypress --save-dev

Atur cypress.config.js untuk Task CAPTCHA

Daftarkan task solveCaptcha di setupNodeEvents — titik masuk Node.js ke API CaptchaAI:

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

Bangun Task Handler CaptchaAI di Node.js

Handler ini bicara ke API CaptchaAI: kirim challenge ke in.php, lalu polling res.php tiap 5 detik sampai token siap.

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

Daftarkan Task Handler ke Config

Import solveCaptchaTask dan hubungkan ke task solveCaptcha:

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

Custom Command: solveCaptcha dan solveTurnstile

cy.solveCaptcha() untuk reCAPTCHA v2, cy.solveTurnstile() untuk Turnstile — keduanya baca data-sitekey, panggil task Node.js, lalu suntik token.

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

Skenario Tes E2E dengan CAPTCHA Aktif

Skenario Login dengan reCAPTCHA v2

Panggil cy.solveCaptcha() persis sebelum submit:

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

Skenario Registrasi dengan CAPTCHA

Form multi-field, pola yang sama:

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

Skenario Checkout dengan Cloudflare Turnstile

Untuk Turnstile pakai cy.solveTurnstile(), bukan solveCaptcha():

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 Penanganan Error saat Solve Gagal

Solve bisa gagal karena sebab yang sifatnya sementara, bukan karena CAPTCHA-nya sendiri bermasalah:

  • Timeout jaringan singkat ke API CaptchaAI
  • Rate limit sesaat saat banyak job paralel submit bersamaan
  • Race condition antara token siap dan render ulang DOM sebelum token sempat disuntikkan

Bungkus task dengan retry supaya kegagalan sesaat seperti ini tidak menggagalkan seluruh test run:

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

Jalankan Tes CAPTCHA di Pipeline CI/CD

GitHub Actions

CAPTCHAAI_KEY masuk sebagai secret, bukan hardcode:

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 API-Level dengan Jest (Opsional)

Untuk tim yang juga menguji CaptchaAI di level API murni, panggil solveCaptchaTask dari 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);

Alokasi Thread untuk Runner Paralel

Untuk job Cypress paralel — termasuk runner di region Singapura (ap-southeast-1) atau Jakarta (asia-southeast2) — hitung thread CaptchaAI, bukan jumlah runner: satu job aktif = satu thread. BASIC ($15/bulan, 5 thread) cukup untuk pipeline kecil; matrix testing lebih besar naik ke STANDARD ($30/bulan, 15 thread). Billing per thread dengan solve tanpa batas membuat biaya tetap flat walau volume test naik.

Pantau pemakaian thread lewat dashboard CaptchaAI sebelum menaikkan concurrency runner di CI, lalu naikkan plan bila kebutuhan thread paralel Anda mendekati batas plan yang sedang dipakai.


Ringkasan Alur Empat Langkah

Berapa pun jumlah skenario tes yang Anda tambahkan, alur intinya selalu sama:

  1. Task handler kirim challenge ke in.php
  2. Simpan taskId dari respons
  3. Polling res.php tiap 5 detik sampai token siap
  4. Custom command suntik token ke DOM lalu submit form

Hafalkan urutan ini dan debugging jadi jauh lebih cepat — begitu ada langkah yang macet, Anda langsung tahu di titik mana harus mencari log.


Masalah Umum dan Solusi Cepat

Gejala Penyebab Solusi
cy.task timed out Solve melewati batas waktu default Naikkan taskTimeout di config
Token ditolak backend Kedaluwarsa sebelum sempat di-submit Perpendek jeda solve ke submit
data-sitekey tidak ditemukan CAPTCHA dimuat dinamis (lazy render) Tambahkan cy.wait() atau cy.intercept()
Callback grecaptcha tidak terpicu Situs pakai nama callback kustom Periksa ___grecaptcha_cfg di DevTools
Lolos lokal, gagal di CI CAPTCHAAI_KEY tidak ter-set di CI Tambahkan sebagai secret CI

Sebagian besar kegagalan di atas selesai dengan menaikkan timeout atau memastikan CAPTCHAAI_KEY benar-benar ter-set di environment CI — jarang perlu debug lebih dalam di sisi CaptchaAI sendiri.


Pertanyaan Umum

Kenapa butuh custom command retry terpisah, bukan cukup naikkan timeout saja?

Naikkan timeout hanya membuat Cypress menunggu lebih lama untuk kegagalan yang sama. solveCaptchaWithRetry mencoba ulang solve dari awal saat gagal karena sebab sementara — timeout jaringan atau rate limit — dua masalah yang beda akar.

Apakah setup ini jalan untuk Cypress component testing?

Tidak. Component testing tidak memuat halaman penuh, jadi tidak ada data-sitekey nyata untuk dibaca — pakai ini hanya untuk E2E.

Berapa thread CaptchaAI yang dibutuhkan untuk CI paralel?

Hitung dari jumlah job yang solve bersamaan, bukan jumlah test total. Satu job aktif = satu thread.

Apa beda penanganan reCAPTCHA v2 dan Turnstile di custom command ini?

solveCaptcha() menyuntikkan token ke g-recaptcha-response dan memicu callback grecaptcha. solveTurnstile() menulis token ke cf-turnstile-response — tidak ada callback JS setara.

Kenapa test tetap gagal walau token sudah berhasil di-inject?

Paling sering delay solve-ke-submit terlalu panjang sehingga token kedaluwarsa, atau elemen submit berpindah di DOM sebelum cy.get() sempat klik.


Panduan Terkait

Komentar dinonaktifkan untuk artikel ini.