Tích Hợp

Xử lý CAPTCHA tự động hóa iOS với XCUITest và CaptchaAI

Thử nghiệm ứng dụng iOS với XCUITest thường đạt được CAPTCHA trong WKWebView được nhúng – biểu mẫu đăng nhập, cổng thanh toán và tích hợp của bên thứ ba đưa ra các thách thức về reCAPTCHA.CaptchaAIgiải quyết những vấn đề này để các bài kiểm tra giao diện người dùng của bạn có thể hoàn thành các luồng từ đầu đến cuối mà không cần can thiệp thủ công.

Hướng dẫn này cho biết cách phát hiện CAPTCHA trong WKWebView trong khi chạy XCUITest, giải quyết chúng thông qua CaptchaAI từ một dịch vụ đồng hành và đưa lại mã thông báo vào nội dung web.

Kịch bản thế giới thực

Ứng dụng iOS của bạn tải biểu mẫu đăng ký trong WKWebView. Biểu mẫu bao gồm reCAPTCHA v2. Trong quá trình kiểm tra tự động, CAPTCHA này chặn tiến trình kiểm tra. Bạn cần một giải pháp:

  1. Phát hiện CAPTCHA trong WebView trong quá trình thực hiện kiểm tra
  2. Trích xuất khóa trang web theo chương trình
  3. Giải quyết nó thông qua CaptchaAI
  4. Tiêm mã thông báo để biểu mẫu có thể gửi

Môi trường: Xcode 15+, Swift, XCUITest, trình chạy thử nghiệm macOS, API CaptchaAI.

Kiến trúc

XCUITest không thể thực thi trực tiếp JavaScript trong WKWebView. Cách tiếp cận này sử dụng điểm cuối của trình trợ giúp mà ứng dụng gọi trong quá trình thử nghiệm:

thành phần Vai trò
XCUITest Điều khiển giao diện người dùng, kích hoạt giải CAPTCHA thông qua trình trợ giúp kiểm tra
API trợ giúp kiểm tra Nhận sitekey + URL, gọi CaptchaAI, trả về mã thông báo
Móc thử nghiệm ứng dụng Đánh giá JavaScript trong WKWebView để phát hiện/inject
API CaptchaAI Giải quyết thử thách CAPTCHA

Bước 1: Thêm Test Hook vào ứng dụng

Trong bộ điều khiển WKWebView của ứng dụng, hãy thêm trình xử lý CAPTCHA ở chế độ thử nghiệm có thể được kích hoạt thông qua mã nhận dạng trợ năng hoặc lược đồ URL:

// CaptchaTestHelper.swift — Add to app target (test build only)
import WebKit

#if DEBUG
class CaptchaTestHelper {
    private let webView: WKWebView

    init(webView: WKWebView) {
        self.webView = webView
    }

    func detectCaptcha(completion: @escaping (String?, String?) -> Void) {
        let script = """
        (function() {
            var el = document.querySelector('.g-recaptcha');
            if (el) {
                return JSON.stringify({
                    sitekey: el.getAttribute('data-sitekey'),
                    pageurl: window.location.href
                });
            }
            return null;
        })();
        """

        webView.evaluateJavaScript(script) { result, error in
            guard let jsonString = result as? String,
                  let data = jsonString.data(using: .utf8),
                  let json = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
                completion(nil, nil)
                return
            }
            completion(json["sitekey"], json["pageurl"])
        }
    }

    func injectToken(_ token: String, completion: @escaping (Bool) -> Void) {
        let script = """
        document.getElementById('g-recaptcha-response').value = '\(token)';
        try {
            var clients = ___grecaptcha_cfg.clients;
            Object.keys(clients).forEach(function(k) {
                Object.keys(clients[k]).forEach(function(j) {
                    if (clients[k][j] && clients[k][j].callback) {
                        clients[k][j].callback('\(token)');
                    }
                });
            });
        } catch(e) {}
        true;
        """

        webView.evaluateJavaScript(script) { _, error in
            completion(error == nil)
        }
    }

    func solveCaptchaViaBackend(
        sitekey: String, pageurl: String,
        completion: @escaping (Result<String, Error>) -> Void
    ) {
        guard let url = URL(string: "http://localhost:3000/api/solve-captcha") else {
            return
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let body: [String: String] = [
            "captchaType": "recaptcha_v2",
            "sitekey": sitekey,
            "pageurl": pageurl
        ]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        URLSession.shared.dataTask(with: request) { data, _, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            guard let data = data,
                  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
                  let token = json["token"] as? String else {
                completion(.failure(NSError(domain: "", code: -1,
                    userInfo: [NSLocalizedDescriptionKey: "No token"])))
                return
            }
            completion(.success(token))
        }.resume()
    }
}
#endif

Bước 2: Dịch vụ bộ giải phụ trợ

Chạy dịch vụ bộ giải cục bộ trong quá trình thử nghiệm giao tiếp với CaptchaAI:

# ios_test_solver.py — Run on test machine during XCUITest execution
import os
import time
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)
API_KEY = os.environ.get("CAPTCHAAI_API_KEY", "YOUR_API_KEY")

@app.route("/api/solve-captcha", methods=["POST"])
def solve():
    data = request.json
    sitekey = data["sitekey"]
    pageurl = data["pageurl"]

    # Submit to CaptchaAI
    resp = requests.get("https://ocr.captchaai.com/in.php", params={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": "1",
    })
    result = resp.json()

    if result.get("status") != 1:
        return jsonify({"error": result.get("request")}), 400

    task_id = result["request"]

    # Poll
    for _ in range(30):
        time.sleep(5)
        poll = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": "1",
        })
        poll_result = poll.json()
        if poll_result.get("status") == 1:
            return jsonify({"token": poll_result["request"]})
        if poll_result.get("request") != "CAPCHA_NOT_READY":
            return jsonify({"error": poll_result["request"]}), 400

    return jsonify({"error": "Timeout"}), 408

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=3000)

Bước 3: Tích hợp XCUITest

Trong XCUITest của bạn, hãy kích hoạt luồng giải CAPTCHA khi WebView có CAPTCHA tải:

// CaptchaUITests.swift
import XCTest

class CaptchaUITests: XCTestCase {

    func testRegistrationWithCaptcha() throws {
        let app = XCUIApplication()
        app.launchArguments.append("--captcha-test-mode")
        app.launch()

        // Navigate to registration
        app.buttons["Register"].tap()

        // Wait for WebView to load
        let webView = app.webViews.firstMatch
        XCTAssertTrue(webView.waitForExistence(timeout: 15))

        // Trigger CAPTCHA solve via test helper button
        // (The app shows this button only in test mode)
        let solveButton = app.buttons["SolveCaptchaTestHelper"]
        if solveButton.waitForExistence(timeout: 5) {
            solveButton.tap()

            // Wait for solve completion indicator
            let solved = app.staticTexts["CaptchaSolved"]
            XCTAssertTrue(solved.waitForExistence(timeout: 120),
                "CAPTCHA should be solved within 2 minutes")
        }

        // Continue with form submission
        app.buttons["SubmitForm"].tap()

        // Verify success
        let success = app.staticTexts["Registration Complete"]
        XCTAssertTrue(success.waitForExistence(timeout: 10))
    }
}

Hợp đồng cầu

  • Xác định trọng tải yêu cầu mà người chạy thử nghiệm gửi đến dịch vụ trợ giúp, bao gồm URL mục tiêu và siêu dữ liệu thử thách.
  • Trả về phản hồi có cấu trúc kèm theo mã thông báo, thời hạn sử dụng và lý do lỗi để lớp kiểm tra có thể phân nhánh rõ ràng.
  • Giữ nhật ký trình mô phỏng và nhật ký dịch vụ trợ giúp được liên kết bằng mã định danh theo dõi dùng chung để phân loại nhanh hơn.

Khắc phục sự cố

Vấn đề Nguyên nhân Cách xử lý
evaluateJavaScript trả về con số 0 WebView chưa tải xong Đợi webView.isLoading == false trước khi tiêm JS
Phần phụ trợ không thể truy cập được từ Trình mô phỏng localhost không truy cập được Sử dụng 127.0.0.1 hoặc IP mạng của máy Mac; kiểm tra bảo mật vận chuyển ứng dụng
Việc tiêm mã thông báo không kích hoạt cuộc gọi lại Lệnh gọi lại reCAPTCHA được lồng trong đối tượng phức tạp Lặp lại tất cả các thuộc tính của ___grecaptcha_cfg.clients theo cách đệ quy
XCUITest hết thời gian chờ giải quyết Thời gian giải quyết CaptchaAI dài Đặt thời gian chờ kiểm tra thành hơn 120 giây cho các kiểm tra liên quan đến CAPTCHA

Câu hỏi thường gặp

XCUITest có thể thực thi JavaScript trực tiếp trong WKWebView không?

Không. XCUITest tương tác với các thành phần giao diện người dùng nhưng không thể đánh giá JavaScript. Bạn cần một hook thử nghiệm trong mã ứng dụng (chỉ bản dựng gỡ lỗi) để thu hẹp khoảng cách này.

Cách tiếp cận này có hiệu quả trong đường ống CI/CD không?

Có, chạy chương trình phụ trợ của bộ giải trên máy CI và Trình mô phỏng iOS. Dịch vụ bộ giải giao tiếp với CaptchaAI qua HTTPS, hoạt động trong mọi môi trường.

Làm cách nào để ngăn móc thử nghiệm được vận chuyển đến nơi sản xuất?

Bao bọc tất cả mã trợ giúp kiểm tra trong các chỉ thị của trình biên dịch #if DEBUG. Mã sẽ bị loại bỏ khỏi bản phát hành.

Điều gì sẽ xảy ra nếu CAPTCHA có trong SDK WebView của bên thứ ba?

Nếu bạn không kiểm soát WebView, hãy sử dụng Appium thay thế - nó cung cấp khả năng execute_script trên mọi ngữ cảnh WebView mà không cần móc nối phía ứng dụng.

bài viết liên quan

Các bước tiếp theo

Tích hợp CaptchaAI vào quy trình thử nghiệm iOS của bạn —lấy khóa API của bạnvà tự động hóa thông qua các luồng được bảo vệ bằng CAPTCHA.

Hướng dẫn liên quan:

Os comentários estão desativados para este artigo.