Skrip Playwright atau Selenium Anda mencari .g-recaptcha atau .cf-turnstile begitu page.goto() selesai, tapi elemennya belum ada di DOM — waitForSelector pun timeout, padahal situs yang sama jelas menampilkan CAPTCHA saat dibuka manual di browser. Ini bukan bug pada skrip Anda. Progressive Web App merender shell aplikasinya lebih dulu lewat React, Vue, atau Angular, baru menyuntikkan widget CAPTCHA ke DOM setelah JavaScript selesai berjalan. CaptchaAI tetap bisa menyelesaikan reCAPTCHA v2 dan Cloudflare Turnstile di PWA — yang berubah hanya cara Anda mendeteksi dan mengekstrak sitekey-nya.
Panduan ini membahas empat langkah praktis: mendeteksi CAPTCHA yang dirender dinamis, mengatasi cache Service Worker yang membuat sitekey basi, memantau navigasi sisi klien yang tidak memuat ulang halaman, serta versi alternatif memakai Selenium untuk tim yang lebih nyaman dengan Python.
Inti masalahnya bukan CaptchaAI gagal menyelesaikan CAPTCHA — skrip Anda membaca DOM sebelum widgetnya ada. Tunggu event yang tepat, dan alurnya sama seperti situs biasa.
Tiga gejala yang biasa muncul:
waitForSelectortimeout padahal elemen terlihat jelas saat dibuka manual- Sitekey yang terbaca di percobaan pertama tiba-tiba
nullsetelah pindah rute - Widget CAPTCHA baru muncul saat masuk
/checkoutatau/login, bukan saat load pertama
Kenapa CAPTCHA di PWA Perlu Penanganan Berbeda
Situs tradisional menyajikan CAPTCHA langsung di respons HTML awal. PWA berbeda dalam beberapa hal mendasar:
| Aspek | Situs Tradisional | PWA |
|---|---|---|
| Pemuatan CAPTCHA | Ada di HTML awal | Dirender oleh JavaScript setelah halaman termuat |
| Navigasi halaman | Reload halaman penuh | Client-side routing, tanpa reload |
| Service Worker | Tidak ada | Meng-cache resource, bisa mencegat request |
| Ketersediaan DOM | Langsung tersedia | Baru ada setelah framework selesai merender |
| Request jaringan | Langsung | Bisa dicegat oleh Service Worker |
Karena CAPTCHA muncul setelah shell selesai render, deteksinya butuh observer — bukan sekadar page.goto() lalu membaca DOM. Berikut alur empat langkahnya.
Langkah 1: Deteksi CAPTCHA yang Dirender Dinamis
Kesalahan paling sering terjadi: mengekstrak sitekey sebelum framework PWA selesai merender widget CAPTCHA-nya. Solusinya pakai
MutationObserveratau sinyal khusus framework, supaya skrip menunggu elemen benar-benar muncul di DOM — bukan menebak dengan jeda waktu tetap.
// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
async function detectCaptchaInPWA(page) {
// Wait for the PWA app shell to render
await page.waitForLoadState('networkidle');
// Use MutationObserver to detect dynamically loaded CAPTCHAs
const captchaInfo = await page.evaluate(() => {
return new Promise((resolve) => {
// Check if CAPTCHA is already present
const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
if (existing) {
resolve({
type: existing.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: existing.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
// Watch for CAPTCHA elements added dynamically
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
const captcha = node.matches?.('.g-recaptcha, .cf-turnstile')
? node
: node.querySelector?.('.g-recaptcha, .cf-turnstile');
if (captcha) {
observer.disconnect();
resolve({
type: captcha.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: captcha.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Timeout after 15 seconds
setTimeout(() => {
observer.disconnect();
resolve(null);
}, 15000);
});
});
return captchaInfo;
}
async function main() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example-pwa.com/login');
const captcha = await detectCaptchaInPWA(page);
if (!captcha) {
console.log('No CAPTCHA detected');
await browser.close();
return;
}
console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);
// Solve with CaptchaAI
const token = await solveCaptcha(captcha);
console.log(`Token: ${token.substring(0, 50)}...`);
// Inject token
await injectToken(page, captcha.type, token);
// Submit form
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
console.log('Form submitted');
await browser.close();
}
async function solveCaptcha(captcha) {
const params = {
key: API_KEY,
pageurl: captcha.pageurl,
json: '1',
};
if (captcha.type === 'recaptcha_v2') {
params.method = 'userrecaptcha';
params.googlekey = captcha.sitekey;
} else {
params.method = 'turnstile';
params.sitekey = captcha.sitekey;
}
const submit = await axios.get(
'https://ocr.captchaai.com/in.php', { params }
);
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') {
throw new Error(poll.data.request);
}
}
throw new Error('Timeout');
}
async function injectToken(page, type, token) {
if (type === 'recaptcha_v2') {
await page.evaluate((t) => {
document.getElementById('g-recaptcha-response').value = t;
try {
const clients = ___grecaptcha_cfg.clients;
Object.keys(clients).forEach((k) => {
Object.keys(clients[k]).forEach((j) => {
if (clients[k][j]?.callback) clients[k][j].callback(t);
});
});
} catch (e) {}
}, token);
} else {
await page.evaluate((t) => {
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = t;
const cb = document.querySelector('.cf-turnstile')
?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb](t);
}, token);
}
}
main().catch(console.error);
Langkah 2: Atasi Cache Service Worker
Service Worker bisa menyimpan skrip CAPTCHA versi lama di cache, sehingga widget yang tampil menjadi basi. Header Cache-Control: no-cache pada request recaptcha dan turnstile memaksa browser mengambil versi terbaru.
Dua pemicu paling umum:
- Widget baru diperbarui di sisi provider, tapi Service Worker masih menyajikan cache lama
- Automation berjalan tepat setelah deploy, sebelum cache sempat basi sendiri
// Intercept and validasi Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
await page.route('**/turnstile/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
Langkah 3: Pantau Navigasi Sisi Klien
PWA berpindah rute lewat history.pushState, bukan memuat ulang halaman — jadi event load standar tidak pernah terpicu saat pengguna masuk ke rute yang dilindungi CAPTCHA seperti /checkout. Pantau perubahan rute secara eksplisit, lalu jalankan ulang deteksi CAPTCHA pada tiap rute baru:
// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
window.dispatchEvent(new Event('pwa-route-change'));
};
});
page.on('console', async (msg) => {
// React to route changes if needed
});
// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);
Kalau Anda menguji rute seperti /checkout dari server di kawasan Asia Tenggara — AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta) — latency ke origin biasanya lebih rendah, tapi waktu render shell tetap ditentukan oleh ukuran bundle JS-nya, bukan lokasi server pengujian Anda. Karena rute semacam ini sering berisi data pengguna, jalankan pengujian dari akun staging milik Anda sendiri, bukan data transaksi nyata — sejalan dengan semangat UU Pelindungan Data Pribadi (UU 27/2022).
Langkah 4: Alternatif dengan Selenium (Python)
Tim yang lebih nyaman di Python bisa memakai pola yang sama lewat Selenium: tunggu elemen .g-recaptcha atau .cf-turnstile muncul dengan WebDriverWait, ambil sitekey dan pageurl, kirim ke CaptchaAI, lalu suntikkan token setelah polling selesai.
# pwa_captcha_selenium.py
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")
# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)
sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")
# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
params["method"] = "turnstile"
params["sitekey"] = sitekey
else:
params["method"] = "userrecaptcha"
params["googlekey"] = sitekey
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["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",
})
if poll.json().get("status") == 1:
token = poll.json()["request"]
break
else:
raise TimeoutError("CAPTCHA not solved")
# Inject token
driver.execute_script(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
""")
driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()
Validasi Kesegaran Token Sebelum Submit
Token CAPTCHA yang sudah tersimpan bisa basi lebih cepat di PWA dibanding situs biasa, karena satu sesi browser sering melewati beberapa rute dan perubahan status tanpa reload sama sekali. Batalkan validasi token yang di-cache setiap kali terjadi perubahan rute, perubahan status autentikasi, atau update Service Worker — ketiganya mengubah konteks halaman tempat token itu awalnya diambil. Simpan hanya state minimum yang perlu untuk menyambungkan token ke tampilan yang sedang aktif, dan verifikasi kesegarannya tepat sebelum submit — jangan berasumsi hasil penyelesaian di latar belakang masih valid begitu saja.
Debug Masalah yang Sering Muncul
Sebelum menelusuri tabel, cek tiga hal ini:
- Apakah
waitForLoadState('networkidle')(atauWebDriverWait) sudah selesai sebelum membaca DOM? - Apakah Service Worker menyajikan resource CAPTCHA dari cache lama?
- Apakah navigasi ke rute terproteksi memicu
pwa-route-changeseperti Langkah 3?
| Masalah | Penyebab | Solusi |
|---|---|---|
| Elemen CAPTCHA tidak pernah muncul | Rute PWA belum selesai dirender | Pakai waitForSelector dengan timeout lebih panjang; pastikan client-side routing sudah selesai |
| Sitekey basi setelah navigasi | Service Worker menyajikan HTML dari cache | Set header no-cache untuk resource terkait CAPTCHA |
| Callback penyuntikan token tidak ditemukan | Framework PWA mengelola state dengan caranya sendiri | Cek state management React/Vue/Angular; picu update state form secara manual |
| Form ter-submit tapi token tidak terkirim | Handler form SPA membaca dari state komponen, bukan dari DOM | Update juga state framework-nya (misalnya React ref, reactive property Vue) |
Pertanyaan Seputar CAPTCHA di PWA
Apakah CAPTCHA di PWA memakai tipe yang berbeda dari situs biasa?
Tidak. PWA tetap memakai reCAPTCHA v2, Cloudflare Turnstile, dan widget CAPTCHA umum lainnya — CaptchaAI menanganinya lewat method API yang sama. Yang berbeda hanya waktu kemunculannya: dirender oleh JavaScript, bukan ikut HTML awal.
Apakah cara ini tetap berlaku untuk PWA yang diinstal sebagai aplikasi standalone (bukan dibuka lewat tab browser)?
Ya. PWA yang diinstal tetap berjalan di WebView browser yang sama, jadi observer dan proses deteksi sitekey pada panduan ini berlaku sama persis — CaptchaAI hanya memerlukan sitekey dan URL halaman, terlepas dari PWA dibuka lewat tab atau ikon terinstal di layar utama.
Berapa lama biasanya CAPTCHA muncul setelah PWA selesai dimuat?
Bervariasi per framework, tapi umumnya dalam hitungan detik setelah shell aplikasi selesai render. Skrip pada Langkah 1 memakai timeout 15 detik untuk MutationObserver — cukup untuk sebagian besar PWA berbasis React, Vue, atau Angular; naikkan nilainya kalau target Anda memuat data tambahan sebelum menampilkan form.
Bagaimana kalau tim front-end memakai Shadow DOM atau web component kustom untuk widget CAPTCHA-nya?
Selector querySelector('.g-recaptcha, .cf-turnstile') pada skrip Langkah 1 tidak menembus Shadow DOM tertutup. Kalau widget dibungkus komponen kustom, sesuaikan observer untuk membaca element.shadowRoot secara eksplisit sebelum mencari sitekey-nya — cek dulu dengan DevTools apakah CAPTCHA-nya benar-benar berada di dalam shadow tree.
Apakah pendekatan ini berlaku juga untuk framework seperti Next.js atau Nuxt?
Ya. Prinsipnya sama: tunggu render selesai dengan observer, pantau perubahan rute, dan cek ulang cache Service Worker. Detail selector CSS bisa berbeda tergantung komponen CAPTCHA yang dipakai, tapi alur deteksi-ekstraksi-penyelesaian di panduan ini berlaku lintas framework.
Artikel Terkait
- Cara menyelesaikan callback reCAPTCHA v2 lewat API
- GeeTest vs Cloudflare Turnstile: mana yang lebih cocok?
- Mengatasi error 403 Cloudflare Turnstile setelah token diperbaiki
Langkah Selanjutnya
Mulai selesaikan CAPTCHA di PWA Anda — dapatkan API key CaptchaAI dan pasang deteksi dinamis ini ke pipeline automation Anda hari ini.
Panduan terkait: