你的第一个脚本
针对模拟手机,一步步构建一个完整的登录脚本,逐行解释,提供 Python、Node.js 和 shell 三种写法。
本页为译文。如与英文版不一致,以英文版为准。 English
在本教程中,你会写一个脚本:打开一个应用,如果有广告就关掉它,登录,检查登录是否成功,从屏幕上读取两个值,并保存一张截图。在这个过程中,你会接触到每个脚本都会用到的概念:查找元素、等待、条件和错误。
你不需要手机。Droidline 自带 droidline-fakephone,这个小程序会假装成一部运行着演示应用的手机。它使用与真实应用完全相同的协议,所以你在这里写的脚本在真机上也能运行。
开始之前,请完成安装的第 1、2 和 5 步:把 droidline 程序放进 PATH;如果你用 Python 或 Node.js,再安装对应语言的库。
1. 启动服务器和模拟手机
你需要三个终端窗口。
终端 1 运行服务器。保持它打开:
droidline serve
终端 2 运行模拟手机。它会在你的网络中找到服务器,并输出一个配对码:
droidline-fakephone
Found MY-PC at 192.168.0.12:8779
Pairing code: 482913
On the PC run: droidline pair 482913
终端 3 是你工作的地方。批准这个代码,并给手机起个名字:
droidline pair 482913 --name fake-01
droidline devices
NAME ID MODEL ANDROID STATE ROUTE MISSING
fake-01 lpez3oxg Droidline Fake 14 online lan
2. 先查看屏幕
为任何应用写脚本之前,先看看 Droidline 是怎样看待它的界面的。打开演示应用并保存它的界面:
droidline launch dev.droidline.demo
droidline dump screen.json
screen.json 描述了屏幕上的每个元素。下面是登录界面中重要的部分,省略了其他字段:
{
"package": "dev.droidline.demo",
"activity": ".LoginActivity",
"tree": {
"children": [
{ "text": "Sign in", "id": "", "class": "android.widget.TextView" },
{ "text": "", "id": "dev.droidline.demo:id/email", "desc": "Email", "class": "android.widget.EditText" },
{ "text": "", "id": "dev.droidline.demo:id/password", "desc": "Password", "class": "android.widget.EditText" },
{ "text": "Keep me signed in", "id": "dev.droidline.demo:id/auto_login", "class": "android.widget.CheckBox", "checked": false },
{ "text": "Log in", "id": "dev.droidline.demo:id/login", "class": "android.widget.Button", "enabled": false },
{ "text": "Close ad", "id": "dev.droidline.demo:id/ad_close", "class": "android.widget.Button" }
]
}
}
有三个字段可以区分元素:
text是元素显示的内容,例如Log in。id是应用开发者给元素起的名字。它不会随手机语言改变,所以只要有,它就是最可靠的选择。:id/之前的部分可以省略:email能匹配dev.droidline.demo:id/email。desc是给屏幕阅读器用的描述,通常是图标唯一的标签。
字段和值合在一起称为选择器:("text", "Log in") 或 ("id", "email")。每个操作元素的命令都接收一个选择器。注意 Log in 是 enabled: false:应用只在输入邮箱之后才启用它。
3. 一步步编写脚本
下面每一步都会加上几行代码。在任意一个示例中选择你的语言,页面其余部分会随之切换。
连接并打开应用
from droidline import connect
d = connect()
d.launch("dev.droidline.demo")
d.wait("text", "Sign in")import { connect } from "droidline";
const d = await connect();
await d.launch("dev.droidline.demo");
await d.wait("text", "Sign in");#!/usr/bin/env bash
set -e # 遇到第一条失败的命令就停止
droidline launch dev.droidline.demo
droidline wait text "Sign in"connect() 找到唯一在线的手机,返回一个向它发送命令的对象,按惯例命名为 d。launch 按包名启动应用,包名是每个 Android 应用独有的名称。wait 会一直等到某个元素出现,默认最多 10 秒,这样下一行就不会在仍在加载的界面上执行。
在 shell 中不需要连接:每条 droidline 命令都会自己找到服务器和手机。
只在有广告时关闭它
if d.exists("text", "Close ad"):
d.touch("text", "Close ad")if (await d.exists("text", "Close ad")) {
await d.touch("text", "Close ad");
}if [ "$(droidline exists text "Close ad")" = "true" ]; then
droidline touch text "Close ad"
fiexists 是一个条件:它立即回答 true 或 false,永远不会失败。所以它适合处理可能出现也可能不出现的东西,例如广告和弹窗。touch 会找到元素,必要时等待它出现,然后点击它。
输入邮箱
d.input("id", "email", "knife")await d.input("id", "email", "knife");droidline input id email knifeinput 接收一个选择器和要输入的文字。它会替换输入框中原有的内容。这里的选择器使用 id,因为邮箱输入框本身没有可以匹配的文字。
如果复选框没有勾选,就勾选它
if not d.checked("id", "auto_login"):
d.touch("id", "auto_login")if (!(await d.checked("id", "auto_login"))) {
await d.touch("id", "auto_login");
}if [ "$(droidline checked id auto_login)" = "false" ]; then
droidline touch id auto_login
fi点击复选框会切换它的状态,所以不加判断地点击,会把已经勾选的复选框取消勾选。checked 先读取当前状态。
登录并检查是否成功
d.touch("text", "Log in")
screen = d.which([("text", "Log in"), ("id", "main_tab")], timeout=15)
if screen != 1:
d.screenshot("login-failed.png")
raise SystemExit("Did not reach the main screen")await d.touch("text", "Log in");
const screen = await d.which([["text", "Log in"], ["id", "main_tab"]], { timeout: 15 });
if (screen !== 1) {
await d.screenshot("login-failed.png");
throw new Error("Did not reach the main screen");
}droidline touch text "Log in"
screen=$(droidline which text="Log in" id=main_tab --timeout 15)
if [ "$screen" != "1" ]; then
droidline screenshot login-failed.png
echo "Did not reach the main screen" >&2
exit 1
fi点击 Log in 之后,可能出现两种情况:主界面打开;或者因为出了问题,登录界面一直停留。which 等待它的候选项中最先出现的那一个,并返回它在列表中的位置:第一个返回 0,第二个返回 1,超时前都没有出现则返回 -1。失败时保存一张截图,之后就很容易看出哪里出了问题。
读取数值并保存截图
print(d.get_text("id", "greeting"))
print("Balance:", d.get_text("id", "balance"))
print("Chats:", d.count("class", "android.widget.CheckBox"))
d.screenshot("after-login.png")console.log(await d.getText("id", "greeting"));
console.log("Balance:", await d.getText("id", "balance"));
console.log("Chats:", await d.count("class", "android.widget.CheckBox"));
await d.screenshot("after-login.png");droidline get_text id greeting
echo "Balance: $(droidline get_text id balance)"
echo "Chats: $(droidline count class android.widget.CheckBox)"
droidline screenshot after-login.pngget_text 返回元素的文字,count 返回匹配的元素有多少个。截图保存在你的电脑上,而不是手机上。
4. 完整脚本
from droidline import connect
d = connect()
# 打开演示应用,等待登录界面出现。
d.launch("dev.droidline.demo")
d.wait("text", "Sign in")
# 关闭广告,但只在它出现时才关闭。
if d.exists("text", "Close ad"):
d.touch("text", "Close ad")
# 输入邮箱;如果 "Keep me signed in" 没有勾选,就勾选它。
d.input("id", "email", "knife")
if not d.checked("id", "auto_login"):
d.touch("id", "auto_login")
# 登录,然后检查出现的是哪个界面。
d.touch("text", "Log in")
screen = d.which([("text", "Log in"), ("id", "main_tab")], timeout=15)
if screen != 1:
d.screenshot("login-failed.png")
raise SystemExit("Did not reach the main screen")
# 读取应用显示的内容,并保存一张图片。
print(d.get_text("id", "greeting"))
print("Balance:", d.get_text("id", "balance"))
print("Chats:", d.count("class", "android.widget.CheckBox"))
d.screenshot("after-login.png")
print("done")import { connect } from "droidline";
const d = await connect();
// 打开演示应用,等待登录界面出现。
await d.launch("dev.droidline.demo");
await d.wait("text", "Sign in");
// 关闭广告,但只在它出现时才关闭。
if (await d.exists("text", "Close ad")) {
await d.touch("text", "Close ad");
}
// 输入邮箱;如果 "Keep me signed in" 没有勾选,就勾选它。
await d.input("id", "email", "knife");
if (!(await d.checked("id", "auto_login"))) {
await d.touch("id", "auto_login");
}
// 登录,然后检查出现的是哪个界面。
await d.touch("text", "Log in");
const screen = await d.which([["text", "Log in"], ["id", "main_tab"]], { timeout: 15 });
if (screen !== 1) {
await d.screenshot("login-failed.png");
throw new Error("Did not reach the main screen");
}
// 读取应用显示的内容,并保存一张图片。
console.log(await d.getText("id", "greeting"));
console.log("Balance:", await d.getText("id", "balance"));
console.log("Chats:", await d.count("class", "android.widget.CheckBox"));
await d.screenshot("after-login.png");
console.log("done");#!/usr/bin/env bash
set -e # 遇到第一条失败的命令就停止
# 打开演示应用,等待登录界面出现。
droidline launch dev.droidline.demo
droidline wait text "Sign in"
# 关闭广告,但只在它出现时才关闭。
if [ "$(droidline exists text "Close ad")" = "true" ]; then
droidline touch text "Close ad"
fi
# 输入邮箱;如果 "Keep me signed in" 没有勾选,就勾选它。
droidline input id email knife
if [ "$(droidline checked id auto_login)" = "false" ]; then
droidline touch id auto_login
fi
# 登录,然后检查出现的是哪个界面。
droidline touch text "Log in"
screen=$(droidline which text="Log in" id=main_tab --timeout 15)
if [ "$screen" != "1" ]; then
droidline screenshot login-failed.png
echo "Did not reach the main screen" >&2
exit 1
fi
# 读取应用显示的内容,并保存一张图片。
droidline get_text id greeting
echo "Balance: $(droidline get_text id balance)"
echo "Chats: $(droidline count class android.widget.CheckBox)"
droidline screenshot after-login.png
echo done把它保存为 login.py、login.mjs 或 login.sh,然后运行:
python login.pynode login.mjsbash login.shWelcome, knife
Balance: 12,500
Chats: 3
done
shell 版本还会输出每条命令的回复,例如 {"ms":0,"via":"node"}。在 Windows 上,请在 Git Bash 中运行 shell 版本,或者参阅 CLI 指南中同一脚本的 PowerShell 版本。
5. 出错时
把点击那一步中的 "Log in" 改成 "Sign up",再运行一次脚本。它会因下面这样的错误而停止(Python 版本把它显示为 traceback 的最后一行):
droidline._generated.NotFoundError: Could not find text 'Sign up' within 10s. Current screen: dev.droidline.demo / .LoginActivity
消息说明了查找的是什么、找了多久,以及当时显示的是哪个界面。每个错误还带有一个 code(这里是 NOT_FOUND),并说明重试是否可能有用。要处理错误而不是停止:
from droidline import connect, NotFoundError
d = connect()
try:
d.touch("text", "Sign up", timeout=2)
except NotFoundError as e:
print(e.code, e.retryable, e)import { connect, DroidlineError } from "droidline";
const d = await connect();
try {
await d.touch("text", "Sign up", { timeout: 2 });
} catch (e) {
if (!(e instanceof DroidlineError)) throw e;
console.log(e.code, e.retryable, e.message);
}if ! droidline touch text "Sign up" --timeout 2; then
echo "not there, carrying on"
fitimeout=2 让命令在 2 秒后放弃,而不是 10 秒。在 shell 中,失败的命令会输出错误并以状态码 1 退出,if ! 会捕获它。错误码列出了所有错误码。
6. 在真机上运行
同样的脚本可以在真机上运行。只有与应用相关的细节需要改变: