refactor: 优化CoreDriver

- 新增 核心操作函数
- 新增 全局变量 EXPLICIT_WAIT_TIMEOUT
This commit is contained in:
2026-01-21 17:29:41 +08:00
parent e59ffa36d3
commit 1bcad0d166
2 changed files with 128 additions and 32 deletions

View File

@@ -11,7 +11,8 @@
""" """
import logging import logging
from enum import Enum from enum import Enum
from typing import Optional, Type from typing import Optional, Type, TypeVar
from time import sleep
from appium import webdriver from appium import webdriver
from appium.options.android import UiAutomator2Options from appium.options.android import UiAutomator2Options
@@ -20,20 +21,28 @@ from appium.options.common.base import AppiumOptions
from appium.webdriver.webdriver import ExtensionBase from appium.webdriver.webdriver import ExtensionBase
from appium.webdriver.client_config import AppiumClientConfig from appium.webdriver.client_config import AppiumClientConfig
from appium.webdriver.common.appiumby import AppiumBy from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.common.bidi.cdp import session_context
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput
from utils.finder import by_converter from utils.finder import by_converter
from settings import IMPLICIT_WAIT_TIMEOUT, EXPLICIT_WAIT_TIMEOUT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = TypeVar("T")
class AppPlatform(Enum): class AppPlatform(Enum):
ANDROID = "android" ANDROID = "android"
IOS = "ios" IOS = "ios"
class CoreDriver: class CoreDriver:
def __init__(self): def __init__(self):
self.driver: Optional[webdriver.Remote] = None self.driver: Optional[webdriver.Remote] = None
@@ -95,37 +104,114 @@ class CoreDriver:
self.driver = None self.driver = None
raise ConnectionError(f"无法连接到 Appium 服务,请检查端口 {self._port} 或设备状态。") from e raise ConnectionError(f"无法连接到 Appium 服务,请检查端口 {self._port} 或设备状态。") from e
# --- 核心操作 --- # --- 核心操作 ---
def find(self, by, value, timeout=10): def find_element(self, by, value, timeout=10):
"""内部通用查找(显式等待)""" """内部通用查找(显式等待)"""
by = by_converter(by) by = by_converter(by)
target = (by, value) mark = (by, value)
# self.driver.find_element()
return WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located(target)) return WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located(mark))
def delay(self, timeout: int | float):
sleep(timeout)
return self
def implicit_wait(self, timeout: float = IMPLICIT_WAIT_TIMEOUT, *args, **kwargs) -> None:
"""
隐式等待
:param timeout: 超时时间
:param args:
:param kwargs:
:return:
"""
self.driver.implicitly_wait(timeout)
def explicit_wait(self, method: T, timeout: float = EXPLICIT_WAIT_TIMEOUT, *args, **kwargs):
"""
显示等待
:param method: 可调用对象名
:param timeout: 超时时间
:param args:
:param kwargs:
:return:
"""
try:
if isinstance(method, str):
# method = custom_ec.get(method, (lambda _: False))
method = lambda _: False
logger.info(f"预期条件: {method.__name__}")
return WebDriverWait(self.driver, timeout).until(method)
except TypeError as te:
logger.error(f"显示等待异常: {te}")
# self.driver.quit()
raise te
def page_load_timeout(self, timeout: float, *args, **kwargs) -> None:
self.driver.set_page_load_timeout(timeout)
def click(self, by, value, timeout=10) -> 'CoreDriver': def click(self, by, value, timeout=10) -> 'CoreDriver':
target = (by_converter(by), value) by = by_converter(by)
logger.info(f"点击: {target}") mark = (by, value)
WebDriverWait(self.driver, timeout).until(EC.element_to_be_clickable(target)).click() logger.info(f"点击: {mark}")
method = EC.element_to_be_clickable(mark)
self.explicit_wait(method, timeout).click()
return self return self
def clear(self, by, value, *args, **kwargs):
by = by_converter(by)
mark = (by, value)
method = EC.visibility_of_element_located(mark)
self.explicit_wait(method).clear()
# self.driver.find_element(by, value).clear()
def input(self, by, value, text, timeout=10) -> 'CoreDriver': def input(self, by, value, text, timeout=10) -> 'CoreDriver':
target = (by_converter(by), value) by = by_converter(by)
logger.info(f"输入 '{text}' 到: {target}") mark = (by, value)
el = WebDriverWait(self.driver, timeout).until(EC.visibility_of_element_located(target)) method = EC.visibility_of_element_located(mark)
el.clear()
el.send_keys(text) self.explicit_wait(method, timeout).send_keys(text)
return self return self
# --- 移动端特有:方向滑动 --- def get_text(self, by, value, *args, **kwargs):
def swipe_to(self, direction: str = "up", duration: int = 800) -> 'CoreDriver': """
"""封装方向滑动,无需计算具体坐标""" 获取元素文本
if not self._size: :param by:
self._size = self.driver.get_window_size() :param value:
:param args:
:param kwargs:
:return:
"""
by = by_converter(by)
mark = (by, value)
method = EC.visibility_of_element_located(mark)
w, h = self._size['width'], self._size['height'] text = self.explicit_wait(method).text
# 这里的 0.8/0.2 比例是为了避开刘海屏和虚拟按键,提高滑动成功率 logger.info(f"获取到的文本{text}")
return text
def get_session_id(self):
return self.driver.session_id
# --- 移动端特有:方向滑动 ---
def swipe_to(self, direction: str = "up", duration: int = 1000) -> 'CoreDriver':
"""
封装方向滑动 (W3C Actions 兼容版)
:param direction: 滑动方向 up/down/left/right
:param duration: 滑动持续时间 (ms)
:return: self
"""
# 每次获取屏幕尺寸以适应旋转
size = self.driver.get_window_size()
w, h = size['width'], size['height']
# 定义滑动坐标 (避开边缘区域)
coords = { coords = {
"up": (w * 0.5, h * 0.8, w * 0.5, h * 0.2), "up": (w * 0.5, h * 0.8, w * 0.5, h * 0.2),
"down": (w * 0.5, h * 0.2, w * 0.5, h * 0.8), "down": (w * 0.5, h * 0.2, w * 0.5, h * 0.8),
@@ -133,18 +219,29 @@ class CoreDriver:
"right": (w * 0.1, h * 0.5, w * 0.9, h * 0.5) "right": (w * 0.1, h * 0.5, w * 0.9, h * 0.5)
} }
start_x, start_y, end_x, end_y = coords.get(direction.lower(), coords["up"]) start_x, start_y, end_x, end_y = coords.get(direction.lower(), coords["up"])
self.driver.swipe(start_x, start_y, end_x, end_y, duration) logger.info(f"执行滑动: {direction} ({start_x}, {start_y}) -> ({end_x}, {end_y})")
# 使用 W3C ActionChains 替代已废弃的 driver.swipe
actions = ActionChains(self.driver)
# 覆盖默认的鼠标输入为触摸输入
actions.w3c_actions = ActionBuilder(self.driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch"))
actions.w3c_actions.pointer_action.move_to_location(start_x, start_y)
actions.w3c_actions.pointer_action.pointer_down()
actions.w3c_actions.pointer_action.pause(duration / 1000) # pause 单位为秒
actions.w3c_actions.pointer_action.move_to_location(end_x, end_y)
actions.w3c_actions.pointer_action.release()
actions.perform()
return self return self
# --- 断言逻辑 --- # --- 断言逻辑 ---
def assert_text(self, by, value, expected_text) -> 'CoreDriver': def assert_text(self, by, value, expected_text) -> 'CoreDriver':
actual = self.find(by, value).text actual = self.get_text(by, value)
assert actual == expected_text, f"断言失败: 期望 {expected_text}, 实际 {actual}" assert actual == expected_text, f"断言失败: 期望 {expected_text}, 实际 {actual}"
logger.info(f"断言通过: 文本匹配 '{actual}'") logger.info(f"断言通过: 文本匹配 '{actual}'")
return self return self
def quit(self): def quit(self):
"""安全退出""" """安全退出"""
if self.driver: if self.driver:

View File

@@ -31,10 +31,9 @@ for folder in [LOG_DIR, LOG_BACKUP_DIR, ALLURE_TEMP]:
LOG_SOURCE = LOG_DIR / "pytest.log" LOG_SOURCE = LOG_DIR / "pytest.log"
# --- 业务常量 (可选) --- # --- 业务常量 (可选) ---
IMPLICIT_WAIT = 10 IMPLICIT_WAIT_TIMEOUT = 10
EXPLICIT_WAIT_TIMEOUT = 10
APPIUM_SERVER = "http://127.0.0.1:4723" APPIUM_SERVER = "http://127.0.0.1:4723"
# --- 核心配置 --- # --- 核心配置 ---
APPIUM_HOST = "127.0.0.1" APPIUM_HOST = "127.0.0.1"
APPIUM_PORT = 4723 APPIUM_PORT = 4723