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:
- Detecta o CAPTCHA no WebView durante a execução do teste
- Extrai a chave do site programaticamente
- Resolve via CaptchaAI
- 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
- Como resolver o retorno de chamada do Recaptcha V2 usando API
- Zapier Captchaai sem automação de código
- Torniquete Recaptcha V2 no mesmo local
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: