Playwright Android 自动化完全指南:AndroidDevice API 详解与 ADB 驱动实现原理
【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright
Playwright 自 v1.9 起提供了实验性的 Android 自动化支持,其核心抽象是AndroidDevice类:它代表一台通过 ADB 连接的真实设备或模拟器(AVD),既能对原生 UI 控件做点击、滑动、填写等交互,也能接管设备上的 Chrome 浏览器与 WebView,让同一套Page/BrowserContextAPI 直接运行在移动端。读完本篇,你将掌握 AndroidDevice 的全部方法签名与参数默认值、从 ADB 连接到浏览器接管的完整实战流程,以及从源码层面看懂 Playwright 如何在设备上安装驱动 APK、通过本地抽象 socket 与 WebView 建立 DevTools 通道的底层机制。
一、AndroidDevice 是什么:概念、前提与获取方式
根据官方 API 文档 class-androiddevice.md,AndroidDevice表示一台已连接的设备(真机或模拟),可通过method: Android.devices获取。它继承自事件源,核心事件有两个:
| 事件 | 载荷 | 触发时机 |
|---|---|---|
close | AndroidDevice | 设备连接关闭时(v1.28 起) |
webView | AndroidWebView | 检测到新的 WebView 实例时 |
运行 Android 自动化前需要满足以下前提(引自 class-android.md):
- 一台 Android 真机或 AVD 模拟器;
- 正在运行并与设备完成认证的ADB daemon——通常执行一次
adb devices即可; - 设备上安装了 Chrome 87 或更高版本;
- 在 Chrome 的
chrome://flags中开启 "Enable command line on non-rooted devices"(这是 Playwright 能给 Chrome 传--remote-debugging-socket-name等启动参数的前提)。
设备发现由Android.devices()完成,支持指定远程 ADB server 的host(默认127.0.0.1)与port(默认5037),以及omitDriverInstall(跳过每次连接时自动安装驱动 APK,见第五节)。多设备场景下还可以用Android.connect(endpoint)(v1.28 起)连接到Android.launchServer()启动的服务器实例;launchServer的 WebSocket 默认只监听localhost,且wsPath默认是一个不可猜测的随机串——文档明确警告:任何知道wsPath的进程都可能接管 OS 用户权限,因此显式指定wsPath时必须使用不可猜测的 token。
设备连接建立后的第一步通常是最基础的三个只读方法:
const { _android: android } = require('playwright'); (async () => { // 获取所有已连接的 Android 设备 const [device] = await android.devices(); console.log(`Model: ${device.model()}`); // 设备型号 console.log(`Serial: ${device.serial()}`); // 设备序列号 await device.screenshot({ path: 'device.png' }); // 整机截图 await device.close(); })();model()返回设备型号。从源码 android.ts 看,它在设备初始化时执行shell:getprop ro.product.model获取,因此这是真实的系统属性值。serial()返回设备序列号,即 ADB 层识别设备的唯一标识。screenshot()返回截图Buffer,可选path参数落盘(相对路径基于当前工作目录)。服务端实现就是执行shell:screencap -p(见 android.ts),所以截图是 PNG 格式且覆盖整个屏幕。文档同时提醒:设备必须处于唤醒状态才能出图,建议开启开发者模式的 "Stay awake"。
二、AndroidDevice 完整 API 参考
以下按功能域整理 class-androiddevice.md 中的全部方法。所有标注timeout的方法共享同一套超时语义:默认 30 秒,可用device.setDefaultTimeout(ms)修改(该设置在设备对象级别,会覆盖Android.setDefaultTimeout的全局默认值),传0关闭超时。
2.1 控件交互方法(都接收 AndroidSelector)
AndroidSelector是匹配原生控件的选择器对象,字段包括:res(资源 id)、text、desc(content description)、pkg、clazz、checkable/checked/clickable/enabled/focusable/focused/longClickable/scrollable/selected等布尔状态、depth,以及结构查询hasChild: { selector }与hasDescendant: { selector, maxDepth }。字符串形式的字段值在客户端会被编译为正则(详见第六节)。
| 方法 | 签名要点 | 说明 |
|---|---|---|
tap(selector, opts?) | opts: { duration?, timeout? } | 点击控件。duration(毫秒)为可选按压时长 |
longTap(selector, opts?) | opts: { timeout? } | 长按控件 |
fill(selector, text, opts?) | opts: { timeout? } | 清空并填入文本,目标须是输入框 |
press(selector, key, opts?) | key: AndroidKey | 在控件上下文中按键。客户端实现是tap(selector)后调用input.press(key)(见 android.ts) |
swipe(selector, direction, percent, opts?) | direction: "down"|"up"|"left"|"right" | 按指定方向滑动,percent为相对控件尺寸的距离百分比 |
scroll(selector, direction, percent, opts?) | 同上 | 滚动控件(作用于可滚动元素) |
fling(selector, direction, opts?) | opts: { speed?, timeout? } | 快速甩动控件,speed单位是像素/秒 |
drag(selector, dest, opts?) | dest: { x, y } | 将控件拖拽到目标坐标点 |
pinchOpen(selector, percent, opts?) | opts: { speed?, timeout? } | 按"放大"方向捏合,percent为相对控件尺寸的比例 |
pinchClose(selector, percent, opts?) | 同上 | 按"缩小"方向捏合 |
除drag(目标是绝对坐标{x, y})外,其余方法都以AndroidSelector定位控件;speed(像素/秒)可选参数决定手势速度,缺省时由驱动端使用默认速度。
2.2 等待与查询
| 方法 | 签名要点 | 说明 |
|---|---|---|
wait(selector, opts?) | opts: { state?: 'gone', timeout? } | 等待控件出现;state: 'gone'时等待控件消失 |
info(selector) | 返回AndroidElementInfo | 返回控件的文本、描述、资源 id、包名、类名等属性,是调试选择器的重要手段 |
waitForEvent(event, optionsOrPredicate?) | 事件名如'webview' | 等待事件并传入谓词,谓词返回 truthy 时 resolve;默认超时 30000ms |
webViews() | 返回AndroidWebView[] | 当前已打开的 WebView 列表 |
webView(selector, opts?) | selector: { pkg?, socketName? } | 等待匹配pkg或socketName的 WebView 打开并返回AndroidWebView。客户端实现(android.ts)是:先在本地已缓存的 WebView 集合中查找,找不到则挂起等待webview事件——事件由服务端每 500ms 轮询一次 Unix socket 列表产生(见第五节) |
2.3 设备级操作
| 方法 | 签名要点 | 说明 |
|---|---|---|
shell(command) | 返回Buffer | 在设备上执行 shell 命令并返回输出。所有命令在服务端加shell:前缀经 ADB 执行 |
open(command) | 返回AndroidSocket | 启动 shell 进程并返回可读写 socket(write/close,以及data/close事件),适合需要双向流的场景,如open('localabstract:playwright_android_driver_socket') |
installApk(file, opts?) | file: string \| Buffer;opts: { args? } | 安装 APK,file可以是本地路径或文件内容。args是传给cmd package install的参数,默认-r -t -S(覆盖安装、允许测试包、静默)。服务端实现通过 ADB socket 把 APK 字节流直接写入cmd package install <args> <length>通道(见 android.ts),无需先推文件 |
push(file, path, opts?) | opts: { mode? } | 把文件拷贝到设备。mode可选,默认644(rw-r--r--)。从源码看它使用的是 ADB sync 协议:打开sync:socket 后按SEND/DATA(65535 字节分块)/DONE三段发送,并等待OKAY应答(见 android.ts) |
screenshot(opts?) | opts: { path? },返回Buffer | 整机截图,见第一节 |
launchBrowser(opts?) | 返回BrowserContext | 在设备上启动 Chrome(或pkg指定的其他浏览器)并返回其持久化上下文。除pkg外,还接受标准 BrowserContext 参数(v1.8 起的共享 context 参数列表),以及proxy、args(v1.29 起) |
close() | — | 断开设备连接;触发close事件时也会清理所有已建立的浏览器连接 |
input | 属性,类型AndroidInput | 低级输入通道:type(text)、press(key)、tap(point)、swipe(from, segments, steps)、drag(from, to, steps),直接以坐标/分段方式注入输入,不依赖控件选择器 |
setDefaultTimeout(timeout) | 毫秒 | 修改该设备下所有接受timeout的方法的默认超时 |
2.4 launchBrowser 的底层流程
launchBrowser()是整个 Android API 中最复杂的调用。从服务端源码 android.ts 可以看到完整链路:
am force-stop <pkg>先杀掉目标浏览器(默认com.android.chrome);- 生成一个唯一的 socket 名
playwright_<guid>_devtools_remote(测试模式下为固定名webview_devtools_remote_playwright_test); - 组装 Chrome 启动参数:
--disable-fre、--no-default-browser-check、--remote-debugging-socket-name=<socketName>、Android 专用 Chromium 开关,以及用户传入的proxy(会翻译成--proxy-server/--proxy-bypass-list)与args; - 命令行的特殊字符容易在 shell 中出问题,所以源码把它 base64 编码后写入设备上的
/data/local/tmp/chrome-command-line,再用am start -a android.intent.action.VIEW -d about:blank <pkg>拉起浏览器(Chrome 的 command-line 文件机制会读取该文件); - 通过
open('localabstract:<socketName>')打开这个 DevTools 抽象 socket,手工完成一次 HTTP Upgrade 握手,把 socket 包装成AndroidBrowser(内置 WebSocket 收发器),随后以persistent持久化上下文模式连接CRBrowser,返回默认BrowserContext; - 成功后删除临时命令行文件;失败则关闭已建立的上下文并抛错。
这也解释了为什么文档要求开启 "Enable command line on non-rooted devices":Chrome 只有在允许命令行覆盖时才会读取注入的调试 socket 参数。
三、完整实战:从连接设备到自动化 Chrome 与 WebView
下面是官方文档(class-android.md)给出的端到端示例,覆盖了model/serial/screenshot/shell/webView/fill/press/launchBrowser/close等主力 API:
const { _android: android } = require('playwright'); (async () => { // 连接设备。 const [device] = await android.devices(); console.log(`Model: ${device.model()}`); console.log(`Serial: ${device.serial()}`); // 对整个设备截图。 await device.screenshot({ path: 'device.png' }); { // --------------------- WebView 自动化 ----------------------- // 启动一个带 WebView 的应用。 await device.shell('am force-stop org.chromium.webview_shell'); await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); // 获取 WebView。 const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); // 填充地址输入框。 await device.fill({ res: 'org.chromium.webview_shell:id/url_field', }, 'github.com/microsoft/playwright'); await device.press({ res: 'org.chromium.webview_shell:id/url_field', }, 'Enter'); // 像普通 Page 一样操作 WebView 里的页面。 const page = await webview.page(); await page.waitForURL(/.*microsoft\/playwright.*/); console.log(await page.title()); } { // --------------------- Chrome 浏览器自动化 ----------------------- // 启动 Chrome。 await device.shell('am force-stop com.android.chrome'); const context = await device.launchBrowser(); // 像普通 BrowserContext 一样使用。 const page = await context.newPage(); await page.goto('https://webkit.org/'); console.log(await page.evaluate(() => window.location.href)); await page.screenshot({ path: 'page.png' }); await context.close(); } // 关闭设备连接。 await device.close(); })();注:示例中地址栏选择器
org.chromium.webview_shell:id/url_field演示了 Android 选择器的res字段用法;fill之后用press(..., 'Enter')提交。AndroidWebView.page()会把 WebView 适配为标准的Page对象(客户端实现见 android.ts,通过connectToWebView在 socket 上建立 DevTools 连接并取到上下文中的第一个页面),此后page.goto/page.title等 API 与桌面端完全一致。原文档示例中使用的page.waitForNavigation是旧版 API,新版可等价写作page.waitForURL。
AndroidWebView对象本身也提供三个属性方法:pid()(宿主进程 PID)、pkg()(宿主包名)、以及内部使用的 socket 名;配合device.webViews()可以枚举当前所有 WebView,配合device.on('webView', ...)事件可以在应用内 WebView 打开的瞬间做出响应。
3.1 跨进程:launchServer / connect 模式
当 ADB 与测试进程不在同一台机器(例如 CI 节点连测试机上的 ADB server),v1.28 起的 server/client 模式更合适。服务端:
const { _android } = require('playwright'); (async () => { const browserServer = await _android.launchServer({ // 多台设备连接、想固定使用其中一台时: // deviceSerialNumber: '<deviceSerialNumber>', }); const wsEndpoint = browserServer.wsEndpoint(); console.log(wsEndpoint); })();客户端:
const { _android } = require('playwright'); (async () => { const device = await _android.connect('<wsEndpoint>'); console.log(device.model()); console.log(device.serial()); await device.shell('am force-stop com.android.chrome'); const context = await device.launchBrowser(); const page = await context.newPage(); await page.goto('https://webkit.org/'); console.log(await page.evaluate(() => window.location.href)); await page.screenshot({ path: 'page-chrome-1.png' }); await context.close(); })();关键选项:launchServer接受adbHost/adbPort(指定 ADB server,默认127.0.0.1:5037)、deviceSerialNumber(多设备时必须显式指定,否则抛错)、host(v1.45 起,默认localhost,显式传0.0.0.0会把设备 RPC 暴露到网络)、port(默认0,随机端口)、wsPath(默认不可猜测的随机串)和omitDriverInstall。connect(endpoint, options?)侧则支持headers、slowMo(毫秒级减速,便于观察)与timeout(默认 30000ms,0表示禁用)。从客户端源码 android.ts 看,连接时会自动带上x-playwright-browser: android头,并在握手后校验 endpoint 是否由launchServer产生,不是则报 "Malformed endpoint"。
四、AndroidSelector 是怎么匹配的:客户端正则编译
AndroidDevice的所有控件方法都接收AndroidSelector,而它的字符串字段在发往服务端之前,会在客户端被统一编译成正则表达式(见 toSelectorChannel):
- 传入
RegExp时,直接取其source,即你写的正则原样生效; - 传入字符串时,所有正则特殊字符(
|\\{}()[\]^$+*?.)被转义,并整体包上^...$锚点——字符串值按"精确全匹配"处理,例如res: 'com.app:id/login_btn'只匹配该完整资源 id,而不是子串; hasChild/hasDescendant会递归做同样的编译,hasDescendant额外携带maxDepth限制向下搜索深度;- 布尔字段(
clickable、enabled、selected等)与depth则原样透传。
这个细节直接影响选择器编写:想"以 login 开头"要写res: /^com\.app:id\/login/,而不是res: 'com.app:id/login'。协议层的完整方法清单定义在 android.yml,可用于核对每个方法对应的 RPC 名称与参数。
五、源码深读:Playwright 如何"驱动"一台 Android 设备
理解了 API 表面之后,真正有趣的是服务端(packages/playwright-core/src/server/android/android.ts)如何在 ADB 之上构建出一套可靠的能力。
5.1 设备发现与 ADB 抽象
Android.devices()调用后端Backend.devices()(ADB 实现的adb devices语义),过滤出status === 'device'的条目,并按序列号增量维护一个serial -> AndroidDevice映射:新序列号创建设备对象,消失的序列号则从映射中移除(android.ts)。设备创建时会执行shell:getprop ro.product.model读型号。AndroidDevice的shell/screenshot/open全部构建在两个后端原语上:
runCommand(command):执行shell:xxx形式的 ADB 命令并拿回输出;open(command):打开一条长连接 socket,例如shell:cmd package install ...、localabstract:<name>、sync:。
AndroidDevice.shell()每次执行完命令后还会立即刷新一次 WebView 列表(_refreshWebViews),保证am start之类命令之后能尽快发现新 WebView。
5.2 驱动 APK:控件交互的真正执行者
tap/fill/swipe等 UI 交互不直接走 ADB shell,而是走设备上安装的驱动 APK。首次需要交互时,_installDriver()(android.ts)会:
am force-stop com.microsoft.playwright.androiddriver停掉旧驱动;- 若未设置
omitDriverInstall,先cmd package uninstall两个驱动包(androiddriver与androiddriver.test),再从 Playwright 安装目录读取android-driver.apk与android-driver-target.apk,用与installApk相同的 socket 通道装上去(文件缺失时提示执行playwright install android); am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner启动 instrument 进程;- 轮询
localabstract:playwright_android_driver_socket直到可连接,包装成 JSON-RPC 式通道:每条消息是{ id, method, params },服务端按id匹配挂起的 Promise,error字段则触发 reject(见 _send)。
从源码结构看,UI 动作(tap、swipe、pinch 等)本质上是发给驱动 APK 的 RPC 调用,由 APK 内的 instrumentation 框架完成手势合成——这也意味着驱动 APK 版本与 Playwright 版本需要配套,默认每次连接都会重装以保证一致;CI 中确认驱动已就位时可用devices({ omitDriverInstall: true })跳过这段安装开销。
5.3 WebView 是怎么被"看见"的
device.webView()/webViews()的数据来自_refreshWebViews()(android.ts):
- 每 500ms 执行
shell:cat /proc/net/unix | grep webview_devtools_remote,扫描系统 Unix socket 表; - 用正则提取
webview_devtools_remote_<pid>[<name>]形式的 socket 名,并从 socket 名中解析出宿主进程 PID,再用ps -A | grep <pid>反查出包名; - 与本地缓存比对:新出现的 socket 触发
webViewAdded事件(客户端即device.on('webView', ...)的来源),消失的触发webViewRemoved。
所以"检测到新 WebView"完全是对/proc/net/unix的轮询结果,webView({ pkg, socketName })的匹配键也由此而来;waitForEvent的默认 30 秒超时覆盖了轮询发现的延迟。
5.4 连接链路与关闭语义
客户端AndroidDevice对象与设备之间是标准的 Playwright 通道协议(dispatcher 见 androidDispatcher.ts),而close的语义值得注意:device.close()在服务端会停止 WebView 轮询、关闭所有浏览器连接(包括由launchBrowser/ WebView 建立的 DevTools 通道)、reject 所有未决的驱动 RPC、关闭驱动 socket 并断开 ADB 会话,最后向客户端广播close事件(android.ts)。在connect()模式下客户端还会把close与 WebSocket 连接绑定(_shouldCloseConnectionOnClose),设备掉线即断开 RPC 连接,避免悬挂状态。
六、已知限制、排错与延伸阅读
结合文档声明与源码行为,实践时的主要限制是:
- 必须有 ADB:原始 USB 通信尚不支持,一切能力都构建在 ADB daemon 之上(
adb devices是最基本的健康检查); - 截图要求设备唤醒:锁屏状态下
screenshot()可能失败或返回黑屏,建议开启 "Stay awake"; - Chrome 版本门槛:
launchBrowser依赖 Chrome 的命令行文件机制与自定义 remote debugging socket,需要 Chrome 87+ 且开启对应 flag; - 驱动安装开销:默认每次连接都卸载重装两个驱动 APK,CI 环境可评估
omitDriverInstall; - 实验性定位:官方文档明确标注 Android 支持为 experimental,并非所有测试都在真机上跑过,遇到个别方法异常属于已知状态。
排错手段上,device.info(selector)可以先确认选择器命中了什么控件(返回AndroidElementInfo);device.shell('logcat -d | tail -n 100')一类命令可用于查看设备日志;DEBUG=pw:android可打开驱动安装与 socket 连接的调试日志(源码中的debug('pw:android')埋点);截图(设备级screenshot()与页面级page.screenshot())则是视觉回归的直接依据。
仓库中与 Android 自动化相关的入口,便于继续深入:
| 资源 | 路径 |
|---|---|
| AndroidDevice API 参考 | docs/src/mobile-api/class-androiddevice.md |
| Android 总览与示例 | docs/src/mobile-api/class-android.md |
| AndroidInput / AndroidSocket / AndroidWebView 参考 | class-androidinput.md、class-androidsocket.md、class-androidwebview.md |
| 客户端 API 实现 | packages/playwright-core/src/client/android.ts |
| 服务端 ADB/驱动实现 | packages/playwright-core/src/server/android/android.ts |
| 驱动 APK 工程 | packages/playwright-core/src/server/android/driver |
| 协议规范 | packages/protocol/spec/android.yml |
| 集成测试 | tests/android/android.spec.ts、tests/android/device.spec.ts、tests/android/browser.spec.ts、tests/android/launch-server.spec.ts |
至此,AndroidDevice的全部方法、参数默认值与底层实现路径都已覆盖:从 ADB 连接、驱动 APK 安装、Unix socket 上的 WebView 发现,到 Chrome 命令行注入与 DevTools WebSocket 升级,每一层都可以对照仓库源码验证。
【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考