Integrações

Manipulação de CAPTCHA de automação iOS com XCUITest e CaptchaAI

Os testes de aplicativos iOS com XCUITest geralmente atingem CAPTCHAs em WKWebViews incorporados – formulários de login, gateways de pagamento e integrações de terceiros que apresentam desafios de reCAPTCHA.CaptchaAIresolve isso para que seus testes de IU possam concluir fluxos de ponta a ponta sem intervenção manual.

Este guia mostra como detectar CAPTCHAs no WKWebView durante a execução do XCUITest, resolvê-los via CaptchaAI de um serviço complementar e injetar o token de volta no conteúdo da web.

Cenário do mundo real

Seu aplicativo iOS carrega um formulário de registro em um WKWebView. O formulário inclui reCAPTCHA v2. Durante os testes automatizados, este CAPTCHA bloqueia a progressão do teste. Você precisa de uma solução que:

  1. Detecta o CAPTCHA no WebView durante a execução do teste
  2. Extrai a chave do site programaticamente
  3. Resolve via CaptchaAI
  4. Injeta o token para que o formulário possa ser enviado

Ambiente: Xcode 15+, Swift, XCUITest, executor de testes macOS, API CaptchaAI.

Arquitetura

O XCUITest não pode executar JavaScript diretamente em um WKWebView. A abordagem usa um endpoint auxiliar que o aplicativo chama durante o teste:

Componente Função
Teste XCUIT Conduz a UI, aciona a resolução do CAPTCHA por meio do auxiliar de teste
API auxiliar de teste Recebe sitekey + URL, chama CaptchaAI, retorna token
Gancho de teste de aplicativo Avalia JavaScript em WKWebView para detectar/inject
API CaptchaAI Resolve o desafio CAPTCHA

Etapa 1: adicionar um gancho de teste ao aplicativo

No controlador WKWebView do seu aplicativo, adicione um manipulador CAPTCHA em modo de teste que pode ser acionado por meio de identificadores de acessibilidade ou esquema de 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

Etapa 2: serviço solucionador de back-end

Execute um serviço de resolução local durante o teste que se comunica com 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)

Etapa 3: Integração XCUITest

No seu XCUITest, acione o fluxo de resolução CAPTCHA quando o WebView com um CAPTCHA for carregado:

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

Contrato ponte

  • Defina a carga útil da solicitação que o executor de teste envia ao serviço auxiliar, incluindo URL de destino e metadados de desafio.
  • Retorne uma resposta estruturada com token, expiração e motivo do erro para que a camada de teste possa ramificar de forma limpa.
  • Mantenha os logs do simulador e os logs do serviço auxiliar vinculados por um identificador de rastreamento compartilhado para uma triagem com menor latência.

Solução de problemas

Problema Causa Correção
evaluateJavaScript retorna zero WebView não terminou de carregar Aguarde webView.isLoading == false antes de injetar JS
Back-end não acessível pelo Simulador localhost não acessível Use 127.0.0.1 ou o IP da rede do Mac; verifique a segurança do transporte de aplicativos
A envio controlado ao endpoint QA não dispara retorno de chamada retorno de chamada reCAPTCHA aninhado em objeto complexo Iterar todas as propriedades de ___grecaptcha_cfg.clients recursivamente
Tempo limite do XCUITest aguardando solução Longo tempo de resolução de CaptchaAI Defina o tempo limite do teste para mais de 120 segundos para testes relacionados ao CAPTCHA

Perguntas frequentes

O XCUITest pode executar JavaScript diretamente no WKWebView?

Não. O XCUITest interage com elementos da UI, mas não pode avaliar JavaScript. Você precisa de um gancho de teste no código do aplicativo (somente compilação de depuração) para preencher essa lacuna.

Essa abordagem funcionará em pipelines CI/CD?

Sim, execute o back-end do solucionador na máquina CI e no simulador iOS. O serviço solucionador se comunica com CaptchaAI via HTTPS, que funciona em qualquer ambiente.

Como evito que o gancho de teste seja enviado para produção?

Envolva todo o código auxiliar de teste nas diretivas do compilador #if DEBUG. O código será retirado das compilações de lançamento.

E se o CAPTCHA estiver em um SDK WebView de terceiros?

Se você não controla o WebView, use o Appium - ele fornece recursos execute_script em qualquer contexto do WebView sem precisar de ganchos do lado do aplicativo.

Artigos relacionados

Próximas etapas

Integre CaptchaAI ao seu pipeline de testes do iOS —obtenha sua chave APIe automatizar através de fluxos protegidos por CAPTCHA.

Guias relacionados:

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