Appium 移动端自动化测试(四):高级元素定位技巧

本篇深入讲解移动端动态元素定位、平台特有的高级定位方法,以及处理复杂 UI 层级结构的技巧。

一、动态元素定位

1.1 常见动态元素场景

移动端动态元素常见模式:

  • 动态资源 IDcom.example:id/text_1738901234
  • 动态文本:列表项文本随数据变化
  • RecyclerView/CollectionView:元素动态加载和回收
  • 广告/弹窗:随机出现的覆盖层

1.2 应对策略

策略一:使用 Accessibility ID

# 稳定性最高的定位方式,不会因版本更新而变化
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "登录按钮")

建议:推动开发团队为关键元素添加 content-desc(Android)或 accessibilityIdentifier(iOS)。

策略二:XPath 模糊匹配

# Android:ID 包含关键词
driver.find_element(AppiumBy.XPATH,
    "//*[contains(@resource-id, 'login_btn')]"
)

# 文本包含关键词
driver.find_element(AppiumBy.XPATH,
    "//*[contains(@text, '登录')]"
)

# Class Name + 属性组合
driver.find_element(AppiumBy.XPATH,
    "//android.widget.Button[contains(@text, '提交')]"
)

# iOS:name 包含关键词
driver.find_element(AppiumBy.XPATH,
    "//*[contains(@name, 'Login')]"
)

策略三:Android UIAutomator 滚动查找

# 在可滚动容器中查找元素(非常适合列表)
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiScrollable(new UiSelector().scrollable(true))'
    '.scrollIntoView(new UiSelector().text("目标文本"))'
)

# 滚动到指定位置的元素
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiScrollable(new UiSelector().scrollable(true))'
    '.scrollIntoView(new UiSelector().resourceId("com.example:id/item_50"))'
)

# 最大滚动次数
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiScrollable(new UiSelector().scrollable(true))'
    '.setMaxSearchSwipes(10)'
    '.scrollIntoView(new UiSelector().text("加载更多"))'
)

# 水平滚动(横向列表)
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiScrollable(new UiSelector().scrollable(true))'
    '.setAsHorizontalList()'
    '.scrollIntoView(new UiSelector().text("更多"))'
)

策略四:通过兄弟/父子关系定位

# 找到文本为"用户名"的 TextView,然后定位旁边的输入框
driver.find_element(AppiumBy.XPATH,
    "//*[@text='用户名']/following-sibling::android.widget.EditText"
)

# 通过父元素定位
driver.find_element(AppiumBy.XPATH,
    "//*[@resource-id='com.example:id/form']//android.widget.EditText[1]"
)

# 通过祖父元素定位
driver.find_element(AppiumBy.XPATH,
    "//*[@resource-id='com.example:id/card']/*[@class='android.widget.LinearLayout']//android.widget.Button"
)

二、Android 高级定位

2.1 UIAutomator 复杂选择器

# 实例选择(第 N 个匹配元素)
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiSelector().className("android.widget.EditText").instance(0)'
)

# 属性组合
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiSelector()'
    '.className("android.widget.Button")'
    '.text("登录")'
    '.enabled(true)'
    '.clickable(true)'
)

# 正则匹配
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiSelector().textMatches(".*登录.*")'
)

# 从子元素向上查找
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiSelector().className("android.widget.EditText")'
    '.fromParent(new UiSelector().text("密码"))'
)

# 检查属性
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiSelector().checkable(true).checked(true)'
)

2.2 Android DataMatcher

# 使用 Espresso DataMatcher 定位(需要 Espresso 驱动)
driver.find_element(AppiumBy.ANDROID_DATA_MATCHER,
    '{"name": "hasEntry", "args": ["title", "Expected Title"]}'
)

2.3 Android ViewMatcher

# 使用 View Matcher
driver.find_element(AppiumBy.ANDROID_VIEW_MATCHER,
    'withText("Login")'
)

三、iOS 高级定位

3.1 iOS Predicate 高级用法

# 基本谓词
driver.find_element(AppiumBy.IOS_PREDICATE,
    "label == 'Login'"
)

# 多条件
driver.find_element(AppiumBy.IOS_PREDICATE,
    "type == 'XCUIElementTypeButton' AND label == 'Login'"
)

# 模糊匹配
driver.find_element(AppiumBy.IOS_PREDICATE,
    "label CONTAINS '登录'"
)

# 正则匹配
driver.find_element(AppiumBy.IOS_PREDICATE,
    "label MATCHES '.*登录.*'"
)

# 值为空
driver.find_element(AppiumBy.IOS_PREDICATE,
    "type == 'XCUIElementTypeTextField' AND value == ''"
)

# 可见性
driver.find_element(AppiumBy.IOS_PREDICATE,
    "type == 'XCUIElementTypeButton' AND visible == true"
)

3.2 iOS Class Chain 高级用法

# 基本查找
driver.find_element(AppiumBy.IOS_CLASS_CHAIN,
    "**/XCUIElementTypeButton[`label == 'Login'`]"
)

# 层级查找
driver.find_element(AppiumBy.IOS_CLASS_CHAIN,
    "**/XCUIElementTypeTable/**/XCUIElementTypeCell[`name == 'Settings'`]"
)

# 指定索引
driver.find_element(AppiumBy.IOS_CLASS_CHAIN,
    "**/XCUIElementTypeButton[`label == 'OK'`][1]"
)

# 组合条件
driver.find_element(AppiumBy.IOS_CLASS_CHAIN,
    "**/XCUIElementTypeTextField[`value == ''` AND visible == 1`]"
)

# 查找所有匹配元素
elements = driver.find_elements(AppiumBy.IOS_CLASS_CHAIN,
    "**/XCUIElementTypeCell"
)

3.3 iOS 深层链接查找

# 使用 Descendant(查找任意层级的后代)
driver.find_element(AppiumBy.IOS_CLASS_CHAIN,
    "XCUIElementTypeWindow/XCUIElementTypeOther/**/XCUIElementTypeButton[`label == 'Submit'`]"
)

四、WebView 元素定位

4.1 WebView 调试模式

Android:在 WebView 初始化时开启调试模式:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
}

iOS:在 scheme 中配置:

<key>NSAllowsArbitraryLoads</key>
<true/>

4.2 WebView 上下文切换

# 获取所有上下文
contexts = driver.contexts
# 示例: ['NATIVE_APP', 'WEBVIEW_com.example.app']

# 切换到 WebView
webview_context = [ctx for ctx in contexts if "WEBVIEW" in ctx][0]
driver.switch_to.context(webview_context)

# 在 WebView 中使用 Web 定位方式
driver.find_element(AppiumBy.CSS_SELECTOR, "#username").send_keys("test")
driver.find_element(AppiumBy.CSS_SELECTOR, ".submit-btn").click()
driver.find_element(AppiumBy.XPATH, "//button[text()='Submit']").click()

# 切回原生
driver.switch_to.context("NATIVE_APP")

4.3 WebView 调试技巧

# 获取 WebView 控制台日志
logs = driver.get_log("webdriver")

# 执行 JavaScript
result = driver.execute_script("return document.title")

五、列表元素定位

5.1 RecyclerView 定位

# 获取所有列表项
items = driver.find_elements(
    AppiumBy.ID, "com.example:id/item_container"
)

# 点击指定文本的列表项
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiSelector().text("目标文本")'
).click()

# 滚动到指定项并点击
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiScrollable(new UiSelector().resourceId("com.example:id/recycler_view"))'
    '.scrollIntoView(new UiSelector().text("目标项"))'
).click()

5.2 通用列表定位封装

def find_list_item_by_text(driver, text, max_scrolls=5):
    """通过滚动查找列表中的文本项"""
    from appium.webdriver.common.touch_action import TouchAction

    for _ in range(max_scrolls):
        try:
            item = driver.find_element(
                AppiumBy.ANDROID_UIAUTOMATOR,
                f'new UiSelector().text("{text}")'
            )
            if item.is_displayed():
                return item
        except:
            pass

        # 向上滑动
        size = driver.get_window_size()
        TouchAction(driver) \
            .press(x=size["width"]//2, y=int(size["height"]*0.8)) \
            .wait(ms=300) \
            .move_to(x=size["width"]//2, y=int(size["height"]*0.2)) \
            .release() \
            .perform()

    raise Exception(f"未找到列表项: {text}")

六、定位策略决策树

需要跨平台?
  ├── 是 → 使用 Accessibility ID
  └── 否 → 仅 Android?
              ├── 是 → 有固定 ID?
              │         ├── 是 → AppiumBy.ID
              │         └── 否 → 需要滚动查找?
              │                   ├── 是 → ANDROID_UIAUTOMATOR + UiScrollable
              │                   └── 否 → XPath / UIAutomator
              └── 仅 iOS → iOS Predicate / Class Chain

七、总结

场景 Android 推荐方法 iOS 推荐方法
跨平台 ACCESSIBILITY_ID ACCESSIBILITY_ID
固定 ID AppiumBy.ID AppiumBy.ID
滚动查找 ANDROID_UIAUTOMATOR + UiScrollable 手势滑动 + 查找
动态文本 XPath contains Predicate CONTAINS
列表项 UIAutomator / XPath Class Chain
WebView CSS_SELECTOR / XPath CSS_SELECTOR / XPath

下一篇:等待机制与脚本优化,提升移动端测试的稳定性。

posted @ 2026-04-07 16:09  小小阿狸。  阅读(1)  评论(0)    收藏  举报