尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

UI自动化中isVisible在滚动列表的误判与解法

UI自动化中isVisible在滚动列表的误判与解法 干 UI 自动化的人大概率在滚动列表上栽过跟头。你辛辛苦苦定位到一个元素明明已经滚出屏幕了isVisible()还是甩你一个True。我第一次遇到这个问题时先怀疑自己写错了选择器又怀疑是元素重名折腾到后半夜才发现不是脚本错了是“可见”这个词在不同框架里的定义和你想的完全不一样。这个坑在 Appium、XCUITest、Selenium 这类 WebDriver 体系里普遍存在尤其容易出现在订单列表、消息流、商品瀑布流这种长列表场景。这篇文章就把这个问题的根因、复现方法、排查思路和能落地的解决方案完整梳理一遍——如果你也卡在这里过应该能帮你省下好几个小时的调试时间。1. 问题的本质isVisible 到底在检测什么1.1 框架里的“可见”和人的直觉不一样先说结论绝大多数 UI 自动化框架里的isVisible()/isDisplayed()跟“元素有没有出现在你眼睛能看到的屏幕区域里”没有直接关系。以大家最熟悉的 Selenium WebDriver 为例WebElement 的isDisplayed()实现逻辑大体是检查元素的 CSS 样式display是否为none、visibility是否为hidden、元素尺寸是否为零以及它是否参与了渲染。它不会去对比元素坐标和浏览器窗口的矩形区域。也就是说一个元素就算被transform: translateY(99999px)移到屏幕外面从 Selenium 的角度看它依然是 displayed 的因为它真实存在于渲染树里。移动端也类似。Appium 在 Android 上用 UiAutomator2is_displayed()返回的是节点属性里的displayed字段。UiAutomator 怎么判定这个字段它看的是 View 的 Visibility 状态VISIBLE / INVISIBLE / GONE、宽高是否为正、以及是否经过了有效绘制。注意这里没有“是否在屏幕可视范围内”这一项。iOS 上 XCUITest 的情况更复杂visible、hittable、exists是三个不同的概念不同 Appium 版本里get_attribute(visible)的语义也不完全一致。所以很多时候你以为自己在问“我能不能看到它”框架其实只是回答了“它有没有被渲染出来”。这就要回到标题里的现象滚动列表里的 item明明已经滚出屏幕了isVisible()却永远返回True。这个现象不是偶发 bug而是框架设计使然。1.2 列表的缓存与复用机制让“离屏元素”继续活在视图树里真正让这个问题变得特别迷惑的是移动端列表的预渲染和缓存机制。Android 的 RecyclerView 会把 item 的 ViewHolder 缓存起来滚出屏幕的 item 要等到被回收后才会真正从父容器里移除。在这个“已经滚出去但还没回收”的时间窗口里这个 View 依然是父容器的一个子 View依然有 layout 参数依然参与了视图树的构建。UiAutomator 在 dump 元素树的时候自然会把它枚举出来。既然它是树上的有效节点displayedtrue就成立了。iOS 的 UITableView / UICollectionView 也有类似逻辑。cell 滚出屏幕后进入 reuse 队列但如果你在测试的时候刚好卡在“已经离屏、还没被复用”的节点上XCUITest 的 accessibility snapshot 里依然能看到它frame 坐标已经跑出屏幕边界了元素本身却还活着。更不要说现在很多 App 用的是 Flutter、React Native、Weex 这类跨端框架。它们都有自己的重排机制虚拟列表virtualized list为了滚动性能往往会预先渲染屏幕外一小段内容作为 buffer。这些“预渲染”的 item 就成为了视图树里的合法节点自动化框架看到的就是一个又一个“存在且可见”的元素。一句话总结isVisible()回答的是“元素在不在视图树里并且没有被标记为隐藏”而你想问的是“元素在不在当前屏幕的矩形范围里”。这两个问题之间的差距就是需要你自己补的那段逻辑。1.3 这个问题最容易伤到哪些测试场景这种“假可见”在很多场景下不痛不痒因为大部分测试的目标元素本来就在屏幕里。真正踩雷的场景有三类判断“某条旧消息是否已经滑出屏幕”想用isVisible()做断言结果永远为真断言不是必挂就是必过完全失去意义。在长列表里查找目标元素脚本逻辑是“先找出来再判断可见不可见就滚动”。结果因为永远可见脚本根本不滚动直接对屏幕之外的元素做点击或断言。列表做专项测试时统计可见 item 数量如果用isVisible()过滤数量永远等于列表总 item 数。这三类场景我都踩过下面把调试过程讲一下方便你对照排查。2. 稳定复现与证据链一步步证明不是玄学2.1 一个能稳定复现的最小脚本最好的排查方式是先写一个最小脚本把现象钉死。以 Python Appium 为例假设被测 App 是一个电商 App首页商品列表的 item 都有item_container这个 resource-id。测试步骤是进入列表 → 往上滑两屏 → 把整个列表的 item 都找出来 → 打印前几个和后几个元素的坐标和“可见性”。from appium import webdriver from selenium.webdriver.common.by import By caps { platformName: Android, automationName: UiAutomator2, appPackage: com.example.shop, appActivity: .MainActivity, noReset: True, } driver webdriver.Remote(http://127.0.0.1:4723/wd/hub, caps) driver.implicitly_wait(10) # 先下滑两屏确保列表处于中间位置 for _ in range(2): driver.swipe(540, 1800, 540, 600, 300) items driver.find_elements(By.ID, item_container) print(当前元素树里 item 的数量:, len(items)) screen driver.get_window_size() print(屏幕尺寸:, screen) for i in [0, 1, len(items) - 2, len(items) - 1]: el items[i] bounds el.get_attribute(bounds) displayed el.is_displayed() print(index{}, bounds{}, displayed{}.format(i, bounds, displayed))在这个脚本里滑动后的前两个 item 理论上已经“看不见”了。但如果 UiAutomator2 缓存了离屏 Viewbounds会明显大于屏幕高度。比如屏幕高 2400前两个 item 的 bounds 可能是[0, -600][1080, 200]或者[0, 4300][1080, 5000]而displayed依然打印True。这一幕我见过太多次第一次见时还以为是 Appium 版本 bug后来发现跟版本无关换哪个版本都一样。2.2 用页面结构源码当证据如果脚本还不够说服你直接看框架眼里的“世界”。Android 端可以命令行执行adb shell uiautomator dump /sdcard/ui.xml adb pull /sdcard/ui.xml .然后打开ui.xml定位到那个离屏 item 的节点你会看到类似这样的属性node index0 resource-idcom.example.shop:id/item_container bounds[0,4300][1080,5000] displayedtrue /注意bounds的坐标已经远超屏幕高度了但displayedtrue依然堂而皇之地挂着。因为在 UiAutomator 眼里“有没有绘制”和“绘制在不在屏幕内”是两套系统。前者由 View 的 visibility 和 attached 状态决定后者没有专门的属性。iOS 端也一样。用 XCUITest 的 snapshot 或者 Appium 的page_source能看到元素还在 accessibility 层级里frame 已经跑到{{0, 4500}, {390, 120}}而屏幕高只有 812。如果这时候调用isVisible()不同 Appium 版本给的答案可能不一样有的版本基于visible属性有的版本基于hittable这也是 iOS 上这个坑更难排查的原因之一。提示排查时不要只看返回值一定要把bounds或frame和屏幕尺寸一起打印出来。有了坐标证据问题根因就非常明确了。2.3 判断“元素是否真的在屏幕内”的证据公式到这一步我们需要的其实就是一个坐标相交判断。元素在屏幕内等价于这个矩形和屏幕矩形有交集。设元素四个边界是left / top / right / bottom屏幕宽高是screen_w / screen_h那么“元素至少有一部分露出”的数学条件是bottom 0 and top screen_h and right 0 and left screen_w这四个条件缺一不可。如果你只比对top和screen_h会漏掉元素顶部已经滚出屏幕、但底部还露在外面的情况如果你只比对left和screen_w又会漏掉横向列表的问题。做纵向列表的测试时至少要把 top 和 bottom 两个维度都检查到横向列表再额外加 left 和 right。后面第 3 节我会给出完整代码这里先把判断逻辑说清楚。3. 正确解法手写“视口内可见”判定函数3.1 通用核心代码坐标相交检测既然框架不给力我们就自己造轮子。一个可用的is_in_viewport函数需要做三件事拿到元素坐标、拿到元素尺寸、拿到屏幕尺寸然后做矩形相交判断。def is_in_viewport(driver, element, ratio0.0): 判断元素是否出现在屏幕可视区域内。 ratio0.0 表示只要出现任意像素就算可见 ratio0 表示可见面积占比需要达到该比例。 screen_w driver.get_window_size()[width] screen_h driver.get_window_size()[height] # Selenium/Appium 的 location 和 size 都是 dict loc element.location size element.size left, top loc[x], loc[y] right, bottom left size[width], top size[height] # 完全在屏幕外的四种情况 if bottom 0 or top screen_h or right 0 or left screen_w: return False # 需要计算可见面积比例 if ratio 0: visible_w min(right, screen_w) - max(left, 0) visible_h min(bottom, screen_h) - max(top, 0) if visible_w 0 or visible_h 0: return False total_area size[width] * size[height] visible_area visible_w * visible_h return visible_area / total_area ratio return True这个函数我在多个项目里用过横竖屏都适用。关键是它把“元素是否出现在视口”这个语义从框架手里接管了过来。这里有个细节要注意element.location在 Appium 的 Android 端是屏幕坐标但在某些 iOS 版本或者某些 Appium 版本里返回的可能是相对 App 窗口的坐标。如果你的 App 是沉浸式全屏这两者通常一致如果有状态栏、刘海屏的额外高度就可能出现几像素的偏差。我的建议是拿到坐标后先和driver.get_window_size()手动验一次。如果元素明明在屏幕可见位置而坐标还是负数说明坐标基准不对需要额外加上偏移量。3.2 兼容 iOS把 hittable / frame 一起用上iOS 上 XCUITest 的isVisible()语义不稳定坐标检测反而是
返回列表