Ir para o conteúdo principal

Automatização de perfis via CDP

O Octo Mobile oferece suporte para o gerenciamento de perfis de navegador usando Playwright ou Puppeteer via protocolo Chrome DevTools (CDP).

Saiba aqui como se conectar a um perfil, quais métodos e eventos do CDP têm suporte e quais limitações você deve considerar ao migrar cenários de automação de um navegador desktop para o Octo Mobile.

Todos os cenários descritos foram testados no aplicativo em execução em um dispositivo de verdade. Se uma limitação estiver relacionada ao navegador e não puder ser contornada usando o Octo Mobile, isso será indicado separadamente.

Conectando-se com um perfil

O aplicativo inicia um servidor HTTP na porta 9222 (ou na próxima porta disponível, caso essa esteja em uso). O endereço é exibido nos detalhes da conta; toque na linha para copiar o endereço.

MétodoEndpointDescrição
Get/profilesLista de perfis e seus status
Get/profiles/{profileId}Obtenha um perfil; um perfil em execução possui webSocketDebuggerUrl
POST/profiles/{profileId}/startExecute o perfil. Responda após a estabilização; retorne 202 se a inicialização ainda estiver em andamento
POST/profiles/{profileId}/stopInterrompa o perfil e sincronize os dados
Get/profiles/{profileId}/tabsObtenha uma lista de abas: id, title, url, active
POST/profiles/{profileId}/tabs/{tabId}/activateSelecione uma aba e exiba o perfil na tela

Cada perfil em execução possui o próprio WebSocket endpoint, fornecido em webSocketDebuggerUrl. Cada perfil usa a própria porta, que é intencionalmente imprevisível; apenas a porta HTTP é fixa. Um endpoint corresponde a um perfil, e um perfil corresponde a um contexto de navegador.

O início do script na íntegra:

import { chromium } from 'playwright-core';

const server = 'http://192.168.1.10:9222';
const profileId = '2724a5ab459a4417b4bf4c627a1c4650';

await fetch(`${server}/profiles/${profileId}/start`, { method: 'POST' });
const status = await (await fetch(`${server}/profiles/${profileId}`)).json();

const browser = await chromium.connectOverCDP(status.webSocketDebuggerUrl);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();

// Exiba esta aba na tela do dispositivo para acompanhar a execução em tempo real
await page.bringToFront();

A rota HTTP activate destina-se a um cenário diferente: quando não há conexão CDP e a aba precisa ser ativada por meio de uma solicitação externa, por exemplo, a partir de um script de shell.

Os identificadores de aba e de perfil são consistentes em todas as interfaces:

  • Aba — o UUID targetId no formato bare-hex. O targetId obtido via CDP pode ser passado diretamente para a rota HTTP sem conversão.
  • Perfil — o valor browserContextId.

O que foi implementado

DomínioMétodos
BrowsergetVersion
TargetgetTargets, getTargetInfo, getBrowserContexts, setDiscoverTargets, setAutoAttach, attachToTarget, attachToBrowserTarget, detachFromTarget, createTarget, closeTarget
Pagenavigate, reload, stopLoading, bringToFront, getFrameTree, getNavigationHistory, getLayoutMetrics, captureScreenshot, printToPDF, createIsolatedWorld, addScriptToEvaluateOnNewDocument
Runtimeenable, evaluate, callFunctionOn, getProperties, releaseObject, addBinding, runIfWaitingForDebugger
DOMenable, getDocument, describeNode, resolveNode, focus, getBoxModel, getContentQuads, scrollIntoViewIfNeeded
InputdispatchMouseEvent, dispatchKeyEvent, insertText
Networkenable, disable, getCookies, getAllCookies, setCookie, setCookies, deleteCookies, clearBrowserCookies
StoragegetCookies

Eventos:

  • Target.targetCreated
  • Target.targetDestroyed
  • Target.attachedToTarget
  • Target.detachedFromTarget
  • Page.frameNavigated
  • Page.frameStartedLoading
  • Page.frameStoppedLoading
  • Page.lifecycleEvent
  • Page.loadEventFired
  • Runtime.executionContextCreated
  • Runtime.executionContextsCleared
  • Runtime.consoleAPICalled
  • Runtime.bindingCalled
  • Network.requestWillBeSent
  • Network.responseReceived
  • Network.loadingFinished

Todos os métodos não listados na tabela acima retornam um resultado vazio. Esse comportamento é intencional.

O que não funciona e como adaptar o script

page.evaluate em um site com CSP restrita

Em sites com uma Política de Segurança de Conteúdo (CSP) restrita, o método page.evaluate pode retornar um EvalError. Não é possível contornar essa restrição no nível da página.

await page.evaluate(() => document.title);   // EvalError neste site

Tanto o Playwright quanto o Puppeteer passam a função para a página como uma string, que então precisa ser convertida em código executável. Caso um site utilize require-trusted-types-for 'script' ou script-src sem unsafe-eval, essa execução será suspensa.

O protocolo Chrome DevTools realiza operações semelhantes com privilégios de inspetor; portanto, evaluate() continua funcionando no Chrome normal. No iOS, esse privilégio não está disponível.

A limitação aplica-se a todas as APIs que passam uma função definida pelo usuário para execução no contexto da página, incluindo:

  • page.evaluate()
  • page.$$eval()
  • page.$eval()
  • page.evaluateHandle()
  • locator.allTextContents()

Se a sua tarefa puder ser resolvida usando seletores, utilize-os em vez de passar uma função para a página:

// em vez de page.$$eval('.row', els => els.map(e => e.textContent))
; const rows = page.locator('.row');
; const texts = [];
for (let i = 0; i < await rows.count(); i++) texts.push(await rows.nth(i).textContent());

Em uma página normal sem essa política, evaluate e $$eval funcionam conforme o esperado. Em páginas com uma política restrita, utilize seletores: eles são suficientes, entre outras coisas, para automatizar cenários de cadastro real com Google e Microsoft.

Documentos data: e about:blank podem herdar a CSP do documento a partir do qual foram abertos. Caso a execução seja inicializada imediatamente após uma sessão em um site que utiliza Trusted Types, a política será transferida para a sua página de teste. Abra uma nova aba para testes, pois ela não herda a CSP do documento anterior.

Frames

page.frames() retorna apenas o frame principal, enquanto frameLocator() aguarda até o fim do tempo limite. O conteúdo de \<iframe> é inacessível. Sem solução alternativa: cenários que dependem de frames ainda não podem ser automatizados aqui.

Interceptação de solicitações

page.route() é configurado sem erros, mas nunca é disparado; as solicitações são ignoradas. Não utilize page.route() para cenários que exijam suspensão, modificação ou spoofing de solicitações HTTP. Em vez disso, verifique o resultado que é efetivamente renderizado na página.

browser.newContext()

A operação falha com um erro explícito. O contexto do navegador aqui é o perfil do Octo, e os perfis são criados por meio da API do Octo, em vez de pelo protocolo. Um segundo perfil e uma segunda conexão são necessários.

Capturas de tela e PDFs

page.screenshot(), page.screenshot({ fullPage: true }), clip e page.pdf() funcionam. Capturas de tela de página inteira registram a página completa, incluindo elementos e imagens de canvas. Diferenças da versão do navegador para desktop:

  • As capturas de tela são retornadas na resolução de pixels do dispositivo, com o triplo da resolução de um pixel CSS. A opção scale: 'css' no Playwright não afeta isso.
  • Para limitar o uso de memória, a resolução da captura de tela pode ser reduzida automaticamente. A área solicitada da página ainda é capturada por completo.
  • Os parâmetros de impressão de page.pdf() (landscape, paperWidth, paperHeight, scale, pageRanges, margens e cabeçalhos/rodapés) são ignorados. O PDF é gerado com base na dimensão da própria página, como uma única folha.
  • Para uma aba criada usando newPage() que não é exibida na tela, não existe viewport: uma captura de tela feita sem fullPage retornará a página inteira. Se você precisar de uma captura de tela real, chame page.bringToFront() primeiro.

waitForLoadState('networkidle')

Tem suporte, mas funciona de maneira diferente de outros navegadores. O estado de inatividade da rede é determinado exclusivamente com base em solicitações concluídas. Solicitações que nunca são concluídas (SSE, long-poll, XHR pendente) não são levadas em consideração aqui. O estado de inatividade pode ser relatado enquanto a conexão ainda está aberta. Para o carregamento de páginas normais, o sinal está correto.

É melhor aguardar uma condição específica:

await page.locator('#results').waitFor();  // em vez de
await page.waitForLoadState('networkidle');

Eventos de entrada e sua confiabilidade

A entrada de texto é realizada usando o mecanismo de entrada do sistema iOS. Como resultado, os eventos beforeinput e input apresentam isTrusted: true. Isso corresponde ao comportamento da entrada real do usuário.

Os eventos de teclado keydown, keypress e keyup são sintéticos. As teclas pressionadas no hardware chegam ao navegador por meio de um canal do sistema inacessível ao aplicativo. Portanto, sites que verificam event.isTrusted para eventos de teclado podem identificar tal entrada como sintética.

Eventos de mouse também são sintéticos, mas seu comportamento corresponde a interações normais nos aspectos que afetam o estado da página:

  • ao clicar em um elemento sob o cursor, o foco é transferido para ele;
  • após click(), você pode usar keyboard.press(), assim como faria após um clique comum;
  • se o manipulador de mousedown chamar preventDefault(), o foco não será transferido para o elemento, assim como ocorreria em um navegador comum.

Os métodos de entrada a seguir são suportados:

  • locator.fill()
  • locator.pressSequentially()
  • keyboard.type()
  • keyboard.press()

Modificadores de teclado são suportados. Por exemplo, a tecla Shift é usada corretamente para digitar caracteres maiúsculos.