Appium 移动端自动化测试(六):处理特殊情况

本篇讲解移动端测试中常见的棘手场景:系统弹窗、权限弹窗、Hybrid App 多上下文切换、以及通知栏和系统对话框的处理。

一、系统弹窗处理

1.1 Android 权限弹窗

from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def handle_android_permission(driver, action="allow"):
    """
    处理 Android 权限弹窗
    :param action: "allow" 允许, "deny" 拒绝
    """
    try:
        if action == "allow":
            # 尝试多种"允许"按钮定位方式
            allow_buttons = [
                (AppiumBy.ID, "com.android.packageinstaller:id/permission_allow_button"),
                (AppiumBy.ID, "android:id/button1"),
                (AppiumBy.XPATH, "//*[@text='允许']"),
                (AppiumBy.XPATH, "//*[@text='ALLOW']"),
                (AppiumBy.XPATH, "//*[@text='While using the app']"),
            ]
            for locator in allow_buttons:
                try:
                    WebDriverWait(driver, 2).until(
                        EC.element_to_be_clickable(locator)
                    ).click()
                    return True
                except:
                    continue
        else:
            deny_buttons = [
                (AppiumBy.ID, "com.android.packageinstaller:id/permission_deny_button"),
                (AppiumBy.ID, "android:id/button2"),
                (AppiumBy.XPATH, "//*[@text='拒绝']"),
                (AppiumBy.XPATH, "//*[@text='DENY']"),
            ]
            for locator in deny_buttons:
                try:
                    WebDriverWait(driver, 2).until(
                        EC.element_to_be_clickable(locator)
                    ).click()
                    return True
                except:
                    continue
    except:
        pass
    return False

1.2 自动处理所有权限

def auto_handle_all_permissions(driver, max_permissions=5):
    """自动处理所有权限弹窗(全部允许)"""
    for _ in range(max_permissions):
        handled = handle_android_permission(driver, "allow")
        if not handled:
            break

1.3 通过 Capabilities 自动授予权限

# 方法一:通过 autoGrantPermissions
capabilities = {
    "appium:autoGrantPermissions": True,  # 自动授予所有权限
}

# 方法二:通过 adb 提前授权
import subprocess

def grant_permissions(package_name):
    """通过 adb 提前授予所有运行时权限"""
    permissions = [
        "android.permission.CAMERA",
        "android.permission.ACCESS_FINE_LOCATION",
        "android.permission.READ_CONTACTS",
        "android.permission.WRITE_EXTERNAL_STORAGE",
        "android.permission.READ_EXTERNAL_STORAGE",
        "android.permission.RECORD_AUDIO",
    ]
    for perm in permissions:
        subprocess.run(
            ["adb", "shell", "pm", "grant", package_name, perm],
            capture_output=True
        )

1.4 iOS 系统弹窗

def handle_ios_alert(driver, action="accept"):
    """
    处理 iOS 系统弹窗
    :param action: "accept" 接受, "decline" 拒绝
    """
    try:
        alert = WebDriverWait(driver, 3).until(
            EC.alert_is_present()
        )
        if action == "accept":
            alert.accept()
        else:
            alert.dismiss()
        return True
    except:
        return False

二、应用内弹窗处理

2.1 通用弹窗处理器

class AppDialogHandler:
    """应用内弹窗处理器"""

    def __init__(self, driver):
        self.driver = driver

    def close_popups(self):
        """尝试关闭所有已知的弹窗"""
        close_strategies = [
            # 通用关闭按钮
            (AppiumBy.ACCESSIBILITY_ID, "Close"),
            (AppiumBy.ACCESSIBILITY_ID, "关闭"),
            (AppiumBy.ID, "com.example:id/close_btn"),
            (AppiumBy.ID, "com.example:id/iv_close"),
            # 跳过按钮
            (AppiumBy.XPATH, "//*[@text='跳过']"),
            (AppiumBy.XPATH, "//*[@text='Skip']"),
            (AppiumBy.XPATH, "//*[@text='不再提示']"),
            (AppiumBy.XPATH, "//*[@text="Don't show again"]"),
            # 确认按钮
            (AppiumBy.XPATH, "//*[@text='确定']"),
            (AppiumBy.XPATH, "//*[@text='OK']"),
            (AppiumBy.XPATH, "//*[@text='我知道了']"),
            # 点击弹窗外区域关闭
        ]

        closed = False
        for locator in close_strategies:
            try:
                element = self.driver.find_element(*locator)
                if element.is_displayed():
                    element.click()
                    closed = True
                    break
            except:
                continue

        return closed

    def dismiss_ad(self):
        """关闭广告弹窗"""
        ad_close_locators = [
            (AppiumBy.ID, "com.example:id/ad_close"),
            (AppiumBy.XPATH, "//*[@text='关闭广告']"),
            (AppiumBy.XPATH, "//*[@content-desc='close ad']"),
            (AppiumBy.XPATH, "//*[@resource-id='com.example:id/iv_ad_close']"),
        ]
        for locator in ad_close_locators:
            try:
                element = self.driver.find_element(*locator)
                if element.is_displayed():
                    element.click()
                    return True
            except:
                continue
        return False

    def wait_and_dismiss(self, timeout=3):
        """等待弹窗出现并关闭"""
        import time
        start = time.time()
        while time.time() - start < timeout:
            if self.close_popups():
                time.sleep(0.5)  # 等待关闭动画
            else:
                break

2.2 首次启动引导页

def skip_onboarding(driver):
    """跳过首次启动引导页"""
    from appium.webdriver.common.appiumby import AppiumBy

    skip_locators = [
        (AppiumBy.XPATH, "//*[@text='跳过']"),
        (AppiumBy.XPATH, "//*[@text='Skip']"),
        (AppiumBy.ACCESSIBILITY_ID, "Skip"),
        (AppiumBy.ID, "com.example:id/skip_btn"),
    ]

    for locator in skip_locators:
        try:
            element = driver.find_element(*locator)
            if element.is_displayed():
                element.click()
                return True
        except:
            continue

    # 尝试多次滑动跳过
    from appium.webdriver.common.touch_action import TouchAction
    size = driver.get_window_size()
    for _ in range(5):
        try:
            driver.find_element(*skip_locators[0]).click()
            return True
        except:
            TouchAction(driver) \
                .press(x=size["width"]*0.9, y=size["height"]//2) \
                .wait(ms=300) \
                .move_to(x=size["width"]*0.1, y=size["height"]//2) \
                .release() \
                .perform()
    return False

三、Hybrid App 多上下文处理

3.1 什么是 Hybrid App?

Hybrid App 是原生容器 + WebView 的混合应用。测试时需要在原生上下文(NATIVE_APP)和 WebView 上下文之间切换。

3.2 上下文管理

class HybridAppHandler:
    """Hybrid App 上下文管理器"""

    def __init__(self, driver):
        self.driver = driver

    def get_all_contexts(self):
        """获取所有可用上下文"""
        return self.driver.contexts

    def get_current_context(self):
        """获取当前上下文"""
        return self.driver.context

    def get_webview_contexts(self):
        """获取所有 WebView 上下文"""
        return [ctx for ctx in self.driver.contexts if "WEBVIEW" in ctx]

    def switch_to_native(self):
        """切换到原生上下文"""
        self.driver.switch_to.context("NATIVE_APP")

    def switch_to_webview(self, index=0):
        """切换到 WebView 上下文"""
        webviews = self.get_webview_contexts()
        if webviews:
            self.driver.switch_to.context(webviews[index])
            return True
        return False

    def switch_to_webview_by_name(self, name):
        """按名称切换到 WebView"""
        for ctx in self.driver.contexts:
            if name in ctx:
                self.driver.switch_to.context(ctx)
                return True
        return False

    def execute_in_webview(self, script):
        """在 WebView 中执行 JavaScript"""
        original_context = self.driver.context
        try:
            self.switch_to_webview()
            return self.driver.execute_script(script)
        finally:
            self.driver.switch_to.context(original_context)

3.3 Hybrid App 测试示例

def test_hybrid_app_login(driver):
    """Hybrid App 登录测试"""
    handler = HybridAppHandler(driver)

    # 1. 在原生界面输入凭据
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "usernameInput").send_keys("admin")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "passwordInput").send_keys("123456")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "loginButton").click()

    # 2. 等待 WebView 加载
    import time
    time.sleep(3)

    # 3. 切换到 WebView
    handler.switch_to_webview()

    # 4. 在 WebView 中验证登录结果
    welcome = driver.find_element(AppiumBy.CSS_SELECTOR, ".welcome-message")
    assert "欢迎" in welcome.text

    # 5. 在 WebView 中操作
    driver.find_element(AppiumBy.CSS_SELECTOR, "#settings-link").click()

    # 6. 切回原生
    handler.switch_to_native()

3.4 WebView 等待加载

def wait_for_webview(driver, timeout=15):
    """等待 WebView 上下文可用"""
    from selenium.webdriver.support.ui import WebDriverWait
    return WebDriverWait(driver, timeout).until(
        lambda d: any("WEBVIEW" in ctx for ctx in d.contexts)
    )

# 使用
wait_for_webview(driver)
handler = HybridAppHandler(driver)
handler.switch_to_webview()

四、通知栏处理

4.1 打开和操作通知栏

def test_push_notification(driver):
    """测试推送通知"""

    # 1. 打开通知栏
    driver.open_notifications()

    # 2. 查找通知
    from appium.webdriver.common.appiumby import AppiumBy
    try:
        notification = driver.find_element(
            AppiumBy.XPATH,
            "//*[@text='新消息']"
        )
        notification.click()
    except:
        # 如果没有找到通知,关闭通知栏
        driver.press_keycode(4)  # 返回键

    # 3. 验证通知跳转
    # ...

4.2 通知栏工具封装

class NotificationHandler:
    """通知栏处理器"""

    def __init__(self, driver):
        self.driver = driver

    def open(self):
        """打开通知栏"""
        self.driver.open_notifications()

    def close(self):
        """关闭通知栏"""
        # Android: 按返回键
        self.driver.press_keycode(4)
        # 或向下滑动关闭
        from appium.webdriver.common.touch_action import TouchAction
        size = self.driver.get_window_size()
        TouchAction(self.driver) \
            .press(x=size["width"]//2, y=0) \
            .move_to(x=size["width"]//2, y=size["height"]) \
            .release() \
            .perform()

    def find_notification(self, text):
        """查找包含指定文本的通知"""
        try:
            return self.driver.find_element(
                AppiumBy.XPATH, f"//*[@text='{text}']"
            )
        except:
            return None

    def clear_all(self):
        """清除所有通知"""
        try:
            clear_btn = self.driver.find_element(
                AppiumBy.ACCESSIBILITY_ID, "Clear all notifications"
            )
            clear_btn.click()
        except:
            pass

五、系统对话框处理

5.1 Android 系统对话框

# 处理 "应用未响应" 对话框
def handle_anr_dialog(driver):
    try:
        wait_btn = driver.find_element(
            AppiumBy.ID, "android:id/button1"
        )
        if wait_btn.is_displayed():
            wait_btn.click()  # 点击"等待"
    except:
        pass

# 处理 "应用已停止" 对话框
def handle_crash_dialog(driver):
    try:
        ok_btn = driver.find_element(
            AppiumBy.ID, "android:id/aerr_close"
        )
        if ok_btn.is_displayed():
            ok_btn.click()
    except:
        pass

5.2 iOS 系统弹窗

def handle_ios_system_alert(driver):
    """处理 iOS 系统弹窗(需要 XCUITest)"""
    try:
        # 使用 XCUITest 处理系统弹窗
        driver.execute_script("mobile: alert", {"action": "accept"})
    except:
        pass

六、总结

场景 解决方案
权限弹窗 autoGrantPermissions / adb 提前授权 / 自动点击
应用弹窗 通用弹窗处理器 + 重试
引导页 查找跳过按钮 / 滑动跳过
Hybrid App 上下文切换(NATIVE_APP ↔ WEBVIEW)
通知栏 open_notifications + 元素操作
系统对话框 按钮定位 / mobile: alert

核心原则:移动端弹窗处理要有防御性思维,不要假设弹窗一定出现或一定不出现。

下一篇:Appium 高级特性——复杂手势操作、设备操作和平台特定功能。

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