提交UI自动化插件

This commit is contained in:
张家训
2026-07-03 16:45:59 +08:00
parent 3f5e6e7ad0
commit bd605c47c7
17 changed files with 4423 additions and 29 deletions
+760
View File
@@ -0,0 +1,760 @@
# UI 自动化插件设计方案
> 插件名称建议:手机 UI 自动化
> 插件 ID 建议:`mobile-ui-auto`
> 核心技术栈:JavaFX 插件壳 + Python Runner + Airtest + Poco(pocoui) + ADB
> 目标:在 WorkUtils 中提供一个可视化入口,用于连接 Android 手机,对指定 APP 执行自动点击、输入、滑动、断言、截图和流程测试。
## 1. 设计结论
本插件不建议在 Java 里直接实现手机 UI 自动化细节,而是采用“双层架构”:
```text
WorkUtils Java 插件层
负责:可视化配置、脚本管理、流程编排、进度展示、报告入口、进程控制
Python 自动化执行层
负责:连接设备、启动 APP、查找元素/图片、点击输入、断言、截图、生成运行结果
```
选择理由:
- `pocoui/Poco` 和 `Airtest` 原生就是 Python 生态,直接用 Python 写动作最顺。
- Java 项目已有插件框架,适合做工具入口、配置管理、运行控制和报告汇总。
- 手机自动化经常依赖 ADB、截图、图片模板、设备状态,这些动作放在独立 Python 进程中更容易隔离异常。
- 后续如果要加 `uiautomator2`、Appium 或云真机,可以扩展 Python Runner,不破坏 Java 插件壳。
## 2. 能力边界
### 2.1 首期能力
| 能力 | 说明 |
|------|------|
| 设备检测 | 通过 ADB 列出已连接设备,展示 serial、状态、系统版本 |
| APP 启动 | 输入 package/activity 或仅 package,启动被测 APP |
| 自动点击 | 支持元素定位点击、图片定位点击、坐标点击 |
| 自动输入 | 支持输入文本、清空输入框、粘贴式输入 |
| 手势模拟 | 支持滑动、返回、Home、等待、按键事件 |
| 元素定位 | 优先 Poco 元素定位,必要时使用图片识别兜底 |
| 流程脚本 | 支持 YAML/JSON 步骤流,自动转 Python 执行 |
| Python 脚本 | 支持直接运行 `.py` 或 `.air` 脚本 |
| 运行日志 | Java UI 实时显示 Python stdout/stderr |
| 截图报告 | 每个关键步骤可保存截图,失败时自动截图 |
| 断言 | 支持元素存在、图片存在、文本存在、页面等待 |
### 2.2 非首期能力
| 能力 | 放到后续原因 |
|------|--------------|
| iOS 自动化 | 依赖环境复杂,首期聚焦 Android |
| 分布式设备池 | 需要设备调度、队列和并发报告模型 |
| 录制回放 | 可以实现,建议第二阶段加入;首期先保证脚本执行闭环 |
| 智能探索测试 | 需要页面状态建模和策略引擎 |
| Appium 兼容 | 跨平台价值高,但启动成本比 Poco 高 |
## 3. 推荐技术组合
### 3.1 主链路
```text
Python + Airtest + Poco(pocoui) + ADB
```
- Airtest:设备连接、截图、图像识别、触控、断言、报告。
- Poco(pocoui):UI 树定位、元素点击、输入、等待。
- ADB:设备列表、安装 APK、启动 APP、logcat、底层命令。
- AirtestIDE:作为辅助工具,用于新手录制/定位图片/生成初版脚本。
### 3.2 兜底链路
```text
uiautomator2
```
首期只做预留,不默认引入。适合 Poco 在部分原生控件上拿不到稳定 UI 层级时,作为元素定位增强。
## 4. 总体架构
```mermaid
flowchart LR
A["MainWindow / PluginManager"] --> B["MobileUiAutomationPlugin"]
B --> C["MobileUiAutomationWindow"]
B --> D["MobileAutomationService"]
D --> E["PythonEnvManager"]
D --> F["DeviceService"]
D --> G["CaseService"]
D --> H["PythonRunner"]
H --> I["runner.py"]
I --> J["Airtest API"]
I --> K["Poco AndroidUiautomationPoco"]
I --> L["ADB"]
J --> M["Android Device"]
K --> M
L --> M
I --> N["reports / screenshots / result.json"]
H --> O["实时日志回调"]
O --> C
```
## 5. Java 插件层设计
### 5.1 包结构
```text
src/main/java/com/example/workutils/biz/mobileuiauto/
MobileUiAutomationPlugin.java
MobileUiAutomationWindow.java
service/
MobileAutomationService.java
PythonEnvManager.java
DeviceService.java
CaseService.java
PythonRunner.java
model/
DeviceInfo.java
AutomationCase.java
AutomationRunConfig.java
AutomationRunResult.java
AutomationStep.java
```
### 5.2 资源和脚本结构
```text
src/main/resources/mobile-ui-auto/
python/
runner.py
requirements.txt
flow_executor.py
action_library.py
templates/
sample-login-flow.yaml
sample-case.py
```
运行时目录:
```text
data/mobile-ui-auto/
config.json
cases/
login-flow.yaml
smoke-test.py
images/
login_button.png
home_tab.png
reports/
20260703-153000/
result.json
run.log
screenshots/
airtest-report/
```
### 5.3 插件描述
```java
private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder()
.id("mobile-ui-auto")
.name("手机UI自动化")
.version("1.0.0")
.description("基于 Python + Airtest + Poco + ADB 的手机 APP 流程自动化测试工具")
.order(80)
.icon("📱")
.build();
```
项目里部分中文注释目前存在编码显示异常,实际落地时建议统一以 UTF-8 保存源码和文档,避免 UI 文案乱码。
## 6. JavaFX 界面设计
插件采用嵌入式 JavaFX 面板,分为 5 个区域:
```text
┌──────────────────────────────────────────────────────────────┐
│ 顶部工具栏:设备刷新 | Python环境检查 | 打开用例目录 | 打开报告目录 │
├───────────────┬──────────────────────────────────────────────┤
│ 左侧设备区 │ 右侧执行区 │
│ - 设备列表 │ - 被测APP package/activity │
│ - 当前设备 │ - 用例选择 YAML/PY/.air │
│ - 连接状态 │ - 运行参数:超时、失败截图、报告开关 │
├───────────────┼──────────────────────────────────────────────┤
│ 用例列表 │ 步骤预览 / 脚本预览 │
├───────────────┴──────────────────────────────────────────────┤
│ 底部日志区:实时日志、进度、失败原因、报告链接 │
└──────────────────────────────────────────────────────────────┘
```
### 6.1 页面控件
| 区域 | 控件 |
|------|------|
| 设备区 | `ComboBox<DeviceInfo>`、刷新按钮、设备状态标签 |
| APP 配置 | package 输入框、activity 输入框、启动 APP 按钮 |
| 用例区 | 用例表格、新建/导入/删除/复制按钮 |
| 执行区 | 运行、停止、保存配置、打开报告按钮 |
| 日志区 | `TextArea` 实时追加日志,错误行高亮 |
### 6.2 用户流程
```text
1. 打开“手机UI自动化”插件
2. 点击“检查环境”
3. 连接手机并打开 USB 调试
4. 点击“刷新设备”
5. 选择设备
6. 填写或选择 APP package
7. 选择流程用例
8. 点击“运行”
9. 查看实时日志、截图、结果
10. 失败时打开报告定位问题
```
## 7. Python 执行层设计
### 7.1 requirements.txt
```text
airtest
pocoui
pyyaml
```
说明:
- `airtest` 提供设备连接、点击、滑动、截图、图片断言、报告能力。
- `pocoui` 提供 Poco UI 层级定位能力。
- `pyyaml` 用于读取流程式 YAML 用例。
### 7.2 runner.py 入参
Java 通过 `ProcessBuilder` 调用 Python:
```text
python runner.py
--device Android:///<serial>
--package com.xxx.app
--activity .MainActivity
--case data/mobile-ui-auto/cases/login-flow.yaml
--report data/mobile-ui-auto/reports/20260703-153000
--timeout 20
```
### 7.3 runner.py 输出协议
Python 进程按行输出 JSON,Java 端解析后更新 UI:
```json
{"type":"log","level":"INFO","message":"连接设备成功"}
{"type":"step_start","index":1,"name":"点击登录按钮"}
{"type":"screenshot","path":"reports/xxx/screenshots/001.png"}
{"type":"step_pass","index":1,"elapsedMs":532}
{"type":"step_fail","index":2,"message":"元素不存在: username"}
{"type":"done","success":false,"reportPath":"reports/xxx/result.json"}
```
如果输出不是 JSON,则按普通日志展示,避免第三方库日志破坏执行。
## 8. 流程用例 DSL 设计
首期建议用 YAML 描述流程,便于非 Python 用户维护。
### 8.1 示例:登录流程
```yaml
name: login-smoke
app:
package: com.example.app
activity: .MainActivity
device:
connect: Android:///
settings:
timeout: 20
screenshotOnFail: true
steps:
- name: 等待首页
action: wait
target:
type: poco
query:
text: 首页
timeout: 15
- name: 点击我的
action: tap
target:
type: poco
query:
text: 我的
- name: 输入用户名
action: input
target:
type: poco
query:
name: username_input
text: test_user
- name: 输入密码
action: input
target:
type: poco
query:
name: password_input
text: "123456"
- name: 点击登录
action: tap
target:
type: image
path: images/login_button.png
- name: 断言登录成功
action: assert_exists
target:
type: poco
query:
text: 退出登录
```
### 8.2 支持动作
| action | 说明 | target |
|--------|------|--------|
| `start_app` | 启动 APP | package/activity |
| `stop_app` | 停止 APP | package |
| `tap` | 点击 | poco/image/coord |
| `double_tap` | 双击 | poco/image/coord |
| `input` | 输入文本 | poco |
| `clear` | 清空输入框 | poco |
| `swipe` | 滑动 | coord/image |
| `wait` | 等待元素出现 | poco/image |
| `sleep` | 固定等待 | seconds |
| `keyevent` | Android 按键 | BACK/HOME/ENTER |
| `screenshot` | 截图 | path |
| `assert_exists` | 断言存在 | poco/image |
| `assert_not_exists` | 断言不存在 | poco/image |
### 8.3 定位方式
```yaml
target:
type: poco
query:
text: 登录
```
```yaml
target:
type: poco
query:
name: com.example:id/login
```
```yaml
target:
type: image
path: images/login_button.png
threshold: 0.85
```
```yaml
target:
type: coord
x: 540
y: 1600
```
推荐优先级:
```text
poco 元素定位 > 图片定位 > 坐标定位
```
坐标定位只做兜底,因为不同分辨率、字体缩放和设备比例会导致脚本脆弱。
## 9. 录制回放设计
可以实现“手动模拟点击后记录下来,后续按记录自动模拟”的能力。推荐把它设计成“录制器 + 定位增强 + YAML 生成 + 回放校验”,而不是只保存点击坐标。
### 9.1 录制目标
用户在手机上手动操作一遍 APP,插件自动记录:
- 点击、长按、滑动、返回、Home、输入文本等事件。
- 每一步操作前后的截图。
- 当前页面 UI 层级信息。
- 被点击元素的 Poco 定位信息。
- 坐标和屏幕分辨率,作为兜底。
- 步骤间等待时间。
录制结束后生成一份可编辑 YAML 用例,后续可直接运行。
### 9.2 为什么不能只记录坐标
只记录坐标虽然最容易实现,但稳定性最差:
- 不同手机分辨率、状态栏高度、导航栏模式会导致坐标偏移。
- APP 页面布局变化后,坐标仍然能点下去,但可能点错位置。
- 弹窗、键盘、网络慢加载会让固定坐标回放失败。
因此每个录制步骤建议保存“多定位策略”:
```yaml
- name: 点击登录按钮
action: tap
target:
preferred: poco
poco:
text: 登录
name: com.example:id/login_button
image:
path: images/step_003_login_button.png
threshold: 0.85
coord:
x: 524
y: 1680
screenWidth: 1080
screenHeight: 2400
waitAfterMs: 500
```
回放时优先使用 Poco 元素定位;找不到元素时用图片识别;图片也找不到时才按比例坐标点击。
### 9.3 录制实现方案
首选实现路径:
```text
Java UI 点击“开始录制”
-> Python recorder.py 连接 Android 设备
-> 周期性读取屏幕截图和 UI 树
-> 用户在真实手机上操作
-> recorder 捕获操作差异/辅助确认
-> 生成 recorded-flow.yaml
```
Android 上直接无侵入捕获所有触摸事件有权限限制,因此录制有两种可落地模式:
| 模式 | 实现方式 | 优点 | 缺点 |
|------|----------|------|------|
| 辅助录制模式 | Java/Python 显示手机镜像或截图,用户在插件界面点,插件记录点击 | 最容易落地,可准确记录点击坐标和截图 | 不是直接点手机屏幕 |
| 真机旁路录制 | 用户点真机,录制器通过截图/UI树变化推断步骤,必要时让用户确认 | 更贴近真实操作 | 自动推断难度更高 |
建议 MVP 采用“辅助录制模式”:插件显示手机当前截图,用户点击截图上的位置,插件把点击转发到手机,并同时记录步骤。这样既能完成真实操作,又能准确生成脚本。
### 9.4 辅助录制交互
```text
1. 用户选择设备和 APP
2. 点击“开始录制”
3. 插件实时刷新手机截图
4. 用户在截图上点击、滑动、输入
5. 插件执行对应手机动作
6. 插件记录动作、截图、UI树和定位候选
7. 用户点击“停止录制”
8. 插件生成 YAML 用例
9. 用户点击“试运行”校验录制流程
```
### 9.5 录制事件模型
```json
{
"index": 3,
"action": "tap",
"timestamp": 1783071000123,
"screen": {
"width": 1080,
"height": 2400
},
"point": {
"x": 524,
"y": 1680
},
"pocoCandidates": [
{
"text": "登录",
"name": "com.example:id/login_button",
"bounds": [420, 1600, 660, 1740]
}
],
"imageTemplate": "images/step_003_login_button.png",
"beforeScreenshot": "screenshots/003_before.png",
"afterScreenshot": "screenshots/003_after.png",
"waitAfterMs": 500
}
```
### 9.6 Python 录制器能力
`recorder.py` 需要提供:
- 获取当前截图。
- 获取当前 Poco UI 树。
- 接收 Java UI 传来的点击/滑动/输入事件。
- 把屏幕坐标转换为设备坐标。
- 执行动作并记录前后截图。
- 根据点击位置匹配最近的 Poco 元素。
- 裁剪点击区域图片,生成图片模板。
- 输出 `recorded-flow.yaml`。
### 9.7 回放策略
回放每一步时按以下顺序执行:
```text
1. 如果有 Poco 定位,先用 Poco 查找并点击
2. 如果 Poco 失败,使用图片模板识别并点击
3. 如果图片失败,使用按分辨率比例换算后的坐标点击
4. 每步后等待 UI 稳定,必要时对比 afterScreenshot 或断言元素
5. 任一步失败时截图并写入报告
```
### 9.8 录制回放验收标准
- 能在插件界面显示当前手机截图。
- 用户点击截图后,手机同步执行点击。
- 插件能记录点击步骤并生成 YAML。
- YAML 能再次运行并完成同样流程。
- 至少支持点击、输入、滑动、返回。
- 失败时能定位到具体步骤并展示截图。
## 10. Python 核心伪代码
```python
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
def connect(device_uri):
auto_setup(__file__, devices=[device_uri])
return AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)
def tap_target(poco, target):
if target["type"] == "poco":
node = find_poco(poco, target["query"])
node.wait_for_appearance(timeout=target.get("timeout", 10))
node.click()
elif target["type"] == "image":
touch(Template(target["path"], threshold=target.get("threshold", 0.85)))
elif target["type"] == "coord":
touch((target["x"], target["y"]))
def input_target(poco, target, text):
node = find_poco(poco, target["query"])
node.wait_for_appearance(timeout=target.get("timeout", 10))
node.click()
text(text)
```
注意:实际实现里要避免函数名 `text` 与 Airtest API 冲突,可封装为 `airtest_text` 或显式 import。
## 11. 环境管理设计
### 11.1 Java 检查项
| 检查项 | 命令 | 失败提示 |
|--------|------|----------|
| Python | `python --version` | 未安装 Python 或未加入 PATH |
| pip | `python -m pip --version` | pip 不可用 |
| airtest | `python -c "import airtest"` | 请安装 airtest |
| pocoui | `python -c "import poco"` | 请安装 pocoui |
| adb | `adb devices` | ADB 不可用或设备未授权 |
### 11.2 自动安装策略
首期建议提供“复制安装命令”,不自动安装:
```text
python -m pip install -U airtest pocoui pyyaml
```
原因:
- 公司内网、代理、镜像源差异大。
- 自动安装可能污染用户全局 Python。
- 后续可以加虚拟环境模式:`data/mobile-ui-auto/.venv`。
## 12. 报告设计
每次执行生成独立目录:
```text
reports/20260703-153000/
result.json
run.log
screenshots/
001-start.png
002-login-fail.png
summary.html
```
`result.json`:
```json
{
"caseName": "login-smoke",
"device": "Android:///xxx",
"package": "com.example.app",
"success": false,
"startTime": "2026-07-03T15:30:00+08:00",
"endTime": "2026-07-03T15:30:18+08:00",
"steps": [
{
"index": 1,
"name": "等待首页",
"status": "PASS",
"elapsedMs": 1200
},
{
"index": 2,
"name": "点击登录",
"status": "FAIL",
"message": "图片不存在: images/login_button.png",
"screenshot": "screenshots/002-login-fail.png"
}
]
}
```
## 13. Java 服务接口草案
```java
public interface MobileAutomationService {
EnvironmentCheckResult checkEnvironment();
List<DeviceInfo> listDevices();
List<AutomationCase> listCases();
AutomationRunHandle runCase(AutomationRunConfig config, AutomationCallback callback);
void stopRun(String runId);
}
```
```java
public interface AutomationCallback {
void onLog(String level, String message);
void onStepStart(int index, String name);
void onStepPass(int index, long elapsedMs);
void onStepFail(int index, String message, String screenshotPath);
void onDone(AutomationRunResult result);
}
```
## 14. 进程控制设计
`PythonRunner` 负责启动、读取和终止 Python 进程:
- 使用 `ProcessBuilder`,不要拼接 shell 字符串。
- stdout/stderr 各自独立线程读取,避免进程阻塞。
- 支持超时终止。
- 点击停止时先发送中断标记,必要时 `destroyForcibly()`。
- 每次运行生成 `runId`,UI 只接收当前 `runId` 的回调。
## 15. 异常和稳定性策略
| 场景 | 策略 |
|------|------|
| 设备未连接 | Java 层运行前阻断 |
| 设备未授权 | 提示手机确认 USB 调试授权 |
| APP 启动失败 | 记录 package/activity 和 ADB 输出 |
| 元素找不到 | 自动截图,失败步骤写入 result.json |
| 图片识别失败 | 提示阈值、模板路径、当前截图 |
| Python 依赖缺失 | 环境检查页给出安装命令 |
| 运行超时 | Java 层终止 Python 进程并标记 TIMEOUT |
| 用户停止 | 标记 CANCELED,不算失败 |
## 16. MVP 迭代计划
### 16.1 第一阶段:跑通闭环
- 新增 `MobileUiAutomationPlugin`
- 新增 JavaFX 嵌入式窗口
- 实现环境检查
- 实现设备刷新
- 实现选择 YAML 用例并运行
- Python Runner 支持 `tap/input/wait/assert/screenshot`
- 生成 `result.json` 和截图
### 16.2 第二阶段:可用性增强
- 用例列表管理
- 新建 YAML 模板
- 图片模板目录管理
- 失败报告 HTML
- 支持 `.py` 和 `.air` 直接运行
- 支持停止运行
- 支持辅助录制回放:在插件截图面板点击、输入、滑动并生成 YAML
### 16.3 第三阶段:进阶能力
- 自动安装/管理 Python 虚拟环境
- uiautomator2 兜底定位
- 多设备并发
- logcat 采集
- APK 安装/卸载
- 接入 CI 命令行模式
## 17. 命令行模式设计
插件实现 `getCommandName()`:
```text
mobile-ui
```
命令示例:
```text
java -jar workutils.jar mobile-ui run --device Android:///xxx --case data/mobile-ui-auto/cases/login-flow.yaml
java -jar workutils.jar mobile-ui devices
java -jar workutils.jar mobile-ui check-env
```
这方便后续接入 Jenkins、GitLab CI 或本地批处理。
## 18. 与现有项目的集成点
| 集成点 | 设计 |
|--------|------|
| 插件注册 | `@Component` + `implements Plugin` |
| 可视化入口 | 实现 `getVisualFactory()` |
| 配置目录 | 复用 `data/`,新增 `data/mobile-ui-auto/` |
| 报告目录 | 复用 `export/` 或插件私有 `reports/` |
| 日志展示 | 参考现有 GitSyncWindow 的实时日志模式 |
| 命令执行 | 参考 GitSyncPlugin 的 `ProcessBuilder` 和输出读取模式 |
## 19. 风险和规避
| 风险 | 规避 |
|------|------|
| UI 测试脆弱 | 优先 Poco 元素定位,图片和坐标只兜底 |
| 录制坐标不稳定 | 录制时同时保存 Poco 候选、图片模板和比例坐标 |
| 手机分辨率差异 | 图片模板分设备维护,坐标按比例转换 |
| 输入法影响文本输入 | 支持 ADB 输入、剪贴板输入、Poco set_text 多策略 |
| 第三方依赖安装困难 | 先做环境检查和命令提示,后做虚拟环境 |
| Python 进程卡死 | Java 层超时和强制终止 |
| 报告不可读 | 每步结构化结果 + 失败截图 |
## 20. 首期验收标准
- 能在 Java UI 中检测 Python、Airtest、Poco、ADB 环境。
- 能刷新并选择一台 Android 设备。
- 能启动指定 APP。
- 能运行一个 YAML 流程:等待页面、点击、输入、断言。
- 能实时显示运行日志。
- 失败时能自动截图并展示失败原因。
- 能生成 `result.json`。
- 支持用户手动停止运行。
## 21. 参考资料
- Airtest 官方文档:https://airtest.readthedocs.io/en/latest/
- Airtest GitHub:https://github.com/AirtestProject/Airtest
- Poco GitHub:https://github.com/AirtestProject/Poco