CAPTCHA di dalam WebView adalah penghambat paling menjengkelkan dalam pengujian Espresso — matcher Espresso biasa tidak bisa mengetuk gambar, jadi alur checkout atau login langsung macet begitu reCAPTCHA v2 muncul. Solusinya ada tiga langkah:
- Deteksi sitekey dan URL halaman lewat JavaScript yang dievaluasi di dalam WebView.
- Selesaikan CAPTCHA lewat backend solver yang memanggil CaptchaAI.
- Suntikkan token hasilnya kembali ke elemen
#g-recaptcha-responsesebelum test lanjut ke assertion berikutnya.
Panduan ini menjabarkan ketiga langkah itu — helper debug Kotlin, backend solver Python, dan skrip test Espresso yang menyatukannya — plus tabel troubleshooting untuk error yang paling sering muncul di device farm CI.
Kapan CAPTCHA WebView Menghambat Test Espresso Anda
Sebagian besar aplikasi e-commerce dan travel di Android tidak membangun ulang halaman checkout secara native. Mereka memuat halaman pihak ketiga di dalam WebView apa adanya, lengkap dengan proteksi CAPTCHA-nya. CAPTCHA WebView biasanya muncul di titik-titik berikut:
- Checkout pembayaran pihak ketiga sebelum transaksi diproses.
- Halaman login/SSO yang di-embed dari penyedia autentikasi eksternal.
- Formulir pendaftaran yang dimuat dari backend web, bukan native form Android.
Saat test instrumented Espresso mencoba memverifikasi salah satu alur ini, CAPTCHA memblokirnya total karena Espresso tidak bisa "mengklik gambar" seperti manusia — dan tidak ada matcher bawaan untuk menyelesaikan reCAPTCHA v2.
Lingkungan pengujian: Android Studio, Kotlin, Espresso, AndroidX Test, API CaptchaAI, backend Python.
Tip infrastruktur: tim QA yang menjalankan backend solver ini di CI sebaiknya deploy layanan Python ke region terdekat dengan runner-nya — AWS
ap-southeast-3(Jakarta) atauap-southeast-1(Singapura) — supaya latensi polling ke CaptchaAI tidak menambah waktu tunggu saat device farm menjalankan puluhan test paralel dengan batas waktu ketat.
Langkah 1: Tambahkan Helper Debug ke WebView Android
Buat helper khusus source set debug yang bisa mengevaluasi JavaScript di dalam WebView aplikasi Anda:
// CaptchaTestHelper.kt — debug source set only
package com.example.app.testing
import android.webkit.JavascriptInterface
import android.webkit.WebView
import kotlinx.coroutines.*
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
class CaptchaTestHelper(private val webView: WebView) {
private var detectedSitekey: String? = null
private var detectedPageUrl: String? = null
private var solvedToken: String? = null
@JavascriptInterface
fun onCaptchaDetected(sitekey: String, pageurl: String) {
detectedSitekey = sitekey
detectedPageUrl = pageurl
}
fun detectCaptcha() {
webView.post {
webView.evaluateJavascript("""
(function() {
var el = document.querySelector('.g-recaptcha');
if (el) {
CaptchaHelper.onCaptchaDetected(
el.getAttribute('data-sitekey'),
window.location.href
);
return 'found';
}
return 'not_found';
})();
""", null)
}
}
suspend fun solveAndInject(): Boolean = withContext(Dispatchers.IO) {
val sitekey = detectedSitekey ?: return@withContext false
val pageurl = detectedPageUrl ?: return@withContext false
// Call backend solver
val client = OkHttpClient.Builder()
.callTimeout(java.time.Duration.ofMinutes(3))
.build()
val body = JSONObject().apply {
put("captchaType", "recaptcha_v2")
put("sitekey", sitekey)
put("pageurl", pageurl)
}.toString().toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("http://10.0.2.2:3000/api/solve-captcha") // Host loopback for emulator
.post(body)
.build()
val response = client.newCall(request).execute()
val json = JSONObject(response.body?.string() ?: "")
val token = json.optString("token", "")
if (token.isEmpty()) return@withContext false
solvedToken = token
// Inject token on main thread
withContext(Dispatchers.Main) {
webView.evaluateJavascript("""
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) {}
""", null)
}
return@withContext true
}
companion object {
fun attach(webView: WebView): CaptchaTestHelper {
val helper = CaptchaTestHelper(webView)
webView.addJavascriptInterface(helper, "CaptchaHelper")
return helper
}
}
}
Langkah 2: Bangun Backend Solver CAPTCHA dengan Python
Jalankan solver Python berikut di mesin development Anda selama eksekusi test — helper di Langkah 1 memanggilnya lewat 10.0.2.2, alamat loopback host khusus emulator Android:
# android_test_solver.py
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
# Submit to CaptchaAI
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": data["sitekey"],
"pageurl": data["pageurl"],
"json": "1",
})
result = resp.json()
if result.get("status") != 1:
return jsonify({"error": result.get("request")}), 400
task_id = result["request"]
# Poll for result
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)
Menyesuaikan Waktu Tunggu Polling
Loop for _ in range(30) di atas menunggu maksimal 150 detik (30 percobaan × 5 detik jeda) sebelum menyerah. reCAPTCHA v2 biasanya selesai dalam waktu di bawah 60 detik dengan tingkat keberhasilan tinggi pada tipe yang didukung, jadi ambang 150 detik memberi ruang aman untuk fluktuasi jaringan tanpa membuat test Espresso Anda menggantung terlalu lama. Kalau test suite CI Anda punya batas waktu ketat per test, dua opsi yang masuk akal:
- Perpendek total timeout (mis. 20 percobaan × 5 detik = 100 detik) dan biarkan test gagal cepat kalau solve benar-benar bermasalah.
- Perbesar jeda antar-polling menjadi 8–10 detik untuk mengurangi jumlah request ke
res.phptanpa memperpendek total waktu tunggu.
Langkah 3: Satukan Semuanya di Test Espresso
Skrip test berikut memanggil helper WebView, menunggu deteksi CAPTCHA, memicu solve, lalu memverifikasi checkout selesai setelah token disuntikkan:
// CheckoutCaptchaTest.kt
package com.example.app
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CheckoutCaptchaTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testCheckoutWithCaptcha() {
// Navigate to checkout
onView(withId(R.id.checkout_button)).perform(click())
// Wait for WebView to load
Thread.sleep(5000)
// Access the WebView and attach helper
activityRule.scenario.onActivity { activity ->
val webView = activity.findViewById<android.webkit.WebView>(R.id.webview)
val helper = CaptchaTestHelper.attach(webView)
helper.detectCaptcha()
// Wait for detection
Thread.sleep(2000)
// Solve and inject
runBlocking {
val solved = helper.solveAndInject()
assert(solved) { "CAPTCHA should be solved successfully" }
}
}
// Continue with form submission after pengaturan token
Thread.sleep(1000)
// Verify checkout completed
onView(withText("Order Confirmed")).check(
androidx.test.espresso.assertion.ViewAssertions.matches(isDisplayed())
)
}
}
Pertanyaan Umum
Apa bedanya menangani CAPTCHA di WebView Espresso dengan otomasi native lewat Appium?
Pendekatan ini berlaku karena CAPTCHA muncul di WebView yang JavaScript-nya bisa dievaluasi langsung. Untuk automation native yang menyentuh komponen di luar WebView, pendekatan Appium biasanya lebih tepat — lihat panduan Appium di bagian bawah artikel ini.
Plan CaptchaAI apa yang cocok untuk pipeline CI dengan banyak device paralel?
CaptchaAI menagih per thread yang sedang aktif menyelesaikan CAPTCHA, bukan per solve — jadi ukuran plan yang tepat mengikuti jumlah device/emulator yang berjalan bersamaan, bukan jumlah test per hari:
| Plan | Harga | Thread | Cocok untuk |
|---|---|---|---|
| BASIC | $15/bulan | 5 | Beberapa emulator lokal yang jalan bergantian |
| STANDARD | $30/bulan | 15 | Device farm CI dengan test paralel penuh |
Kalau thread lebih sedikit dari jumlah device yang solve CAPTCHA bersamaan, sebagian test akan mengantre menunggu thread kosong — bukan gagal, hanya lebih lambat.
Apakah pendekatan yang sama berlaku untuk reCAPTCHA v3 di WebView Android?
Ya, prinsipnya sama — deteksi sitekey, kirim ke CaptchaAI, suntikkan token. Bedanya v3 tidak menampilkan widget yang perlu diklik, jadi cukup sesuaikan captchaType di body request, karena v3 biasanya mengembalikan skor lewat callback JavaScript, bukan elemen #g-recaptcha-response.
Bagaimana cara mencegah helper pengujian ini ikut ke build rilis produksi?
Tempatkan CaptchaTestHelper di source set yang tepat, bukan di source set utama aplikasi:
src/debug/java/— helper CAPTCHA masuk di sini, hanya ikut build debug.src/main/java/— kode produksi; jangan pernah taruh helper pengujian di sini.
Build variant Android otomatis mengecualikan source debug dari build rilis, jadi kode ini tidak pernah masuk ke APK yang dipublikasikan ke Play Store.
Bagaimana dengan reCAPTCHA Enterprise di WebView aplikasi Android?
Alurnya identik, tapi Anda perlu sitekey Enterprise dan mungkin parameter tambahan seperti enterprise: 1 ke CaptchaAI saat submit task.
Error WebView dan Espresso yang Sering Muncul
| Masalah | Penyebab | Solusi |
|---|---|---|
10.0.2.2 tidak terjangkau |
Tidak berjalan di Android Emulator | Gunakan IP host asli untuk perangkat fisik; 10.0.2.2 khusus emulator |
Callback evaluateJavascript bernilai null |
WebView belum selesai dimuat | Tambahkan listener WebViewClient.onPageFinished() sebelum evaluasi |
addJavascriptInterface tidak berfungsi |
JavaScript dinonaktifkan | Set webView.settings.javaScriptEnabled = true |
| Request diblokir kebijakan Cleartext | HTTP ke localhost di Android 9+ | Tambahkan android:usesCleartextTraffic="true" di AndroidManifest.xml (khusus debug) |
| Solve selalu timeout di CI, lancar di lokal | Runner CI di region jauh dari API CaptchaAI, latensi polling tinggi | Deploy backend solver ke region terdekat (lihat tip infrastruktur di atas) atau perbesar total timeout |
Kalau masalah tetap muncul setelah kelima perbaikan di atas, periksa log respons in.php — status selain 1 biasanya berarti sitekey atau pageurl yang terkirim ke CaptchaAI salah, bukan masalah di sisi WebView atau Espresso.
Artikel Terkait
- Cara Menyelesaikan Callback reCAPTCHA v2 lewat API
- Membangun Pipeline Pengujian Otomatis dengan CaptchaAI
- Menangani reCAPTCHA v2 dan Turnstile di Satu Situs yang Sama
Langkah Selanjutnya
Hentikan CAPTCHA WebView yang menghambat test suite Android Anda — ambil API key CaptchaAI dan pasang backend solver dalam hitungan menit.
Panduan terkait: