feat(driver): 新增CoreDriver基础操作,更新文档

- 优化 is_visible,支持快速状态检查。
- 新增 wait_until_visible/wait_until_not_visible 支持元素状态检查。
- 新增 clear_popups 支持弹窗清理。
- 优化 implicit_wait 状态追踪,确保等待时间恢复的准确性。
- 更新 README.md
- 其他优化
[clear_popups 采用“非阻塞探测 + 阻塞确认”策略,大幅提升清理效率并减少无效等待]
This commit is contained in:
2026-01-29 18:15:48 +08:00
parent f1d1a5d35f
commit d3f9326baa
7 changed files with 312 additions and 130 deletions

View File

@@ -1,2 +1,52 @@
# AppAutoTest # AppAutoTest
设备能力配置 (Capabilities)
```python
ANDROID_CAPS = {
"platformName": "Android",
"automationName": "uiautomator2",
"deviceName": "Android",
"appPackage": "com.android.settings",
"appActivity": ".Settings",
"noReset": False
}
```
| 字段名称 | 字段含义解释 | 示例值说明 |
|----------------|---------------------------------------------|---------------------------------------------------------------------------------------|
| platformName | 测试的平台/操作系统。这是必填项告诉自动化框架如Appium目标是什么系统。 | "Android" 表示这是一个Android设备。如果是iOS设备则为 "iOS"。 |
| automationName | 使用的自动化驱动引擎。指定底层用哪个工具来驱动设备进行UI交互。 | "uiautomator2" 是当前主流的Android驱动框架比旧的 "UiAutomator" 更稳定和高效。 |
| deviceName | 设备标识/名称。用于在同时连接多台设备时指定目标。 | "Android" 是一个通用标识。在实际测试中,通常用 adb devices 获取的真实设备序列号(如 emulator-5554来替换它以确保连接到正确的设备。 |
| appPackage | 要测试的应用程序包名。这是应用的唯一标识就像它在Android系统中的“身份证号”。 | "com.android.settings" 是Android系统“设置”应用的包名。测试你自己的应用时需替换为你应用的包名。 |
| appActivity | 要启动的应用内具体页面。它指定了应用启动后打开的第一个界面Activity。 | ".Settings" 是“设置”应用的主界面。前面的点. 表示它是 appPackage 下的一个相对路径。 |
获取应用的 appPackage 有几种常用方法
```shell
adb shell pm list packages | findstr your_package_name
# adb shell pm list packages -3 仅列出用户安装的第三方应用的包名
```
获取应用的 appActivity 有几种常用方法
```shell
# 1使用 aapt 工具分析 APK 文件(需有安装包)/build-tools/{version}/aapt
aapt dump badging your_app.apk | findstr launchable-activity
# 输出结果launchable-activity: name='com.example.myapp.MainActivity'
# name= 后面的值 'com.example.myapp.MainActivity' 就是你需要的主 Activity
# 2通过 ADB 命令获取(需应用已安装)
adb shell dumpsys window | findstr mCurrentFocus
# 输出结果mCurrentFocus=Window{... u0 com.example.myapp/com.example.myapp.MainActivity}
# / 后面的部分 com.example.myapp.MainActivity 就是当前 Activity
```
常用补充字段:
noResetTrue/False。是否在会话开始前重置应用状态例如清除应用数据。设置为 True 可以避免每次测试都重新登录。
platformVersion指定设备的Android系统版本如 "11.0")。虽然不是必须,但指定后能增强兼容性。
unicodeKeyboard 和 resetKeyboard用于处理中文输入等特殊字符输入。
newCommandTimeout设置Appium服务器等待客户端发送新命令的超时时间默认为60秒。在长时间操作中可能需要增加。

View File

@@ -38,12 +38,20 @@ def driver(app_server):
""" """
# 实例化你提供的类结构 # 实例化你提供的类结构
app_helper = CoreDriver() app_helper = CoreDriver()
caps = {
"platformName": "Android",
"automationName": "uiautomator2",
"deviceName": "Android",
"appPackage": "com.bocionline.ibmp",
"appActivity": "com.bocionline.ibmp.app.main.launcher.LauncherActivity",
"noReset": False, # 不清除应用数据
"newCommandTimeout": 60
}
# 连接并获取原生 driver 实例 # 连接并获取原生 driver 实例
# 这里可以根据需要扩展,比如通过命令行参数选择平台 # 这里可以根据需要扩展,比如通过命令行参数选择平台
app_helper.connect(platform="android", caps=ANDROID_CAPS) app_helper.connect(platform="android", caps=caps)
yield app_helper yield app_helper.driver
# 用例结束,只关 session不关 server # 用例结束,只关 session不关 server
app_helper.quit() app_helper.quit()

View File

@@ -10,36 +10,53 @@
@desc: @desc:
""" """
import logging import logging
from typing import Type, TypeVar import secrets
from typing import Type, TypeVar, List, Tuple
from appium import webdriver
from selenium.common import TimeoutException
from core.driver import CoreDriver from core.driver import CoreDriver
# 定义一个泛型用于类型推断IDE 依然会有补全提示) # 定义一个泛型用于类型推断IDE 依然会有补全提示)
T = TypeVar('T', bound='BasePage') T = TypeVar('T', bound='BasePage')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BasePage:
def __init__(self, driver: CoreDriver):
self.driver = driver
# 这里放全局通用的 Page 属性和逻辑
# --- 页面工厂:属性懒加载 ---
# 这样你可以在任何页面直接通过 self.home_page 访问首页
@property
def home_page(self):
from page_objects.home_page import HomePage
return HomePage(self.driver)
@property
def login_page(self): class BasePage(CoreDriver):
from page_objects.login_page import LoginPage
return LoginPage(self.driver) def __init__(self, driver: webdriver.Remote):
super().__init__(driver)
# 定义常见弹窗的关闭按钮定位
# 这里放全局通用的 Page 属性和逻辑
# 封装一些所有页面通用的元动作 # 封装一些所有页面通用的元动作
def get_toast(self, text): def clear_permission_popups(self):
return self.driver.is_visible("text", text) # 普适性黑名单
_black_list = [
("id", "com.android.packageinstaller:id/permission_allow_button"),
("xpath", "//*[@text='始终允许']"),
("xpath", "//*[@text='稍后提醒']"),
("xpath", "//*[@text='以后再说']"),
("id", "com.app:id/iv_close_global_ad"),
("accessibility id", "Close"), # iOS 常用
]
self.clear_popups(_black_list)
def to_page(self, page_class: Type[T]) -> T: def clear_business_ads(self):
"""在这里定义一些全 App 通用的业务广告清理"""
_ads = [("id", "com.app:id/global_ad_close")]
return self.clear_popups(_ads)
def get_toast(self, text):
return self.is_visible("text", text)
def go_to(self, page_name: Type[T]) -> T:
""" """
通用的页面跳转/获取方法 通用的页面跳转/获取方法
:param page_class: 目标页面类 :param page_name: 目标页面类
:return: 目标页面的实例 :return: 目标页面的实例
""" """
logger.info(f"跳转到页面: {page_class.__name__}") logger.info(f"跳转到页面: {page_name.__name__}")
return page_class(self.driver) return page_name(self.driver)

View File

@@ -10,6 +10,7 @@
@desc: Appium 核心驱动封装,提供统一的 API 用于 Appium 会话管理和元素操作。 @desc: Appium 核心驱动封装,提供统一的 API 用于 Appium 会话管理和元素操作。
""" """
import logging import logging
import secrets # 原生库,用于生成安全的随机数
from typing import Optional, Type, TypeVar, Union, Callable from typing import Optional, Type, TypeVar, Union, Callable
from time import sleep from time import sleep
@@ -21,7 +22,7 @@ from appium.webdriver.webdriver import ExtensionBase
from appium.webdriver.webelement import WebElement from appium.webdriver.webelement import WebElement
from appium.webdriver.client_config import AppiumClientConfig from appium.webdriver.client_config import AppiumClientConfig
from selenium.common import TimeoutException from selenium.common import TimeoutException, StaleElementReferenceException, NoSuchElementException
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.action_chains import ActionChains
@@ -32,7 +33,7 @@ from selenium.webdriver.common.actions.pointer_input import PointerInput
from utils.finder import by_converter from utils.finder import by_converter
from utils.decorators import resolve_wait_method from utils.decorators import resolve_wait_method
from core.modules import AppPlatform from core.modules import AppPlatform
from core.settings import IMPLICIT_WAIT_TIMEOUT, EXPLICIT_WAIT_TIMEOUT, APPIUM_HOST, APPIUM_PORT from core.settings import IMPLICIT_WAIT_TIMEOUT, EXPLICIT_WAIT_TIMEOUT, APPIUM_HOST, APPIUM_PORT, SCREENSHOT_DIR
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,21 +41,58 @@ T = TypeVar("T")
class CoreDriver: class CoreDriver:
def __init__(self): def __init__(self, driver: Optional[webdriver.Remote] = None):
""" """
初始化 CoreDriver 实例。 初始化 CoreDriver 实例。
从 settings.py 加载默认的 Appium 服务器主机和端口。 从 settings.py 加载默认的 Appium 服务器主机和端口。
""" """
self.driver: Optional[webdriver.Remote] = None self.driver = driver
self._current_implicit_timeout = IMPLICIT_WAIT_TIMEOUT
self._host = APPIUM_HOST self._host = APPIUM_HOST
self._port = APPIUM_PORT self._port = APPIUM_PORT
def server_config(self, host: str = APPIUM_HOST, port: int = APPIUM_PORT): @property
def server_url(self) -> str:
"""
动态构造 URL确保 server_config 修改后立即生效。
"""
return f"http://{self._host}:{self._port}"
def server_config(self, host: str = APPIUM_HOST, port: int = APPIUM_PORT)-> 'CoreDriver':
"""
配置服务器信息。支持链式调用。
:param host: ip
:param port: 端口
:return: 返回 CoreDriver 实例自身,支持链式调用。
"""
self._host = host self._host = host
self._port = port self._port = port
logger.info(f"Appium Server 指向 -> {self._host}:{self._port}") logger.info(f"Appium Server 指向 -> {self._host}:{self._port}")
return self return self
@staticmethod
def _make_options(platform: str | AppPlatform, caps: dict) -> AppiumOptions:
"""
根据平台生成对应的 Options
:param platform: 目标平台 ('android''ios'),支持 AppPlatform 枚举或字符串。
:param caps: Appium capabilities 字典。
:return: AppiumOptions
"""
match platform:
case AppPlatform.ANDROID.value:
logger.info(f"正在初始化 Android 会话...")
return UiAutomator2Options().load_capabilities(caps)
case AppPlatform.IOS.value:
logger.info(f"正在初始化 iOS 会话...")
return XCUITestOptions().load_capabilities(caps)
case _:
# 优化:不再默认返回 Android而是显式报错 (Fail Fast)
msg = f"不支持的平台类型: [{platform}]。当前仅支持: [android, ios]"
logger.error(msg)
raise ValueError(msg)
def connect(self, platform: str | AppPlatform, caps: dict, def connect(self, platform: str | AppPlatform, caps: dict,
extensions: list[Type[ExtensionBase]] | None = None, extensions: list[Type[ExtensionBase]] | None = None,
client_config: AppiumClientConfig | None = None) -> 'CoreDriver': client_config: AppiumClientConfig | None = None) -> 'CoreDriver':
@@ -71,33 +109,20 @@ class CoreDriver:
""" """
# 1. 统一格式化平台名称 # 1. 统一格式化平台名称
platform_name = platform.value if isinstance(platform, AppPlatform) else platform.lower().strip() platform_name = platform.value if isinstance(platform, AppPlatform) else platform.lower().strip()
url = f"http://{self._host}:{self._port}"
# 2. 预校验:如果已经有 driver 正在运行,先清理(防止 Session 冲突) # 2. 预校验:如果已经有 driver 正在运行,先清理(防止 Session 冲突)
if self.driver: if self.driver:
logger.warning("发现旧的 Driver 实例尚未关闭,正在强制重置...") logger.warning("发现旧的 Driver 实例尚未关闭,正在强制重置...")
self.quit() self.quit()
# 3. 匹配平台并加载 Options
options: AppiumOptions = self._make_options(platform_name, caps)
try: try:
# 3. 匹配平台并加载 Options
match platform_name:
case AppPlatform.ANDROID.value:
logger.info(f"正在初始化 Android 会话...")
options: AppiumOptions = UiAutomator2Options().load_capabilities(caps)
case AppPlatform.IOS.value:
logger.info(f"正在初始化 iOS 会话...")
options: AppiumOptions = XCUITestOptions().load_capabilities(caps)
case _:
# 优化:不再默认返回 Android而是显式报错 (Fail Fast)
msg = f"不支持的平台类型: [{platform_name}]。当前仅支持: [android, ios]"
logger.error(msg)
raise ValueError(msg)
# 4. 创建连接 # 4. 创建连接
self.driver = webdriver.Remote( self.driver = webdriver.Remote(
command_executor=url, command_executor=self.server_url,
options=options, options=options,
extensions=extensions, extensions=extensions,
client_config=client_config client_config=client_config
@@ -158,6 +183,7 @@ class CoreDriver:
:return: :return:
""" """
self.driver.implicitly_wait(timeout) self.driver.implicitly_wait(timeout)
self._current_implicit_timeout = timeout # 记录等待时间
@resolve_wait_method @resolve_wait_method
def explicit_wait(self, method: Union[Callable[[webdriver.Remote], T], str], timeout: Optional[float] = None) -> \ def explicit_wait(self, method: Union[Callable[[webdriver.Remote], T], str], timeout: Optional[float] = None) -> \
@@ -256,11 +282,40 @@ class CoreDriver:
self.explicit_wait(method, timeout).send_keys(text) self.explicit_wait(method, timeout).send_keys(text)
return self return self
def is_visible(self, by: str, value: str, timeout: Optional[float] = None) -> bool: def is_visible(self, by: str, value: str) -> bool | None:
""" """
判断元素是否可见 判断元素是否可见
:param by: 定位策略。 :param by: 定位策略。
:param value: 定位值。 :param value: 定位值。
:return: bool
"""
# 禁用隐式等待
original_timeout = self._current_implicit_timeout
try:
self.implicit_wait(0)
by = by_converter(by)
elements = self.driver.find_elements(by, value)
if elements:
return elements[0].is_displayed()
# 2. 元素存在于 DOM 中,还需要判断它在 UI 上是否真正可见(宽/高 > 0 且未隐藏)
return False
except (StaleElementReferenceException, NoSuchElementException):
# 这些属于预料中的“不可见”情况
return False
except Exception as e:
_ = e
return False
finally:
# 恢复原来的隐式等待时间
self.implicit_wait(original_timeout)
def wait_until_visible(self, by: str, value: str, timeout: Optional[float] = None) -> bool:
"""
等待元素出现
:param by: 定位策略。
:param value: 定位值。
:param timeout: 等待超时时间。 :param timeout: 等待超时时间。
:return: bool :return: bool
""" """
@@ -273,6 +328,23 @@ class CoreDriver:
except TimeoutException: except TimeoutException:
return False return False
def wait_until_not_visible(self, by: str, value: str, timeout: Optional[float] = None) -> bool:
"""
等待元素消失
:param by: 定位策略。
:param value: 定位值。
:param timeout: 等待超时时间。
:return: bool
"""
try:
by = by_converter(by)
mark = (by, value)
method = EC.invisibility_of_element_located(mark)
self.explicit_wait(method, timeout)
return True
except TimeoutException:
return False
def get_text(self, by: str, value: str, timeout: Optional[float] = None) -> str: def get_text(self, by: str, value: str, timeout: Optional[float] = None) -> str:
""" """
获取元素文本 获取元素文本
@@ -305,6 +377,60 @@ class CoreDriver:
logger.info(f"获取属性 {name} of {mark}: {attr_value}") logger.info(f"获取属性 {name} of {mark}: {attr_value}")
return attr_value return attr_value
def clear_popups(self, black_list: list = None, max_rounds: int = 5) -> bool:
"""
显式清理弹窗函数。
说明:
1. 快速扫描:使用 is_visible (不等待) 确认弹窗是否存在。
2. 动作处理:发现后点击,并触发 wait_until_not_visible (异步等待消失)。
3. 自适应退出:当整轮扫描无障碍物时,立即返回,不浪费时间。
4. 异常存证:若点击失败或发生错误,自动截图。
:param black_list: 允许传入当前页面特有的弹窗定位 [(by, value), ...](如某个活动的特殊广告)
:param max_rounds: 最大扫描轮数
:return: active: bool(清理过一个弹窗都将返回 True)
"""
if not black_list:
logger.warning("未提供黑名单列表,跳过清理动作。")
return False
list_len = len(black_list)
active = False
logger.info(f"开始执行显式弹窗清理,待检查项: {list_len}")
for round_idx in range(max_rounds):
skip_count = 0
for by, value in black_list:
if not self.is_visible(by, value):
skip_count += 1
continue
logger.info(f"当前权重{skip_count},第 {round_idx + 1} 轮:待清理弹窗 -> {value}")
try:
elements = self.find_elements(by, value, timeout=0.5) # 使用极短超时
if not elements:
skip_count += 1
else:
elements[0].click()
active = True
# 消失得快返回得就快,最多等 1.5s
if self.wait_until_not_visible(by, value, timeout=1.5):
logger.info(f"弹窗已成功消失")
else:
logger.warning(f"弹窗点击后仍存在")
except Exception as e:
safe_val = secrets.token_hex(8)
file_name = f"popup_fail_{round_idx}_{safe_val}.png"
logger.error(f"清理弹窗尝试点击时失败[{safe_val}:{value}]: {e}")
self.full_screen_screenshot(file_name)
raise e
if skip_count == list_len:
break
return active
@property @property
def session_id(self): def session_id(self):
"""获取当前 Appium 会话的 Session ID。""" """获取当前 Appium 会话的 Session ID。"""
@@ -411,7 +537,8 @@ class CoreDriver:
return self return self
def swipe_by_percent(self, start_xp:int, start_yp:int, end_xp:int, end_yp:int, duration: int = 1000) -> 'CoreDriver': def swipe_by_percent(self, start_xp: int, start_yp: int, end_xp: int, end_yp: int,
duration: int = 1000) -> 'CoreDriver':
""" """
按屏幕比例滑动 (0.5 = 50%) 按屏幕比例滑动 (0.5 = 50%)
""" """
@@ -426,13 +553,52 @@ class CoreDriver:
duration duration
) )
def full_screen_screenshot(self, name: str | None = None) -> str:
"""
截取当前完整屏幕内容 (自愈逻辑、异常报错首选)
:param name: 图片文件名
:return: 截图保存的路径
"""
file_name = f"{name or secrets.token_hex(8)}.png"
path = (SCREENSHOT_DIR / file_name).as_posix()
try:
# 核心save_screenshot 是底层原生方法,不依赖任何元素定位
self.driver.save_screenshot(path)
logger.info(f"全屏截图已保存: {path}")
return path
except Exception as e:
logger.error(f"全屏截图失败: {e}")
return ""
def element_screenshot(self, by: str, value: str, name: str | None = None) -> str:
"""
截取特定元素的图像 (业务校验、UI对比首选)
:param by: 定位策略
:param value: 定位值
:param name: 图片文件名
:return: 截图保存的路径
"""
file_name = f"{name or secrets.token_hex(8)}.png"
path = (SCREENSHOT_DIR / file_name).as_posix()
try:
by = by_converter(by)
# 核心:直接调用底层 find_element
self.driver.find_element(by, value).screenshot(path)
logger.info(f"元素截图已保存: {path}")
return path
except Exception as e:
logger.error(f"元素截图失败: {e}")
return ""
@property @property
def is_alive(self) -> bool: def is_alive(self) -> bool:
"""判断当前驱动会话是否仍然存活。""" """判断当前驱动会话是否仍然存活。"""
return self.driver is not None and self.driver.session_id is not None return self.driver is not None and self.driver.session_id is not None
# --- 断言逻辑 --- # --- 断言逻辑 ---
def assert_text(self, by:str, value:str, expected_text:str, timeout: Optional[float] = None) -> 'CoreDriver': def assert_text(self, by: str, value: str, expected_text: str, timeout: Optional[float] = None) -> 'CoreDriver':
""" """
断言元素的文本内容是否符合预期。 断言元素的文本内容是否符合预期。
:param by: 定位策略。 :param by: 定位策略。

View File

@@ -23,9 +23,9 @@ LOG_DIR = OUTPUT_DIR / "logs"
LOG_BACKUP_DIR = LOG_DIR / "backups" LOG_BACKUP_DIR = LOG_DIR / "backups"
ALLURE_TEMP = BASE_DIR / "temp" ALLURE_TEMP = BASE_DIR / "temp"
REPORT_DIR = BASE_DIR / "report" REPORT_DIR = BASE_DIR / "report"
SCREENSHOT_DIR = OUTPUT_DIR / "screenshots"
# 确保必要的目录存在 # 确保必要的目录存在
for folder in [LOG_DIR, LOG_BACKUP_DIR, ALLURE_TEMP]: for folder in [LOG_DIR, LOG_BACKUP_DIR, ALLURE_TEMP, SCREENSHOT_DIR]:
folder.mkdir(parents=True, exist_ok=True) folder.mkdir(parents=True, exist_ok=True)
# --- 文件路径 --- # --- 文件路径 ---
@@ -46,4 +46,21 @@ ANDROID_CAPS = {
"deviceName": "Android", "deviceName": "Android",
"appPackage": "com.android.settings", "appPackage": "com.android.settings",
"appActivity": ".Settings", "appActivity": ".Settings",
"noReset": False
}
# ANDROID_CAPS = {
# "platformName": "Android",
# "automationName": "uiautomator2",
# "deviceName": "Android",
# "appPackage": "com.bocionline.ibmp",
# "appActivity": "com.bocionline.ibmp.app.main.launcher.LauncherActivity",
# "noReset":False
# }
IOS_CAPS = {
"platformName": "iOS",
"automationName": "XCUITest",
"autoAcceptAlerts": True, # 自动接受系统权限请求
"waitForQuiescence": False, # 设为 False 可加速扫描
# 如果是某些特定的业务弹窗 autoAcceptAlerts 无效,
# 此时就会触发我们代码里的 PopupManager.solve()
} }

View File

@@ -1,78 +0,0 @@
#!/usr/bin/env python
# coding=utf-8
"""
@desc: 模拟登录功能测试用例
"""
import pytest
import logging
from core.driver import CoreDriver
from core.modules import AppPlatform
# 配置日志
logging.basicConfig(level=logging.INFO)
class TestLogin:
driver: CoreDriver = None
def setup_method(self):
"""
每个测试用例开始前执行:初始化 Driver 并连接设备
"""
# 定义测试设备的 Capabilities
# 注意实际使用时appPackage 和 appActivity 需要替换为被测 App 的真实值
caps = {
"platformName": "Android",
"automationName": "UiAutomator2",
"deviceName": "Android Emulator",
"appPackage": "com.example.android.apis", # 替换为你的 App 包名
"appActivity": ".ApiDemos", # 替换为你的 App 启动 Activity
"noReset": True, # 不清除应用数据
"newCommandTimeout": 60
}
self.driver = CoreDriver()
# 连接 Appium Server
self.driver.server_config(host="127.0.0.1", port=4723)
self.driver.connect(platform=AppPlatform.ANDROID, caps=caps)
def teardown_method(self):
"""
每个测试用例结束后执行:退出 Driver
"""
if self.driver:
self.driver.quit()
def test_login_success(self):
"""
测试场景:使用正确的用户名和密码登录成功
"""
# 1. 定位元素信息 (建议后续抽离到 Page Object 层)
# 假设登录页面的元素 ID 如下:
input_user = "id:com.example.app:id/et_username"
input_pass = "id:com.example.app:id/et_password"
btn_login = "id:com.example.app:id/btn_login"
txt_welcome = "xpath://*[@text='登录成功']"
# 2. 执行操作步骤
# 显式等待并输入用户名
self.driver.input(input_user, "", "test_user_001")
# 输入密码 (开启敏感模式,日志中脱敏)
self.driver.input(input_pass, "", "Password123!", sensitive=True)
# 点击登录按钮
self.driver.click(btn_login, "")
# 3. 断言结果
# 方式 A: 检查特定文本是否存在
# self.driver.assert_text(txt_welcome, "", "登录成功")
# 方式 B: 检查跳转后的页面元素是否可见
is_login_success = self.driver.is_visible(txt_welcome, "")
assert is_login_success, "登录失败:未检测到欢迎提示或主页元素"
if __name__ == "__main__":
# 允许直接运行此文件进行调试
pytest.main(["-v", "-s", __file__])

View File

@@ -2,8 +2,10 @@ import logging
from functools import wraps from functools import wraps
from typing import Union, Callable from typing import Union, Callable
from core.custom_expected_conditions import get_condition from core.custom_expected_conditions import get_condition
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)