""" App 端根级 fixture:Appium Driver 实例 + 登录态复用 + 失败自动截图。 核心机制 — 手动登录后复用: Appium noReset=true 确保 app 数据不重置,登录态持久保留在设备上。 只需手动在设备上登录一次,后续用例通过 ensure_logged_in 检查首页是否已登录态。 如果检测到未登录(在登录页),给出明确指引让用户先去设备上手动登录。 配置 app.skip_login: true 时: - 用例通过 ensure_logged_in() 检查是否已登录 - 未登录则抛出明确错误,引导用户先去设备上手动登录 - 已登录则直接开始执行 配置 app.skip_login: false 时: - 每次用例尝试完整 UI 登录(有验证码时会失败) 提供: app_config — session 级 App 配置 driver — Appium WebDriver(每个用例独立,失败自动截图) platform — 当前平台 """ from datetime import datetime from pathlib import Path import pytest SCREENSHOTS_DIR = Path("output/screenshots") @pytest.fixture(scope="session") def app_config(config): """session 级:App 配置段。""" return config["app"] @pytest.fixture def platform(app_config) -> str: """当前执行的平台。""" return app_config.get("platforms", ["android"])[0] @pytest.fixture def driver(app_config, platform, request): """每个用例独立的 Appium driver,失败自动截图。 前提: Appium Server 已启动(appium --log-level info)。 noReset=true → app 不重置 → 登录态持久保留。 """ try: from appium import webdriver from appium.options.android import UiAutomator2Options from appium.options.ios import XCUITestOptions except ImportError: pytest.skip("Appium-Python-Client 未安装,跳过 App 测试。") appium_host = app_config.get("appium_host", "http://localhost:4723") screenshot_on_failure = app_config.get("screenshot_on_failure", True) driver = None try: if platform == "android": android_cfg = app_config["android"] options = UiAutomator2Options() options.device_name = android_cfg["device_name"] options.app_package = android_cfg["app_package"] options.app_activity = android_cfg["app_activity"] options.no_reset = android_cfg.get("no_reset", True) options.new_command_timeout = android_cfg.get("new_command_timeout", 120) options.automation_name = "UiAutomator2" else: ios_cfg = app_config["ios"] options = XCUITestOptions() options.device_name = ios_cfg["device_name"] options.bundle_id = ios_cfg["bundle_id"] options.no_reset = ios_cfg.get("no_reset", True) options.new_command_timeout = ios_cfg.get("new_command_timeout", 120) options.automation_name = "XCUITest" driver = webdriver.Remote(appium_host, options=options) yield driver # ── 失败自动截图 ── if ( screenshot_on_failure and hasattr(request.node, "rep_call") and request.node.rep_call.failed and driver is not None ): scenario_dir = SCREENSHOTS_DIR / request.node.name scenario_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") path = str(scenario_dir / f"FAIL_{platform}_{ts}.png") driver.save_screenshot(path) finally: if driver is not None: driver.quit() @pytest.fixture def ensure_logged_in(driver, platform, app_config, config): """确保 APP 已处于登录状态。 使用方式: def test_xxx(driver, ensure_logged_in): ensure_logged_in() # 检查登录态,未登录则报错 # 后面直接写业务断言 """ def _ensure_logged_in(timeout_s: float = 10.0) -> None: skip_login = app_config.get("skip_login", True) from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from appium.webdriver.common.appiumby import AppiumBy # ⚠️ home_tab 的 accessibility_id 需根据实际 APP UI 确认 HOME_INDICATOR_ID = "tab_home" # 首页 tab 的 accessibility_id wait = WebDriverWait(driver, timeout=timeout_s) try: wait.until( EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, HOME_INDICATOR_ID)) ) # 已登录,无需操作 return except Exception: pass # ── 未检测到首页元素,判断是否在登录页 ── try: # 尝试找登录页元素 login_input = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_phone_input") if login_input.is_displayed(): if skip_login: raise RuntimeError( "APP 当前在登录页,未检测到登录态!\n" "请在设备上手动完成一次登录(包含验证码),之后重新运行用例即可。\n" "如需通过自动化登录,请设置 app.skip_login: false(不推荐,有验证码)。" ) # skip_login: false — 走自动化登录 account = config["accounts"].get("driver") if not account: raise RuntimeError("配置中未找到 driver 账号") login_input.send_keys(account["phone"]) driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_password_input").send_keys( account["password"] ) driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_submit_btn").click() # 等待登录完成 wait.until( EC.visibility_of_element_located( (AppiumBy.ACCESSIBILITY_ID, HOME_INDICATOR_ID) ) ) return except RuntimeError: raise # 重新抛出明确的错误提示 except Exception: pass raise RuntimeError( f"无法确定 APP 的登录状态!请确保 APP 已启动且首页或登录页可见。\n" f"当前平台: {platform}" ) return _ensure_logged_in @pytest.fixture def is_logged_in(driver, platform, app_config) -> bool: """快速检查当前是否已登录(不执行登录操作)。""" from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from appium.webdriver.common.appiumby import AppiumBy try: wait = WebDriverWait(driver, timeout=5) wait.until( EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "tab_home")) ) return True except Exception: return False # ── pytest 报告 hook ── @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """记录用例执行结果,供失败截图使用。""" outcome = yield rep = outcome.get_result() setattr(item, f"rep_{rep.when}", rep)