При тестировании приложений iOS с помощью XCUITest часто встречаются CAPTCHA во встроенных WKWebViews — формах входа, платежных шлюзах и сторонних интеграциях, которые создают проблемы с reCAPTCHA.CaptchaAIрешает эти проблемы, чтобы ваши UI-тесты могли выполнять сквозные потоки без ручного вмешательства.
В этом руководстве показано, как обнаруживать CAPTCHA в WKWebView во время запуска XCUITest, решать их с помощью CaptchaAI из сопутствующей службы и внедрять токен обратно в веб-контент.
Реальный сценарий
Ваше приложение iOS загружает регистрационную форму в WKWebView. Форма включает reCAPTCHA v2. Во время автоматического тестирования эта CAPTCHA блокирует ход тестирования. Вам нужно решение, которое:
- Обнаруживает CAPTCHA в WebView во время выполнения теста.
- Извлекает ключ сайта программно
- Решает это через CaptchaAI
- Вводит токен, чтобы форма могла быть отправлена.
Среда: Xcode 15+, Swift, XCUITest, средство запуска тестов macOS, CaptchaAI API.
Архитектура
XCUITest не может напрямую выполнять JavaScript в WKWebView. В этом подходе используется вспомогательная конечная точка, которую приложение вызывает во время тестирования:
| Компонент | Роль |
|---|---|
| XCUITest | Управляет пользовательским интерфейсом, запускает решение CAPTCHA с помощью помощника по тестированию |
| API помощника по тестированию | Получает ключ сайта + URL-адрес, вызывает CaptchaAI, возвращает токен. |
| Тестовый крючок приложения | Оценивает JavaScript в WKWebView для обнаружения /inject |
| CaptchaAI API | Решает задачу CAPTCHA |
Шаг 1. Добавьте тестовый хук в приложение
В контроллере WKWebView вашего приложения добавьте обработчик CAPTCHA тестового режима, который можно активировать с помощью идентификаторов доступности или схемы 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
Шаг 2. Служба серверного решателя
Во время тестирования запустите локальную службу решателя, которая взаимодействует с 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)
Шаг 3: Интеграция XCUITest
В XCUITest запустите поток решения CAPTCHA, когда загружается WebView с CAPTCHA:
// 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))
}
}
Поиск неисправностей
| Проблема | Причина | Исправить |
Следующие шаги
- CaptchaAI Quickstart: ваше первое решение CAPTCHA за 5 минут
- Как решить reCAPTCHA v2 через API: пошаговое руководство
- Как решить Cloudflare Turnstile через API
- Как решить GeeTest v3 с помощью API
Мостовой контракт
- Определите полезные данные запроса, которые исполнитель теста отправляет вспомогательной службе, включая целевой URL-адрес и метаданные запроса.
- Верните структурированный ответ с токеном, сроком действия и причиной ошибки, чтобы тестовый уровень мог правильно разветвляться.
- Сохраняйте журналы симулятора и журналы вспомогательных служб связанными общим идентификатором трассировки для более быстрой сортировки.