Tutorials

Tratamento de CAPTCHA em aplicativos da Web progressivos (PWAs)

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 null mesmo 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

  1. Descarte tokens em cache quando a rota mudar, o estado de autenticação mudar ou o Service Worker atualizar o app shell.
  2. Guarde só o estado necessário para reconectar o token ao formulário ativo.
  3. 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:

Artigos relacionados

Os comentários estão desativados para este artigo.