Автоматизация профиля через 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.
Что реализовано
| Домен | Методы |
|---|---|
| Browser | getVersion |
| Target | getTargets, getTargetInfo, getBrowserContexts, setDiscoverTargets, setAutoAttach, attachToTarget, attachToBrowserTarget, detachFromTarget, createTarget, closeTarget |
| Page | navigate, reload, stopLoading, bringToFront, getFrameTree, getNavigationHistory, getLayoutMetrics, captureScreenshot, printToPDF, createIsolatedWorld, addScriptToEvaluateOnNewDocument |
| Runtime | enable, evaluate, callFunctionOn, getProperties, releaseObject, addBinding, runIfWaitingForDebugger |
| DOM | enable, getDocument, describeNode, resolveNode, focus, getBoxModel, getContentQuads, scrollIntoViewIfNeeded |
| Input | dispatchMouseEvent, dispatchKeyEvent, insertText |
| Network | enable, disable, getCookies, getAllCookies, setCookie, setCookies, deleteCookies, clearBrowserCookies |
| Storage | getCookies |
События:
Target.targetCreatedTarget.targetDestroyedTarget.attachedToTargetTarget.detachedFromTargetPage.frameNavigatedPage.frameStartedLoadingPage.frameStoppedLoadingPage.lifecycleEventPage.loadEventFiredRuntime.executionContextCreatedRuntime.executionContextsClearedRuntime.consoleAPICalledRuntime.bindingCalledNetwork.requestWillBeSentNetwork.responseReceivedNetwork.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 корректно используется для ввода символов в верхнем регистре.