Files
QaAutomationHub/tests/app/conftest.py
T
xst 7b4a54f1ce feat: 框架增加登录态复用机制,绕过图形验证码
Web 端 (storage_state):
- 新增 tests/web/save_auth.py: 手动登录后保存 cookies+localStorage
- conftest.py: skip_login:true 时自动加载 storage_state,跳过 UI 登录
- ensure_login fixture: 登录态有效则直接通过,过期则给出明确指引

App 端 (noReset):
- conftest.py: ensure_logged_in fixture,手动登录后无需重新登录
- noReset=true 持久化登录态,未登录自动检测并给出明确引导

配置文件:
- web.skip_login: true + storage_state_dir
- app.skip_login: true + 手动登录说明
2026-07-15 10:47:34 +08:00

199 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
App 端根级 fixtureAppium 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)