编辑与预览模拟器回归脚本
脚本:test/ui/run.py。 保留30 分钟测试报告的 E01–E04, 并补充编辑历史、Markdown 预览、草稿恢复与关闭确认。当前预期均为通过。
代码组织与扩展
test/ui/driver.py:AndroidDriver负责 ADB、界面树、点击输入、等待、隔离仓库、断言及证据采集。wait_enabled(label, enabled, nodes=None)等待文字或无障碍标签对应控件的启用状态,返回完整界面树供后续步骤复用。- 例如
driver.wait_enabled('保存', False)等待保存按钮禁用;沿用统一轮询、超时与证据采集。 wait_selected(label, nodes=None)等待模式的无障碍标签带有选中状态,返回完整界面树;仅出现提示文字不算选中。- 编辑与预览模式统一使用
driver.wait_selected('编辑')或driver.wait_selected('预览')。
test/ui/note_editing.py:E01–E04 的业务步骤及四种场景,通过传入的驱动操作应用。test/ui/edit_preview.py:独立创建第五篇笔记,执行 E01–E04 及 EP01–EP16 编辑、预览检查。test/ui/run.py:命令行入口,负责设备选择、轮次、unittest 用例执行、日志生命周期及汇总报告。test/ui/test_driver.py:不连接设备的驱动单元测试;test/ui/test_runner.py验证用例调度与报告。test/ui/test_edit_preview.py:验证多行输入的空行与动作顺序及扩展用例注册;模式选中条件由驱动单元测试验证。- 新增用例时,在
test/ui/新建业务模块,提供tests(driver, repeat)返回 unittest 测试列表, 并提供每轮断言数CHECKS_PER_ROUND, 再加入run.py的CASES;驱动无需依赖具体用例编号。 - 当前注册笔记编辑及编辑预览两组用例,每轮执行 36 项断言;总断言数按已注册用例计算。
- 通过标准库
unittest.FunctionTestCase复用现有业务函数,每轮四种笔记创建场景各一个测试, 编辑预览完整流程一个测试,共五个测试;同一场景内的连续步骤保持顺序。 - 每轮初始化独立仓库后,由
unittest.TextTestRunner以failfast=True执行;首个失败后停止后续场景和轮次, 完整异常继续写入 JSON 和 Markdown 报告,截图、界面树及 logcat 保留。- 测试套件按
CASES中的模块顺序、各模块返回的测试顺序逐个添加。
- 测试套件按
- UI 断言不符抛出
AssertionError,在 unittest 中计为失败;设备操作异常计为错误。 - 重构前的历史证据仍位于下文记录的
build/e01-e04/,新执行结果统一写入build/ui/。
准备
- 安装 Python 3.8 或更新版本及 Android SDK Platform Tools,确保
python、adb在 PATH 中。 - 使用运行脚本的同一个 Python 安装固定版本依赖:
python -m pip install -r test/ui/requirements.txt。 - Windows 若使用 Python 启动器,上述安装和执行命令中的
python均可替换为py -3。 - 启动模拟器,安装本项目需要验证的 Debug APK;
run-as必须能读取应用测试数据。 - 使用竖屏,关闭已有弹窗,先保存当前笔记。
- 运行期间避免手动操作目标模拟器。
- 脚本会重启应用、切换到新建的独立本地测试仓库,不自动构建或安装 APK。
- 切换仓库遵循应用现有的自动保存行为;脚本不主动同步,也不删除原有笔记或测试数据。
执行
在项目根目录运行:
python test/ui/run.py --device emulator-5554
连续执行三轮:
python test/ui/run.py --device emulator-5554 --repeat 3
--device可以省略,此时必须只有一台在线 Android 模拟器。--repeat默认为 1,必须为正整数;每轮使用新仓库,名称以QA-UI-开头。- 每轮创建五篇笔记,各执行 E01–E04,另对
preview.md执行 EP01–EP16,共 36 项断言;三轮共 108 项。 - 测试或环境失败时停止后续动作并返回退出码 1,全部完成返回 0;参数错误返回 2。
- PowerShell 可读取
$LASTEXITCODE。
清理测试生成库
先保存当前笔记并结束回归运行,然后在项目根目录执行:
python test/ui/cleanup.py --device emulator-5554 --dry-run
python test/ui/cleanup.py --device emulator-5554
--dry-run仅列出待删除仓库,不停止或重启应用;省略该参数时立即删除。- 复用回归脚本的设备选择规则,
--device可在只有一台在线模拟器时省略。 - 只清理本地目录内名称完整匹配
QA-UI-<10 位小写十六进制>-<正整数>或历史QA-E01-E04-<10 位小写十六进制>-<正整数>的仓库,并核对元数据 ID 与实际目录。 - 普通仓库、带远端 ID 或同步身份文件的仓库不删除;发现目录重定向时中止。
- 删除包含库内笔记、附件和元数据,无法撤销;不会删除主机上的回归报告或远端仓库。
- 删除前停止应用,完成后自动启动刷新仓库列表;若已删除当前选中库,清除其选择记录。
- 失败会返回非零退出码并保留异常;若删除中途失败,之前完成的删除不会回滚。 应用可能保持停止状态,排除错误后重新执行脚本或手动启动。
检查内容
| 编号 | 自动操作与断言 |
|---|---|
| E01 | 在空仓库创建首篇笔记、应用重启后再建、隐藏键盘后再建、第四次新建;确认进入空白编辑器 |
| E02 | 实际输入两行英文正文,使用 Enter 产生换行,逐字核对编辑区 |
| E03 | 点击保存,等待保存按钮禁用,通过 run-as 读取本轮测试文件,与正文逐字比较 |
| E04 | 关闭已保存标签,从文件选择器重开,切换到源码,与磁盘和预期正文逐字比较 |
编辑与预览扩展检查
夹具通过实际输入追加,不直接写入应用文件。preview.md 包含空行、标题、强调、列表和引用。
使用 ASCII 内容避开 ADB input text 的 Unicode 注入限制;中文和输入法组合不由此组用例证明。
追加前按当前短夹具的字符数发送右方向键,保证到达全文末尾;不依赖点击后的光标位置或
Android 注入 Ctrl+End 时的修饰键状态。这种方式仅用于短夹具,长文应单独设计选区定位。
| 编号 | 自动操作与断言 |
|---|---|
| EP01 | 输入 Markdown,逐字核对源码,包括连续空行 |
| EP02 | 未保存时保存按钮可用,磁盘仍为上次保存版本 |
| EP03 | 点击预览,确认模式选中且没有可编辑文本框 |
| EP04 | 未保存内容的标题、列表和引用出现在预览,标题和加粗源码标记未原样显示 |
| EP05 | 切回编辑,源码逐字不变,未意外保存 |
| EP06 | 保存 Markdown,确认按钮禁用且磁盘全文一致 |
| EP07 | 追加独立草稿标记,核对正文 |
| EP08 | 通过笔记菜单撤销,恢复保存版本,磁盘未变 |
| EP09 | 通过笔记菜单重做,恢复草稿,磁盘未变 |
| EP10 | 再次往返编辑和预览,草稿完整保留 |
| EP11 | 往返后再次撤销,证明历史未因预览丢失;随后重做准备恢复测试 |
| EP12 | 等待草稿保存防抖后强制停止并重启,恢复编辑模式及草稿,磁盘仍为保存版本 |
| EP13 | 关闭脏标签,确认显示应用内弹窗及取消、丢弃按钮 |
| EP14 | 取消关闭,标签及草稿逐字保留 |
| EP15 | 丢弃并关闭,标签消失且磁盘未改写 |
| EP16 | 重开默认预览,再切源码,确认丢弃的草稿未复活 |
这组断言检查语义内容与状态,不以语义树证明字体、颜色或强调样式正确;截图留供人工检查。
失败即停止,报告中的 failed_at 指向正在执行的用例编号。
所有点击均根据当时的无障碍树定位,不依赖历史测试仓库 ID 或固定屏幕坐标。
界面状态等待有超时,失败的中间截图和树也会保留;不会无限重试掩盖失败。
只读界面树由常驻 uiautomator2 服务采集;单次 RPC 请求超时 12 秒,断连恢复由依赖库最多执行一次。
仓库列表或新建笔记入口点击后仍停留在主界面时,按新观测补点一次;创建、输入和保存断言失败不自动重做。
观察复用
- 弹窗、输入框和按钮定位可复用同一次界面树;进入编辑器后的空内容检查与首次定位也共用观察。
- 点击、输入、隐藏键盘或重启后重新采集;传入的树不满足条件时继续采集并等待,保留原有超时和歧义检查。
- 每次实际采集仍保存 XML 和截图,失败重试规则不变;合并的检查可能引用同一张截图。
- 正常路径每轮减少约 27 次重复采集;按单次约 2 秒估算可节约约 54 秒,实际受界面等待和重试影响。
- 无设备单元验证:
py -3 -m unittest discover -s test/ui -p "test_*.py" -v。
常驻采集服务
- 依赖固定为
uiautomator2==3.7.1;脚本开始时连接设备,复用服务直接返回 XML,省去反复启动命令与中间文件读取。 - 首次连接会部署依赖包自带的
u2.jar并启动设备端服务,无需修改或重新安装本项目 APK。 - 使用公开的
jsonrpc_call('dumpWindowHierarchy', [False, 50], timeout=12),保留非压缩节点及 50 层深度。 - 不经过高层
dump_hierarchy()的三次空树重试;空树交给现有状态等待处理,保持失败证据可见。 - HTTP 或无障碍连接异常由库重启服务并重试一次,脚本不再叠加采集异常重试;最终失败记录完整异常并尝试截图。
- 12 秒是单次 RPC 限制;恢复时另有停止服务、启动服务(最多等待 30 秒)及第二次请求的耗时。
- 状态等待的 25 秒仍在每次观察返回后检查,并非包含服务恢复在内的严格总时限。
- ADB 点击、输入、截图、日志和文件校验保持原有方式;
hierarchy-timing记录每次界面树采集秒数。 - 原 shell 采集保留在
dump_shell(label)中供手动诊断,正常回归不调用、不自动回退。- 不要同时运行原生
uiautomator dump和常驻服务;手动诊断前先停止常驻服务。
- 不要同时运行原生
- 脚本结束后不强行停止设备服务,后续执行可复用;模拟器重启或服务退出后,下次连接会重新启动。
输出
每次执行在以下目录创建独立结果:
build/ui/<时间戳-随机后缀>/
report.md 可阅读的检查结果及截图链接
results.json 总体结果、设备、各项断言、失败阶段、初始化警告
actions.jsonl 带时间戳的操作与观测记录
logcat.txt 本次运行期间的 Android 日志
001.png / 001.xml ...
- 终端首先打印完整结果目录,每项断言输出
PASS或FAIL。 - 测试仓库与笔记保留在模拟器内,便于检查;重复运行不会覆盖之前的仓库或证据。
flutter clean会删除build/下的结果;需要长期保留时,复制对应的单次结果目录。- 脚本不自动录屏,也不连接 Dart VM 事件流;通过界面和磁盘断言判断用例,logcat 作为辅助证据。
仓库初始化与已知限制
- 每轮通过应用界面创建新仓库,随后统一重启应用,再确认该仓库已选中后开始 E01。
- 当前版本另观察到“新建本地仓库”对话框关闭时可能红屏。
- 该步骤属于测试环境初始化,不属于 E01 的新建笔记断言。
- 重启前未显示新仓库主界面时,脚本在终端、JSON 和 Markdown 报告记录
WARNING,保留对应截图。 - 重启后仍未恢复该测试仓库时立即失败,不会改用原有仓库继续执行。
- E01–E04 的通过不代表仓库初始化异常已修复。
- 本脚本针对 Android Debug 版的竖屏界面;不覆盖真机输入法、Release、iOS、双指缩放、 双击源码定位、长文滚动、附件、链接跳转或横屏;相关历史人工检查不等于本脚本自动覆盖。
本次验证
- 2026-10-23,在 Android 15、320 × 640、160 dpi 的
emulator-5554上连续执行--repeat 2。 - 两轮共 32 项 E01–E04 断言全部通过,使用两个不同的新建本地仓库。
- 两轮均记录了仓库初始化警告;重启后测试仓库恢复成功,业务断言没有因此跳过。
- 验证了不存在的设备返回退出码 1 并写入失败报告,非法重复次数返回退出码 2。
flutter test:360 项通过;flutter analyze:只有原有的 1 条格式 info。
2026-10-24 观察复用验证
- Android 模拟器
emulator-5554一轮 16 项断言全部通过,共 55 次观察,操作日志跨度约 165 秒。 - 结果目录:
build/e01-e04/20261024-084503-4068f2/,保留 XML、截图、日志和检查报告。 - 仓库初始化记录 1 次既有警告,重启恢复后全部业务检查通过。
- 观察复用单元测试 4 项通过;
flutter test366 项通过。 flutter analyze仅有原有的test/mp3_refresh_view_test.dart:37:11格式 info。
2026-10-24 常驻服务验证
- 安装并固定
uiautomator2 3.7.1;pip check未发现依赖冲突。 - 初次接入时,隐藏键盘场景因弹窗移动中读取到旧位置,在等待编辑器时失败。
- 失败证据保留于
build/e01-e04/20261024-110311-430917/。 - 增加隐藏键盘后的 0.5 秒动画等待,再重新读取位置,随后连续两轮均通过该场景。
- 失败证据保留于
emulator-5554 --repeat 2的 32 项断言全部通过;两轮共 111 次观察、约 97 秒,平均每轮约 48 秒。- 对照此前 shell 采集单轮约 165 秒,本次平均每轮用时减少约 71%;这是本机实测,非固定性能保证。
- 界面树 RPC 耗时中位数约 0.076 秒、平均约 0.100 秒;截图与其他动作另计。
- 两轮各记录 1 次既有仓库初始化警告,重启后业务断言全部通过。
- 结果目录:
build/e01-e04/20261024-110449-112918/。
- 在两轮结束后识别并停止采集服务进程,下一次观察自动恢复,耗时约 1.1 秒,重新获得包含应用节点的树。
- 恢复前后 XML、截图和结果:
build/e01-e04/service-recovery-20261024-1107/。
- 恢复前后 XML、截图和结果:
- 脚本单元测试 9 项、
flutter test366 项全部通过。 flutter analyze仅有原有的test/mp3_refresh_view_test.dart:37:11格式 info。- 未验证真机、iOS 或旧版本 Python;应用 APK 无需重启或重装,重新执行脚本即可使用新采集方式。
2026-10-24 驱动与用例拆分验证
- UI 测试代码与依赖迁移至
test/ui/,运行入口改为python test/ui/run.py。 - 原观察测试与新增用例调度、断言数量校验、报告生成测试共 12 项通过。
- 笔记编辑步骤经 Python AST 比对,除
self改为传入的driver外与原步骤一致。 flutter test366 项通过;flutter analyze仅报告既有的test/mp3_refresh_view_test.dart:37:11格式 info,退出码为 1。emulator-5554实跑在初始化时等待“新建本地仓库”超时,未进入 E01–E04,不能视为业务回归通过。- 点击仓库入口后仍显示原笔记界面;本次保留原有点击与重试行为,未扩展修复初始化逻辑。
- 失败报告、XML、截图及 logcat 位于
build/ui/20261024-120400-5aad3c/。
- 无需重启或重新安装应用;使用新入口重新执行测试即可。
2026-10-24 测试库清理验证
cleanup.py --device emulator-5554 --dry-run识别 9 个历史测试库;实际删除后再次预览为 0 个。- 原有 4 个普通仓库仍在,应用已自动重启;主机回归证据保留。
- Python 单元测试共 16 项通过,覆盖完整名称匹配、同步库保护、路径重定向拒绝、预览及选择记录清理。
flutter test366 项通过;flutter analyze仍仅报告既有的test/mp3_refresh_view_test.dart:37:11格式 info,退出码为 1。- 本次只修改 Python 脚本及文档,未重新构建 APK,未验证真机或 iOS。
2026-10-24 编辑与预览用例扩展
- 每轮由 16 项扩展为 36 项,保留原四种创建场景,另增加一篇独立 Markdown 笔记。
- Python 单元测试 19 项通过;
flutter test366 项通过。 flutter analyze仅报告既有的test/mp3_refresh_view_test.dart:37:11格式 info,退出码为 1。- 首次实跑在重启后等待新库超时,未进入业务用例。
- 失败证据:
build/ui/20261024-124122-97439f/。
- 失败证据:
- 第二次实跑原 E01–E04 的 20 项及 EP01–EP06 通过,EP07 捕获测试输入定位错误。
- Android 注入
Ctrl+End后,标记实际插入列表行末而非全文末尾;逐字断言正确失败。 - 修正为按短夹具字符数发送方向键后再追加,保留失败证据:
build/ui/20261024-124330-b91b24/。
- Android 注入
- 第三次实跑 E01–E04 的 20 项及 EP01–EP07 通过,EP08 因菜单名称匹配不完整而超时。
- 菜单无障碍名称包含换行及快捷键,已使用完整的
撤销\nCtrl+Z、重做\nCtrl+Shift+Z定位。 - 失败证据:
build/ui/20261024-124758-1f51a0/;没有放宽正文或磁盘断言。
- 菜单无障碍名称包含换行及快捷键,已使用完整的
- 修正菜单名称后,单独执行扩展模块,E01–E04 及 EP01–EP16 共 20 项全部通过,退出码为 0。
- 结果目录:
build/ui/20261024-125216-3219c3/,共 57 次观察,保留报告、XML、截图和日志。 - 记录 1 次既有仓库初始化警告,重启后恢复;该警告仍未修复。
- 原四种场景的 16 项在第三次完整运行中全部通过;最终修改仅调整扩展模块菜单定位。
- 本次分组验证了全部注册检查,最终版本未再次将 36 项作为同一次完整运行执行。
- 结果目录:
- 这些检查使用模拟器已安装的 Debug APK,未重新构建,不能据此确认安装包与当前源码一致。
- 无需重启或重新安装应用以使用新脚本;执行脚本本身会按场景重启应用。
2026-10-24 unittest 接入验证
- 用
unittest.FunctionTestCase包装完整场景,保留原业务函数、命令行参数、每轮新建仓库及证据格式。 - Python 单元测试 22 项通过,新增验证五个场景的注册和调度、断言失败与设备异常的立即停止、 后续轮次不再初始化、完整异常和失败截图链接保留;UI 断言不符计为 unittest 失败。
flutter test376 项通过;flutter analyze仅报告既有的test/mp3_refresh_view_test.dart:37:11格式 info,退出码为 1。emulator-5554实跑一轮:unittest 执行五个测试,前四个通过,编辑预览测试在 EP12 报错,退出码为 1。- 31 项断言通过;重启后等待“编辑”模式选中超时,EP12–EP16 未完成,不能视为完整 UI 回归通过。
- 终端、JSON 和 Markdown 保留完整异常,
failed_at为round-1/preview/EP12;后续步骤停止。 - 结果目录:
build/ui/20261024-133723-cc06df/,最后观察为129,保留截图、XML、操作记录和 logcat。 - 仓库初始化仍记录一次界面警告,重启恢复后才开始业务场景。
- 未重新构建或安装 APK,使用模拟器已安装的 Debug 版;未验证真机、iOS 或其他 Python 版本。
- 无需重启或重新安装应用以使用本次脚本修改;重新运行原命令即可,脚本仍按场景重启应用。
