Cypress không tự giải được CAPTCHA — đó là câu trả lời ngắn cho câu hỏi nhiều đội QA gặp khi bộ test E2E dừng lại ở bước đăng nhập hoặc thanh toán. Tắt CAPTCHA ở staging tạo ra môi trường test khác hẳn production: lỗi đặt token vào biểu mẫu QA, lỗi callback chỉ lộ ra khi CAPTCHA thật đang chạy. Cách xử lý đúng: để Cypress gọi CaptchaAI qua một task handler trong cypress.config.js, nhận token thật rồi tiêm ngược vào form.
Bốn bước CaptchaAI xử lý CAPTCHA trong một lần test
Nắm luồng xử lý chung trước khi vào code. Mọi tích hợp CaptchaAI — kể cả trong Cypress — đều đi qua đúng bốn bước:
- Cypress gửi
sitekey+ URL trang tớiin.php, nhận về mộttask ID. - Task handler polling
res.phpmỗi 5 giây bằngtask IDđó. - CaptchaAI trả token thật khi giải xong (hoặc
CAPCHA_NOT_READYnếu chưa xong). - Custom command đặt token vào biểu mẫu QA vào field ẩn của form (
g-recaptcha-responsehoặccf-turnstile-response) rồi kích hoạt callback nếu site có.
Vì sao không nên tắt CAPTCHA khi chạy test E2E?
Ba cách tiếp cận phổ biến, và chỉ một cách giữ được độ tin cậy tương đương production:
| Cách tiếp cận | Rủi ro |
|---|---|
| Tắt CAPTCHA ở môi trường staging | Bỏ sót lỗi tích hợp, luồng biểu mẫu chạy khác production |
| Dùng site key kiểm thử (luôn qua) | Không kiểm tra việc đặt token vào biểu mẫu QA, không kiểm tra xử lý callback |
| Giải bằng CaptchaAI | Test đúng như production, đầy đủ CAPTCHA thật |
Dấu hiệu bộ test đang né CAPTCHA thay vì test CAPTCHA thật:
- Test pass ở CI nhưng người dùng thật vẫn gặp lỗi ở form có CAPTCHA trên production.
- Không dòng code nào tiêm
tokenhoặc gọi callback___grecaptcha_cfg. - Biến
DISABLE_CAPTCHA=truechỉ tồn tại ở staging, không có trên production.
Nhiều đội QA outsourcing ở TP.HCM và Hà Nội viết test Cypress cho ứng dụng thương mại điện tử kiểu Shopee/Tiki, nơi checkout được bảo vệ bằng Cloudflare Turnstile — bỏ qua bước giải CAPTCHA nghĩa là lỗi đặt token vào biểu mẫu QA chỉ lộ ra khi khách đang thanh toán thật.
Khi nào KHÔNG cần CaptchaAI trong bộ test Cypress
Không phải test nào cũng cần CAPTCHA thật:
- Component testing không tải trang thật,
cy.solveCaptcha()không códata-sitekeyđể đọc — dùng mock token. - Unit test xác thực logic form (validate email, độ dài mật khẩu…) không liên quan CAPTCHA.
- Chỉ dùng CaptchaAI cho E2E chạy trên URL trang thật, nơi CAPTCHA thật đang hiển thị.
Thiết lập dự án Cypress
Cài Cypress bình thường, không cần plugin riêng cho CaptchaAI:
npm install cypress --save-dev
Cấu hình Cypress (cypress.config.js)
Khai báo task tên solveCaptcha trong setupNodeEvents — Node.js (không phải trình duyệt) gọi API CaptchaAI để tránh giới hạn CORS:
// 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",
},
});
Viết task handler gọi API CaptchaAI
Handler này gửi CAPTCHA tới in.php, polling res.php cho tới khi có token — đúng bốn bước ở trên:
// 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 };
Kết nối handler với cypress.config.js
Import handler ở trên và đăng ký lại task, thay thế bản khai báo rút gọn ban đầu bằng bản trỏ thẳng tới captcha-solver.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);
},
});
},
},
});
Custom command: solveCaptcha và solveTurnstile
Gói task thành hai custom command gọi được như lệnh Cypress bình thường. solveCaptcha tự đọc data-sitekey, gọi task, đặt token vào biểu mẫu QA vào #g-recaptcha-response và callback ___grecaptcha_cfg nếu có:
// 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;
});
});
});
});
});
Ví dụ test E2E thực tế với CaptchaAI
Test luồng đăng nhập có reCAPTCHA v2
// 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");
});
});
Test luồng đăng ký tài khoản
// 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");
});
});
Test checkout có Cloudflare Turnstile
Cùng pattern, chỉ đổi sang solveTurnstile() vì Turnstile dùng field token khác (cf-turnstile-response):
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 và xử lý lỗi khi giải CAPTCHA thất bại
CAPTCHA thật thỉnh thoảng timeout hoặc trả token rỗng — thêm retry với backoff ngắn thay vì để pipeline fail ngay:
// 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);
});
Chạy Cypress + CaptchaAI trong pipeline CI/CD
Mẹo: tách test có CAPTCHA vào job riêng, không chạy chung unit test — mỗi lần giải cộng thêm 15-30 giây.
GitHub Actions
Đặt CAPTCHAAI_KEY vào secrets của repo, không hardcode trong file cấu hình:
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
Test tích hợp với Jest (tầng API)
Đội cũng dùng Jest để test CAPTCHA tầng API thì tái sử dụng thẳng handler đã viết ở trên:
// 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);
Câu hỏi thường gặp
Cypress có tự giải được CAPTCHA mà không cần dịch vụ ngoài không?
Không. Cypress chỉ điều khiển trình duyệt và assertion, không có engine giải CAPTCHA. Cần một service như CaptchaAI nhận sitekey, trả token, rồi tiêm vào form qua custom command như ở trên.
CaptchaAI có hỗ trợ hCaptcha trong bộ test Cypress không?
Chưa. CaptchaAI hiện giải reCAPTCHA v2/v3, Cloudflare Turnstile và Challenge, GeeTest v3, CAPTCHA ảnh/OCR, grid image và BLS; hCaptcha, FunCaptcha chưa hỗ trợ. Custom command ở bài này chưa dùng được cho hCaptcha.
Nên test với CAPTCHA thật hay mock trong pipeline CI?
Dùng CAPTCHA thật ở tầng E2E, mock ở unit test. Mock nhanh, ổn định cho logic component, nhưng chỉ CAPTCHA thật mới bắt được lỗi đặt token vào biểu mẫu QA và lỗi callback — đúng lỗi chỉ lộ ra trên production.
Chạy song song trên Cypress Cloud có cần nhiều API key CaptchaAI không?
Không cần nhiều key. Mỗi máy song song gọi chung một API key, CaptchaAI xử lý request đồng thời theo số thread của gói bạn dùng — BASIC ($15/tháng, 5 thread) đủ cho vài máy chạy test nhẹ, nhiều container hơn thì cần gói nhiều thread hơn.
Các lỗi thường gặp và cách xử lý
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
cy.task timed out |
Quá trình giải CAPTCHA mất quá lâu | Tăng taskTimeout trong cấu hình |
| Token bị từ chối | Token hết hạn trước khi tiêm | Giảm độ trễ giữa lúc giải xong và lúc submit |
Không tìm thấy data-sitekey |
CAPTCHA tải động (lazy-load) | Thêm cy.wait() rõ ràng hoặc chặn (intercept) request |
| Callback không được kích hoạt | Site dùng tên callback tùy chỉnh | Kiểm tra ___grecaptcha_cfg trong DevTools |
| CI fail, chạy local pass | Thiếu biến môi trường | Thêm CAPTCHAAI_KEY vào secrets của CI |