Em um Progressive Web App, o CAPTCHA não vem junto com o HTML inicial: o reCAPTCHA v2 ou o Cloudflare Turnstile só chegam ao DOM depois que o framework (React, Vue, Angular) termina de montar a rota. Essa diferença de tempo, não o CAPTCHA em si, é o que quebra a maioria das automações escritas para sites tradicionais.
A CaptchaAIcuida da resolução; o trabalho é ajustar a detecção. Este guia mostra, em ordem, como capturar o CAPTCHA, lidar com o cache do Service Worker, acompanhar a navegação client-side e resolvê-lo com Playwright, Selenium e a API da CaptchaAI.
O que muda em um PWA:
- O widget só aparece depois que o framework monta a rota.
- O Service Worker pode servir um script em cache, com sitekey desatualizada.
- Trocar de tela não recarrega a página nem dispara
waitForNavigation.
Por que a detecção tradicional falha aqui
| Aspecto | Sítio Tradicional | PWA |
|---|---|---|
| Carregando CAPTCHA | No HTML inicial | Renderizado por JavaScript após o carregamento da página |
| Navegação na página | Recarregar página inteira | Roteamento do lado do cliente (sem recarga) |
| Service Worker | Não presente | Armazena recursos em cache, pode interceptar solicitações |
| Disponibilidade do DOM | Imediato | Depois da renderização do framework |
| Solicitações de rede | Direto | Pode ser interceptado pelo Service Worker |
Uma automação escrita para sites tradicionais falha silenciosamente aqui: procura o widget antes de ele existir e segue sem token, sem erro nenhum. Rodar workers em sa-east-1 (São Paulo) reduz o RTT, mas não resolve o atraso de renderização do framework — são causas diferentes.
Etapa 1: capture o CAPTCHA depois que o framework renderiza
Sinais do problema:
- Sitekey
nullmesmo com o seletor certo. - CAPTCHA visível na tela, mas não encontrado pelo script.
Observe o DOM em vez de assumir um tempo fixo de espera — use um MutationObserver ou um sinal específico do framework:
// 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);
Etapa 2: evite scripts de CAPTCHA presos no cache do Service Worker
Sintoma típico: a sitekey "funcionava ontem" e some logo depois de uma navegação.
Force uma busca fresca para os recursos de CAPTCHA:
// Intercept and validação autorizada 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' } });
});
Etapa 3: sincronize a detecção com a navegação client-side
PWAs trocam de tela sem recarregar: ir de /login para /checkout não aciona waitForNavigation. Monitore a troca de rota diretamente:
// 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);
Etapa 4: versão em Python com Selenium
Prefira esta rota quando:
- O pipeline de QA já é Python.
- O time evita manter duas linguagens de automação.
# 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()
Boas práticas para invalidar tokens antigos
- Descarte tokens em cache quando a rota mudar, o estado de autenticação mudar ou o Service Worker atualizar o app shell.
- Guarde só o estado necessário para reconectar o token ao formulário ativo.
- Confirme a validade do token logo antes do envio.
Solução de problemas
Esses sintomas costumam aparecer juntos: sitekey ausente e cache de Service Worker ativo.
| Problema | Causa | Correção |
|---|---|---|
| O elemento CAPTCHA nunca aparece | PWA ainda não renderizou a rota | Use waitForSelector com timeout estendido; garantir que o roteamento do lado do cliente seja concluído |
| Sitekey obsoleto após navegação | Service Worker serviu HTML em cache | Ignorar cabeçalhos de cache para recursos relacionados ao CAPTCHA |
| Callback de injeção do token não é encontrado | O framework do PWA gerencia o estado de maneira diferente | Verifique o gerenciamento de estado React/Vue/Angular; acione a atualização do estado do formulário |
| O envio do formulário não envia token | O manipulador de formulário SPA lê o estado do componente, não o DOM | Atualize também o estado da estrutura (por exemplo, React ref, propriedade reativa Vue) |
Perguntas frequentes
Por que o Playwright não encontra o elemento CAPTCHA logo após o page.goto()?
Porque ele ainda não existe nesse instante. Troque a espera fixa por um MutationObserver (Etapa 1) ou por waitForSelector com timeout maior.
Vale a pena desativar o Service Worker durante os testes automatizados?
Em QA controlado, sim. Em produção, prefira interceptar as rotas do CAPTCHA com Cache-Control: no-cache.
O CaptchaAI resolve CAPTCHA em SPAs feitas com React, Vue ou Angular sem ajuste extra?
Sim — a chamada à API é idêntica em qualquer stack. O que muda por framework é como você injeta o token de volta no estado do componente.
Quanto tempo leva para resolver o CAPTCHA depois que ele é detectado?
Depende do tipo: Cloudflare Turnstile resolve em menos de 10 segundos, reCAPTCHA v2 em menos de 60 segundos — ambos com alta taxa de sucesso.
Próximas etapas
Comece a resolver CAPTCHAs em PWAs —obtenha sua chave API CaptchaAIe leve a detecção dinâmica descrita acima para dentro da sua própria automação.
Guias relacionados:
- Carregamento dinâmico de CAPTCHA: detectando desafios de carregamento lento
- Extração de chave de site Cloudflare Turnstile
- Extraindo parâmetros reCAPTCHA da origem da página