ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Readest 安卓 E2E 双击手势测试修复实录:CDP 合成手势与 250ms 消歧窗口的攻防战

Readest 安卓 E2E 双击手势测试修复实录:CDP 合成手势与 250ms 消歧窗口的攻防战 桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载导读本文完整复盘 Readest 仓库中一次典型的“夜间回归失败”排查与修复Android E2E (CDP)车道在 PR #4846 合并后的每个夜晚都因double-click.android.test.ts超时而失败。文章以 android-e2e-doubletap-cdp-gesture.md 的记忆文档为主体结合 double-click.android.test.ts、cdp.ts、reader.ts 等源码深入剖析两个叠加的根因、纯 harness 侧的修复方案以及一套可复用的“通过 CDP 在 Android WebView 中可靠合成触摸双击手势”的工程实践。读完你既能理解 Readest 阅读器中双击选词的触发原理与移动端默认配置的取舍也能直接复用这套修复思路解决同类 E2E 时序类问题。一、问题现象从未在 CI 上通过的双击测试1.1 失败时间线与典型报错记忆文档记录了一个非常清晰的失败画像从2026-06-28PR #4846 合并后的第一个夜晚开始夜间Android E2E (CDP)车道每晚必挂报错统一为timed out waiting for selection of ... (last: null)即测试等待“双击选中某单词”这一条件永远不满足该测试在 CI 上从未通过直到2026-07-04才修复关键结论修复只发生在测试 harness 侧src/__tests__/android/应用代码零改动。这个结论本身就是一条重要工程经验并非每次夜间失败都意味着产品 bug也可能是测试环境/测试手段与产品默认配置之间的假设错位。1.2 被测功能的真实行为先看测试本身要验证什么。double-click.android.test.ts 顶部注释说得很清楚End-to-end coverage for the double-click / touch double-tap gesture: tapping a word twice quickly selects that word — as if long-press-selecting it — and raises the annotation toolbar.测试流程it(selects the double-tapped word and shows the annotation toolbar)通过locateAnyWord(page)在渲染好的书页里找一个“安全的单词”≥4 个拉丁字母、无相邻撇号、远离页边、且必须渲染为单行矩形对单词中心执行page.doubleTap(hit.cssX, hit.cssY)waitFor轮询直到getSelectionState(page)返回“存在且未折叠”的选择并断言sel.text hit.word整词选中行为与长按选词一致断言.selection-popup标注工具栏出现——因为默认没有配置快捷操作annotationQuickAction nulldismissSelection(page)收尾清理。二、根因一主因双击选词在移动端是默认关闭的 opt-in 功能2.1 默认配置的“陷阱”记忆文档给出的第一层根因是PR #4846 引入的双击选词特性在移动端默认关闭。证据在 constants.ts 中直接可见DEFAULT_VIEW_SETTINGS桌面默认中disableDoubleClick: falseconstants.tsDEFAULT_MOBILE_VIEW_SETTINGS中disableDoubleClick: trueconstants.ts移动端显式覆盖为“关闭双击检测”。移动端为何选择关闭因为双击检测会给单击翻页引入延迟单次点击后需要等待一段“消歧窗口”来确认用户是否还会再点第二下才能决定是翻页还是选词。这个窗口时长定义在 constants.tsexport const DOUBLE_CLICK_INTERVAL_THRESHOLD_MS 250;配套的还有LONG_HOLD_THRESHOLD 500长按阈值constants.ts。也就是说在移动端默认配置下为了不让“快速翻页”的手感被双击检测拖慢 250ms产品选择直接放弃双击选词这个能力——它是用户主动开启的 opt-in 功能。该默认值还有单测守护constants.test.ts 断言DEFAULT_MOBILE_VIEW_SETTINGS.disableDoubleClick true。2.2 handleClick 的分支逻辑窗口“从未被武装”在 iframeEventHandlers.ts 的handleClick中双击窗口的行为清晰可循export const handleClick (bookKey, doubleClickDisabled, isFixedLayout, isComicBook, event) { const now Date.now(); if (consumeSuppressedDomClick(bookKey, event, now)) return; if (!doubleClickDisabled.current now - lastClickTime DOUBLE_CLICK_INTERVAL_THRESHOLD_MS) { lastClickTime now; // ... 命中双击漫画/图片特殊处理否则 postMessage(iframe-double-click) return; } lastClickTime now; const postSingleClick () { /* ... postMessage(iframe-single-click) */ }; if (!doubleClickDisabled.current) { // 窗口已武装延迟 250ms 再决定是否派发 single-click setTimeout(() { if (Date.now() - lastClickTime DOUBLE_CLICK_INTERVAL_THRESHOLD_MS) { postSingleClick(); } }, DOUBLE_CLICK_INTERVAL_THRESHOLD_MS); } else { // 窗口未武装立即派发 single-click postSingleClick(); } };两个关键分支一目了然doubleClickDisabled.current true移动端默认第二次点击永远不会命中“两次点击间隔 250ms”的分支iframe-single-click在点击后~20ms 内立即发出双击窗口从未被“武装”doubleClickDisabled.current false桌面默认/用户开启第一次点击后延迟 250ms 再派发 single-click若期间又来一次点击则转为iframe-double-click诊断特征为单击事件约~250ms后才发出。这正是记忆文档给出的诊断签名Diagnostic signature:iframe-single-click~20ms after click (window disabled) vs ~250ms after (window armed, no second click).2.3 为什么测试永远过不了E2E 测试跑在全新/刚装的模拟器上应用使用出厂默认配置移动端disableDoubleClick: true于是双点击中后第一次点击立即触发iframe-single-click→ 翻页根本没有iframe-double-click事件发出 → 永远不会出现选中状态 →waitFor一直轮询到超时抛出timed out waiting for selection of ...。测试假设了“默认配置下双击可用”而产品语义是“用户需要显式开启”——测试与产品默认行为的假设错位这是第一层根因也是主因。三、根因二次因adb 双拍根本无法命中 250ms 窗口即使把disableDoubleClick关掉旧 helper 用 adb 发送两次独立点击也几乎必然失败。记忆文档给出了实测数据旧实现是input tap x y input tap x y每一次input调用都会spawn 一个全新的app_processJVM实测单次开销0.9~1.05s冷启动更可达28s在速度快的宿主机上两次点击间隔约130ms测试偶尔能过在负载高的 CI 模拟器上间隔必然超过250ms→ 永远失败。两次点击间隔若大于DOUBLE_CLICK_INTERVAL_THRESHOLD_MS应用端就判定为两次独立单击直接翻两次页选词永远不会发生。这就是第二层根因注入手段的时序精度达不到被测应用的时间预算。四、修复方案全部落在测试 harness 侧4.1 方案 ApatchGlobalViewSettings——用 run-as 直接改写持久化配置既然默认配置不对测试就应在启动前把配置“播种”成用户开启该功能后的状态。reader.ts 中的patchGlobalViewSettings(patch)实现了这一目标步骤为am force-stop强停应用防止应用退出时用内存中的旧配置把补丁覆盖回去通过adb shell run-as pkg读取应用数据目录根目录注意不是files/子目录下的settings.json解析 JSON往globalViewSettings对象写入补丁键值undefined表示删除该键通过 base64 管道写回文件返回被覆盖键的旧值供afterAll恢复现场。两个精妙的兼容点settings.json 不存在也无妨全新安装时文件可能尚未生成读取失败就按空对象处理应用侧loadSettings会把部分文件与平台默认值做deep-merge因此播种一个最小化的 settings 文件完全可行run-as只对 debug 构建有效与本车道“仅针对 debug 构建”的定位一致。在测试中double-click.android.test.tsbeforeAll(async () { savedViewSettings await patchGlobalViewSettings({ disableDoubleClick: false }); page await openFixtureBook(FIXTURE); }, 120_000); afterAll(async () { page?.close(); if (savedViewSettings) await patchGlobalViewSettings(savedViewSettings); }, 60_000);测试前强制把disableDoubleClick置为false即用户开启双击选词的状态结束后恢复原值——测试环境被精确“装扮”成用户开启功能后的形态这正是记忆文档所说的the lane seedsdisableDoubleClick: false— the setting a user who wants the gesture turns on — and restores the previous value afterwards.4.2 方案 BCdpPage.doubleTap——一次 CDP 手势替代两次 adb 点击时序精度问题的解法是放弃 adb 层注入改走WebView 自身的输入管线。cdp.ts 中的实现只有一次调用async doubleTap(cssX: number, cssY: number): Promisevoid { await this.send(Input.synthesizeTapGesture, { x: cssX, y: cssY, tapCount: 2, duration: 20, gestureSourceType: touch, }); }为什么这样可行记忆文档给出了三条实测结论tapCount: 2的单次手势由渲染进程内部计时即使模拟器繁忙两次点击间隔实测约200ms稳定落在 250ms 窗口内两次独立的synthesizeTapGesture命令仍然太慢——每条命令要等其手势完全结束后才 resolve实测间隔约535ms超出窗口裸的Input.dispatchTouchEvent能派发触摸事件但无法可靠地在 Android WebView 中合成出 clickclick 合成需要浏览器完整的手势识别管线因此不可用。换言之关键不是“快”而是让渲染器内部的时序来保证两次点击的间隔而不是依赖外部命令往返的开销。CDP 会话的建立同样值得留意cdp.tsAndroid WebView 在开启远程调试后会暴露名为webview_devtools_remote_pid的抽象 unix socketharness 通过pidof找到应用 pid、检查 socket 存在后用adb forward tcp:port localabstract:webview_devtools_remote_pid把端口转发到宿主机再连 WebSocket 驱动Runtime/Input域。注意一个细节WebView 的 HTTP 帧格式会干扰 curl代码注释明确要求用node:http并显式携带Host: localhost头。4.3 单词定位的“单行矩形”约束双击命中的必须是目标单词的中心而页面里的单词可能被连字符断行或折行。locateAnyWorddouble-click.android.test.ts用range.getClientRects()检查命中词的外接矩形要求rects.length 1否则跳过该词若单词被连字符断行/折行其包围矩形会横跨两行中心点恰好落在两行之间的行隙上实际点击会选中相邻单词——排查记录中真实出现过party ! sensation的断言失败因此只选择“渲染为单个行盒”的单词作为双击靶点同时要求单词 ≥4 个字母、前后无撇号保证原生选词与分词器结果一致、且中心点在屏幕安全区内避开页边距。4.4dismissSelection避开标注工具栏的“盲区点击”测试清理阶段要点击空白处取消选中但.selection-popup标注工具栏紧贴选区渲染盲点 0.78H 高度可能正好按中工具栏按钮。reader.ts 的dismissSelection采用了两段式策略先page.evaluate读取.selection-popup的矩形在候选点[0.78, 0.25]页面高度比例中挑选一个与工具栏矩形上下间距 ≥24px的点若工具栏恰好盖住两者如选中位置偏低回退到 0.78H点击后等待“无自定义拖拽手柄selection-handle且选择为空”再clearDomSelection兜底清理 DOM 选区。五、其余坑位清单Nightly 实战补充记忆文档还记录了修复过程中踩到的三个环境级坑坑现象对策headless 模拟器屏幕休眠屏幕关闭后 adbinput注入变成 no-op而 CDP 注入仍然生效先发input keyevent KEYCODE_WAKEUP唤醒泄漏的单击污染会话一次失败的双击被识别成两次单击第二次单击会切换页眉/打开媒体查看器污染后续断言在beforeEach里检查并dismissSelection清理残留选择fixture 书冷启动上一次测试留下的 reader 会话会让 VIEW intent 重新导入并 remount foliate-view就绪探测可能命中 remount 窗口每次openFixtureBook都先am force-stop冷启动reader.ts此外reader.ts 的detectAndroidEnv还会预确认一次性的 “Viewing full screen” 系统弹窗settings put secure immersive_mode_confirmations confirmed——这个弹窗首次进入沉浸模式时会悬在屏幕上半部静默吞掉所有注入到该区域的触摸表现为“手势莫名其妙地不生效”而不是可见失败。六、本地复现与验证方式记忆文档给出了本地复现整条车道的命令链pnpm tauri android build --debug --target aarch64 adb install -r apk pnpm test:android要点必须是debug 构建——run-as与 WebView 远程调试都依赖 debug 签名/开关车道是软跳过设计detectAndroidEnv只有在 adb 可用、存在设备且已安装 Readest 时才真正执行否则打印警告并跳过double-click.android.test.ts因此pnpm test:android在任何机器上都是安全的CDP 端口默认9333可用环境变量READEST_CDP_PORT覆盖reader.tsfixture 书为src/__tests__/fixtures/data/sample-alice.epub通过android.intent.action.VIEWintent 以“打开方式”流程打开不污染用户书库文件推送后先经 MediaStore 注册拿到content://URI以便在无存储权限的全新模拟器上也能获得读授权reader.ts。七、经验总结这次修复留给后续开发者的方法论价值比修复本身更值得记录E2E 失败先核对“默认配置假设”PR #4846 的功能在移动端是 opt-inDEFAULT_MOBILE_VIEW_SETTINGS.disableDoubleClick: true测试却按“默认开启”设计这是所有夜间失败的源头。测试必须显式播种它依赖的配置而不是依赖出厂默认值注入手段的时序预算必须低于被测逻辑的阈值adb 单次点击的 JVM 开销约 1s远大于 250ms 消歧窗口再快的点按也追不上。改用渲染器内部计时的单次tapCount: 2CDP 手势把时序从“外部往返”转移给浏览器自身选择器要选“几何上无害”的目标被断行/折行的单词中心点会落在行隙上导致误选相邻词getClientRects().length 1是可靠的筛选条件清理动作也要防御 UI 覆盖取消选中前先查询工具栏位置避免盲点按中按钮并等待“无手柄且无选区”的完成态环境级弹窗与休眠是隐性杀手沉浸模式确认弹窗、headless 屏幕休眠都会静默吞掉注入测试环境准备阶段就应显式处理。如果你正在为自家应用的移动端 WebView 编写触摸手势 E2E这套“CDP 合成手势 run-as 配置播种 几何安全选点 环境预确认”的组合拳可以直接照搬如果你正在维护 Readest 的安卓车道记忆文档 android-e2e-doubletap-cdp-gesture.md 与相关测试源码就是这份经验的最新存档。赞分享桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载相关推荐CANN/ge DataFlow Python开发指南概述概述 DataFlow简介 DataFlow是一套面向异构系统的应用开发和执行框架提供了一套API来帮助用户快速表达业务逻辑。框架屏蔽异构硬件和组网的差异并人工智能模型编译模型优化模型推理服务CANNAscendMaterial Shell窗口操作手势配置自定义手势动作Material Shell窗口操作手势配置自定义手势动作 你是否还在为Linux桌面环境中繁琐的窗口操作而烦恼Material Shell提供了强大的窗口桌面应用PGmigrate完全指南PostgreSQL数据库迁移从未如此简单PGmigrate完全指南PostgreSQL数据库迁移从未如此简单 PGmigrate是由Yandex开发的PostgreSQL数据库迁移工具它让Post上一篇拖 ncm 到 main.exe 上免费、本地跑、零门槛的 NCM 转 MP3下一篇QMK Firmware 2026-02-22 破坏性变更解析移除废弃 GPIO 定义与 isLeftHand附完整变更清单创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表