Перейти к основному содержимому

Автоматизация профиля через CDP

Octo Mobile поддерживает управление браузерными профилями из Playwright или Puppeteer через Chrome DevTools Protocol (CDP).

В этой документации описано, как подключиться к профилю, какие методы и события CDP поддерживаются, а также ограничения, которые нужно учитывать при переносе сценариев автоматизации с десктопного браузера на Octo Mobile.

Все описанные сценарии протестированы в приложении, работающем на реальном устройстве. Если ограничение связано с движком и его нельзя обойти средствами Octo Mobile, это указано отдельно.

Подключение

Приложение поднимает HTTP-сервер на порте 9222 (следующий свободный, если занят). Адрес показан в карточке аккаунта, тап по строке копирует его.

МетодEndpointОписание
Get/profilesСписок профилей и их состояние
Get/profiles/{profileId}Получить один профиль; у запущенного профиля есть webSocketDebuggerUrl
POST/profiles/{profileId}/startЗапустить профиль. Ответить после стабилизации; 202, если запуск еще выполняется
POST/profiles/{profileId}/stopОстановить профиль и синхронизировать данные
Get/profiles/{profileId}/tabsПолучить список вкладок: id, title, url, active
POST/profiles/{profileId}/tabs/{tabId}/activateВыбрать вкладку и вывести профиль на экран

У запущенного профиля свой WebSocket-эндпойнт — он приходит в webSocketDebuggerUrl. Порт свой на каждый профиль и намеренно непредсказуемый, фиксирован только HTTP-порт. Один эндпойнт — один профиль, один профиль — один browser context.

Начало скрипта целиком:

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();

// вывести эту вкладку на экран устройства, чтобы смотреть за прогоном вживую
await page.bringToFront();

HTTP-маршрут activate предназначен для другого сценария: когда CDP-соединение отсутствует и вкладку необходимо активировать внешним запросом, например из shell-скрипта.

Идентификаторы вкладок и профилей единообразны во всех интерфейсах:

  • Вкладка — UUID targetId в формате bare-hex. Значение targetId, полученное через CDP, можно напрямую передать в HTTP-маршрут без преобразования.
  • Профиль — значение browserContextId.

Что реализовано

ДоменМетоды
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

События:

  • 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

Все методы, не перечисленные в таблице выше, возвращают пустой результат. Это предусмотренное поведение.

Что не работает и как адаптировать скрипт

page.evaluate на сайте со строгим CSP

На сайтах со строгой Content Security Policy (CSP) метод page.evaluate может возвращать EvalError. Обойти ограничение на уровне страницы нельзя.

await page.evaluate(() => document.title);   // EvalError на таком сайте

И Playwright, и Puppeteer передают функцию на страницу в виде строки, которую затем нужно преобразовать в исполняемый код. Если сайт использует require-trusted-types-for 'script' или script-src без unsafe-eval, такое выполнение блокируется.

Chrome DevTools Protocol выполняет подобные операции с привилегиями инспектора, поэтому в обычном Chrome evaluate() продолжает работать. На iOS такой привилегии нет.

Ограничение распространяется на все API, которые передают пользовательскую функцию для выполнения в контексте страницы, включая:

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

Если задачу можно решить с помощью локаторов, используйте их вместо передачи функции на страницу:

// вместо 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());

На обычной странице без такой политики evaluate и $$eval работают штатно. На страницах со строгой политикой используйте локаторы: их достаточно в том числе для автоматизации реальных сценариев регистрации в Google и Microsoft.

Документы data: и about:blank могут наследовать CSP документа, из которого они были открыты. Если прогон начинается сразу после сессии на сайте с Trusted Types, политика переедет на вашу тестовую страницу. Открывайте для теста новую вкладку — она не наследует CSP предыдущего документа.

Фреймы

page.frames() отдает только главный фрейм, frameLocator() ждет до тайм-аута. Содержимое <iframe> недостижимо. Обходного пути нет: сценарий, завязанный на фреймы, здесь пока не автоматизируется.

Перехват запросов

page.route() устанавливается без ошибки и затем не срабатывает ни разу — запросы уходят мимо. Не используйте page.route() для сценариев, которые требуют блокировки, модификации или подмены HTTP-запросов. Вместо этого проверяйте результат, который фактически отобразился на странице.

browser.newContext()

Отказывает с явной ошибкой. Browser context здесь и есть профиль Octo, а профили создаются через API Octo, а не через протокол. Нужен второй профиль и второе подключение.

Скриншоты и PDF

page.screenshot(), page.screenshot({ fullPage: true }), clip и page.pdf() работают. Полная страница снимается целиком, включая элементы canvas и изображения. Отличия от десктопа:

  • Снимки возвращаются в пикселях устройства, разрешение втрое больше CSS-пикселя. Опция scale: 'css' у Playwright на это не влияет.
  • Для ограничения потребления памяти разрешение скриншота может автоматически снижаться. При этом запрошенная область страницы захватывается полностью.
  • Печатные параметры page.pdf()landscape, paperWidth, paperHeight, scale, pageRanges, поля и колонтитулы — игнорируются. PDF формируется по размеру самой страницы, одним листом.
  • Для вкладки, созданной через newPage() и не выведенной на экран, фактический viewport отсутствует: снимок без fullPage вернет всю страницу целиком. Нужен именно экранный кадр — сначала вызовите page.bringToFront().

waitForLoadState('networkidle')

Поддерживается, но работает иначе, чем в других браузерах. Состояние простоя сети определяется только по запросам, которые завершились. Запросы, которые не завершаются никогда (SSE, long-poll, зависший XHR), здесь не учитываются. Простой может быть объявлен, пока соединение еще открыто. Для обычной загрузки страницы сигнал корректен.

Лучше ждать конкретного условия:

await page.locator('#results').waitFor();  // вместо
await page.waitForLoadState('networkidle');

События ввода и их достоверность

Ввод текста выполняется через системный механизм ввода iOS. Поэтому события beforeinput и input имеют isTrusted: true. Это соответствует поведению реального пользовательского ввода.

События клавиатуры keydown, keypress и keyup являются синтетическими. Аппаратные нажатия клавиш поступают в браузер через системный канал, недоступный приложению. Поэтому сайт, проверяющий event.isTrusted для событий клавиатуры, может определить такой ввод как синтетический.

События мыши также являются синтетическими, но их поведение соответствует обычному взаимодействию в тех аспектах, которые влияют на состояние страницы:

  • нажатие на элемент под курсором переводит на него фокус;
  • после click() можно использовать keyboard.press(), как после обычного клика;
  • если обработчик mousedown вызывает preventDefault(), фокус не переносится на элемент — так же, как в обычном браузере.

Поддерживаются следующие методы ввода:

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

Модификаторы клавиатуры учитываются. Например, Shift корректно используется для ввода символов в верхнем регистре.