Se o teste falha na linha que procura o g-recaptcha-response, o seletor está certo: o elemento é que ainda não existe. Em React, Vue ou Angular o widget nasce por JavaScript na montagem do componente, às vezes só após um clique ou uma troca de rota.
São quatro passos: esperar o widget, achar a sitekey, pedir o token à API e entregá-lo no ponto certo.
O que muda no reCAPTCHA dentro de um SPA
| Página tradicional | SPA |
|---|---|
| script no HTML inicial | script injetado na troca de rota |
| widget no load | widget na montagem do componente |
| sitekey no código-fonte | sitekey no bundle JS |
| envio por POST | envio por XHR/fetch |
Nada disso muda o desafio, só o instante em que ele existe.
O custo disso em uma suíte de QA
Um time em São Paulo roda 40 cenários ponta a ponta em staging.example.com a cada deploy, numa loja em Vue com reCAPTCHA v2 no checkout. Em série, só os desafios já estouram a janela noturna.
Paralelize por thread: no BASIC (US$ 15/mês, 5 threads) cinco cenários avançam juntos; no STANDARD (US$ 30/mês, 15 threads) a suíte cabe em três ondas. A cobrança é por thread simultânea, com resoluções ilimitadas: rodar duas vezes ao dia não muda a fatura. Mantenha tudo em ambiente próprio e com dados fictícios — dado pessoal real traz a LGPD junto.
Passo 1: espere o widget do reCAPTCHA renderizar
Nada de sleep fixo: o tempo de montagem varia com a rede e com o code splitting. Dispare a ação que cria o widget e espere o iframe.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://staging.example.com/qa-login")
# SPA may need a click or navigation to trigger reCAPTCHA
page.click("#show-login-form")
# Wait for reCAPTCHA iframe to appear
recaptcha_frame = page.wait_for_selector(
"iframe[src*='recaptcha']",
timeout=15000
)
print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))
Passo 2: descubra onde está a sitekey
Ela quase nunca está no HTML inicial. Procure nesta ordem:
- no
srcdo iframe, no parâmetrok=; - no atributo
data-sitekeydo container renderizado; - no bundle JavaScript, como último recurso.
# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None
# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
"[data-sitekey]",
"el => el.getAttribute('data-sitekey')"
)
# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
() => {
// Check for grecaptcha render calls
const scripts = document.querySelectorAll('script');
for (const s of scripts) {
const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
if (match) return match[2];
}
return null;
}
""")
Passo 3: envie a sitekey à API da CaptchaAI
Daqui em diante o framework não importa: a CaptchaAI recebe method=userrecaptcha, googlekey e pageurl em in.php e devolve o token em res.php.
import requests
import time
def solve_recaptcha(site_key, page_url):
# Submit task
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
# Poll for result
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("Solve timed out")
Passo 4: entregue o token onde o SPA espera
Um SPA não faz POST de formulário: chama uma API. O token precisa chegar onde o framework o lê.
Opção 1: preencher o textarea oculto
Serve quando o widget renderiza uma vez só e nada reage àquele campo.
token = solve_recaptcha(site_key, "https://staging.example.com/qa-login")
# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
""")
# Submit the form
page.click("#login-button")
Opção 2: chamar o callback registrado
O caminho mais confiável em React e Vue: chame a função de data-callback em vez de escrever num campo que o framework limpa.
# Find the callback function name
callback = page.evaluate("""
() => {
const widget = document.querySelector('.g-recaptcha');
return widget?.getAttribute('data-callback') || null;
}
""")
# Call it with the token
if callback:
page.evaluate(f"window['{callback}']('{token}')")
else:
# Try the default grecaptcha callback
page.evaluate(f"""
if (window.grecaptcha) {{
// Set the response and trigger verification
document.getElementById('g-recaptcha-response').value = '{token}';
// Find and call any registered callbacks
const form = document.querySelector('form');
if (form) form.dispatchEvent(new Event('submit'));
}}
""")
Opção 3: acompanhar a requisição XHR no Puppeteer
Quando o envio sai por fetch para um endpoint REST, confirme que o token viajou no payload.
const puppeteer = require('puppeteer');
async function solveRecaptchaInSPA() {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// Intercept the API call and inject the token
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/login')) {
const postData = JSON.parse(request.postData() || '{}');
// Token already set via page.evaluate — let it pass
request.continue();
} else {
request.continue();
}
});
await page.goto('https://staging.example.com/qa-login');
await page.waitForSelector('[data-sitekey]');
const siteKey = await page.$eval(
'[data-sitekey]',
el => el.getAttribute('data-sitekey')
);
// Solve with CaptchaAI (implementation omitted — same as Python)
const token = await solveCaptcha(siteKey, page.url());
// Inject token and trigger callback
await page.evaluate((t) => {
document.getElementById('g-recaptcha-response').value = t;
const callback = document.querySelector('.g-recaptcha')
?.getAttribute('data-callback');
if (callback && window[callback]) {
window[callback](t);
}
}, token);
}
React: espere o componente montar
Com react-google-recaptcha a montagem é assíncrona: espere o .g-recaptcha ficar anexado.
# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")
# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
".g-recaptcha", "el => el.dataset.sitekey"
)
Vue: o v-if adia a renderização do widget
Com vue-recaptcha sob um v-if, o widget só existe depois da interação que satisfaz a condição.
# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")
Angular: o lazy loading atrasa o widget
Módulos sob demanda adiam a renderização: aceite mais de um seletor.
# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")
Troca de rota sem recarregar a página
A URL muda sem recarregar e nenhum evento de load avisa a automação: espere o iframe a cada navegação interna.
# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")
# Navigate within the SPA
page.click("a[href='/login']")
# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)
Erros comuns ao resolver reCAPTCHA em SPA
| Sintoma | Causa | Correção |
|---|---|---|
g-recaptcha-response ausente |
componente não montou | wait_for_selector com timeout folgado |
| token no campo, nada é enviado | callback não foi chamado | chame a função de data-callback |
| sitekey fora do código-fonte | está no bundle | leia o src do iframe |
| funciona uma vez, falha depois | re-render limpa o campo | escreva o token junto ao envio |
| widget volta ao início | reatividade sobrescreve o valor | prefira data-callback ao textarea |
Perguntas frequentes
O token expira enquanto o componente re-renderiza?
Pode. O token vale 120 segundos e aceita uma única validação: resolva perto do envio.
A sitekey muda de uma rota para outra?
Costuma ser a mesma, mas login e checkout podem carregar widgets diferentes. Leia a sitekey em cada rota.
Quantas threads preciso para rodar cenários em paralelo?
Uma por cenário simultâneo. O BASIC (US$ 15/mês) entrega 5 threads e o ADVANCE (US$ 90/mês), 50. Com resoluções ilimitadas, você dimensiona simultaneidade, não volume.
E se o SPA usar hCaptcha no lugar do reCAPTCHA?
Não dá: o hCaptcha não é suportado, nem o FunCaptcha. A cobertura inclui reCAPTCHA v2 e v3, Cloudflare Turnstile e Challenge, GeeTest v3 e CAPTCHAs de imagem e grade.
Artigos relacionados
- Resolver o callback do reCAPTCHA v2 pela API
- reCAPTCHA v2 e Turnstile no mesmo site
- Como funciona o callback do reCAPTCHA v2
Próximas etapas
Rode os quatro passos no seu ambiente de homologação: pegue sua chave de API da CaptchaAI.