feat(base_page): 新增BasePage基础操作

- 优化 is_visible,支持快速状态检查。
- 新增 log_screenshot/log_screenshot_bytes 截图。
- 更新 README.md。
- 其他优化。
This commit is contained in:
2026-01-30 18:06:15 +08:00
parent d3f9326baa
commit fd6f4fbcbe
9 changed files with 288 additions and 27 deletions

View File

@@ -1,6 +1,6 @@
# AppAutoTest
设备能力配置 (Capabilities)
## 设备能力配置 (Capabilities)
```python
ANDROID_CAPS = {
@@ -49,4 +49,17 @@ platformVersion指定设备的Android系统版本如 "11.0")。虽然不
unicodeKeyboard 和 resetKeyboard用于处理中文输入等特殊字符输入。
newCommandTimeout设置Appium服务器等待客户端发送新命令的超时时间默认为60秒。在长时间操作中可能需要增加。
newCommandTimeout设置Appium服务器等待客户端发送新命令的超时时间默认为60秒。在长时间操作中可能需要增加。
## allure核心属性表
| 属性 | 说明 | 用法示例 |
|---------------------|--------------------------------------|-------------|
| @allure.epic | 顶层分类APP项目名称 | 定义在测试类/项目上 |
| @allure.feature | 功能模块(如:登录模块、交易模块) | 定义在测试类上 |
| @allure.story | 用户场景(如:成功登录、账号锁定) | 定义在测试方法上 |
| @allure.title | 测试用例标题(支持动态显示) | 替换方法名显示在报告中 |
| @allure.severity | "严重程度BLOCKER, CRITICAL, NORMAL..." | 用于筛选高优先级用例 |
| @allure.description | 详细描述(支持 Markdown | 解释测试背景或前提条件 |
| @allure.link | 外部链接Bug系统、需求文档 | 快速点击跳转 |
| @allure.issue | 缺陷链接(通常会自动带上 ISSUE 前缀) | 追踪已知 Bug |

View File

@@ -42,8 +42,8 @@ def driver(app_server):
"platformName": "Android",
"automationName": "uiautomator2",
"deviceName": "Android",
"appPackage": "com.bocionline.ibmp",
"appActivity": "com.bocionline.ibmp.app.main.launcher.LauncherActivity",
"appPackage": "io.appium.android.apis",
"appActivity": "io.appium.android.apis.ApiDemos",
"noReset": False, # 不清除应用数据
"newCommandTimeout": 60
}

View File

@@ -11,12 +11,14 @@
"""
import logging
import secrets
from typing import Type, TypeVar, List, Tuple
from typing import Type, TypeVar, List, Tuple, Optional
import allure
from pathlib import Path
from appium import webdriver
from selenium.common import TimeoutException
from core.driver import CoreDriver
from utils.decorators import exception_capture
# 定义一个泛型用于类型推断IDE 依然会有补全提示)
T = TypeVar('T', bound='BasePage')
@@ -29,7 +31,74 @@ class BasePage(CoreDriver):
super().__init__(driver)
# 定义常见弹窗的关闭按钮定位
def log_screenshot(self, label: str = "步骤截图"):
"""
业务级截图:执行截图并附加到 Allure 报告。
用户可自由手动调用此方法。
"""
path_str = self.full_screen_screenshot(name=label)
if path_str:
img_path = Path(path_str)
if img_path.exists():
allure.attach.file(
img_path,
name=label,
attachment_type=allure.attachment_type.PNG
)
def log_screenshot_bytes(self, label: str = "步骤截图"):
"""
业务级截图:执行截图并附加到 Allure 报告。
用户可自由手动调用此方法。
"""
_img: bytes = self.driver.get_screenshot_as_png()
allure.attach(
_img,
name=label,
attachment_type=allure.attachment_type.PNG
)
# --- 常用断言逻辑 ---
def assert_text(self, by: str, value: str, expected_text: str, timeout: Optional[float] = None) -> 'BasePage':
"""
断言元素的文本内容是否符合预期。
:param by: 定位策略。
:param value: 定位值。
:param expected_text: 期望的文本。
:param timeout: 等待元素可见的超时时间。
:return: self支持链式调用。
:raises AssertionError: 如果文本不匹配。
"""
# 1. 增强报告展示:将断言动作包装为一个清晰的步骤
step_name = f"断言校验 | 预期结果: '{expected_text}'"
with allure.step(step_name):
actual = self.get_text(by, value, timeout)
# 2. 动态附件:在报告中直观对比,方便后期排查
allure.attach(
f"预期值: {expected_text}\n实际值: {actual}",
name="文本对比结果",
attachment_type=allure.attachment_type.TEXT
)
# 3. 执行核心断言
# 如果断言失败,抛出的 AssertionError 会被 conftest.py 中的 Hook 捕获并截图
assert actual == expected_text, f"断言失败: 期望 {expected_text}, 实际 {actual}"
logger.info(f"断言通过: 文本匹配 '{actual}'")
return self
# 这里放全局通用的 Page 属性和逻辑
def assert_visible(self, by: str, value: str, msg: str = "元素可见性校验"):
"""
增强版断言:成功/失败均截图
"""
with allure.step(f"断言检查: {msg}"):
try:
element = self.find_element(by, value)
assert element.is_displayed()
# 成功存证
except Exception as e:
raise e
# 封装一些所有页面通用的元动作
def clear_permission_popups(self):

View File

@@ -58,7 +58,7 @@ class CoreDriver:
"""
return f"http://{self._host}:{self._port}"
def server_config(self, host: str = APPIUM_HOST, port: int = APPIUM_PORT)-> 'CoreDriver':
def server_config(self, host: str = APPIUM_HOST, port: int = APPIUM_PORT) -> 'CoreDriver':
"""
配置服务器信息。支持链式调用。
:param host: ip
@@ -358,7 +358,7 @@ class CoreDriver:
method = EC.visibility_of_element_located(mark)
text = self.explicit_wait(method, timeout).text
logger.info(f"获取到的文本{text}")
logger.info(f"获取到的文本: {text}")
return text
def get_attribute(self, by: str, value: str, name: str, timeout: Optional[float] = None) -> str:
@@ -597,22 +597,6 @@ class CoreDriver:
"""判断当前驱动会话是否仍然存活。"""
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':
"""
断言元素的文本内容是否符合预期。
:param by: 定位策略。
:param value: 定位值。
:param expected_text: 期望的文本。
:param timeout: 等待元素可见的超时时间。
:return: self支持链式调用。
:raises AssertionError: 如果文本不匹配。
"""
actual = self.get_text(by, value, timeout)
assert actual == expected_text, f"断言失败: 期望 {expected_text}, 实际 {actual}"
logger.info(f"断言通过: 文本匹配 '{actual}'")
return self
def quit(self):
"""安全关闭 Appium 驱动并断开连接。"""
if self.driver:

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei,ChenWei
@Software: PyCharm
@contact: t6g888@163.com
@file: test_home
@date: 2026/1/30 17:18
@desc:
"""
import logging
import allure
from appium import webdriver
from core.base_page import BasePage
logger = logging.getLogger(__name__)
class HomePage(BasePage):
# 定位参数
text = ("accessibility id", "Text")
unicode = ("accessibility id", "Unicode")
def __init__(self, driver: webdriver.Remote):
super().__init__(driver)
@allure.step("点击 “Text ”")
def click_text(self):
if self.wait_until_visible(*self.text, timeout=1):
with allure.step("发现Text开始执行点击"):
# self.log_screenshot_bytes("Text截图").click(*self.text)
self.log_screenshot_bytes("Text截图")
self.click(*self.text)
@allure.step("点击 “Unicode ”:{1}")
def click_unicode(self, taget):
"""执行登录业务逻辑"""
# 调用继承自 CoreDriver 的方法(假设你的 CoreDriver 已经被注入或组合)
if self.wait_until_visible(*self.unicode):
self.swipe("left")
self.click(*self.unicode).log_screenshot()
@allure.step("获取 “Text ”文本")
def get_home_text(self):
"""执行登录业务逻辑"""
# 调用继承自 CoreDriver 的方法(假设你的 CoreDriver 已经被注入或组合)
return self.get_text(*self.text)

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei,ChenWei
@Software: PyCharm
@contact: t6g888@163.com
@file: test_views
@date: 2026/1/30 17:37
@desc:
"""
import logging
import allure
from appium import webdriver
from core.base_page import BasePage
logger = logging.getLogger(__name__)
class ViewsPage(BasePage):
# 定位参数
views = ("accessibility id","Views")
def __init__(self, driver: webdriver.Remote):
super().__init__(driver)
@allure.step("截图 “Views ”")
def screenshot_views(self):
if self.wait_until_visible(*self.views):
with allure.step("发现Views开始执行点击"):
self.log_screenshot_bytes("Text截图")

View File

@@ -8,4 +8,45 @@
@file: conftest
@date: 2026/1/19 14:08
@desc:
"""
"""
import pytest
import allure
from pathlib import Path
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""
本钩子函数会在每个测试阶段setup, call, teardown执行后被调用。
item: 测试用例对象
call: 测试执行阶段的信息
"""
# 1. 先执行常规的用例报告生成逻辑
outcome = yield
report = outcome.get_result()
# 2. 我们只关注测试执行阶段 ("call")
# 如果该阶段失败了failed则触发截图
if report.when == "call" and report.failed:
# 3. 从测试用例中获取 driver 实例
# 假设你在 fixture 中注入的参数名为 'driver'
driver_instance = item.funcargs.get("driver")
if driver_instance:
try:
# 4. 调用你在 CoreDriver 中实现的底层截图方法
# 这里的 name 我们可以动态取测试用例的名字
case_name = item.name
file_path = driver_instance.full_screen_screenshot(name=f"CRASH_{case_name}")
# 5. 如果路径存在,将其关联到 Allure 报告
if file_path:
p = Path(file_path)
if p.exists():
allure.attach.file(
source=p,
name="【故障现场自动截图】",
attachment_type=allure.attachment_type.PNG
)
except Exception as e:
print(f"故障自动截图执行失败: {e}")

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei,ChenWei
@Software: PyCharm
@contact: t6g888@163.com
@file: test_api_demos
@date: 2026/1/30 17:42
@desc:
"""
import logging
import allure
import pytest
from page_objects.api_demod_home import HomePage
from page_objects.api_demos_views import ViewsPage
# 配置日志
logger = logging.getLogger(__name__)
@allure.epic("ApiDemos")
@allure.feature("登录认证模块")
class TestApiDemos:
# @allure.story("常规登录场景")
@allure.title("使用合法账号登录成功")
@allure.severity(allure.severity_level.BLOCKER)
@allure.description("""
验证用户在正常网络环境下:
1. 处理初始化弹窗和广告
2. 选择证券登录类型
3. 输入正确凭证后成功跳转至‘交易’首页
""")
@allure.link("https://docs.example.com/login_spec", name="登录业务说明文档")
@allure.issue("BUG-1001", "已知偶发部分机型广告Banner无法滑动")
def test_api_demos_success(self, driver,user):
"""
测试场景:使用正确的用户名和密码登录成功
"""
api_demos = HomePage(driver)
# 执行业务逻辑
api_demos.click_text()
api_demos.click_unicode()
# 断言部分使用 allure.step 包装,使其在报告中也是一个可读的步骤
with allure.step("最终校验:检查是否进入首页并显示‘交易’标题"):
actual_text = api_demos.get_home_text()
assert actual_text == "Text"
# 页面跳转
api_demos.go_to(ViewsPage).screenshot_views()
api_demos.delay(5)

View File

@@ -5,7 +5,6 @@ from typing import Union, Callable
from core.custom_expected_conditions import get_condition
logger = logging.getLogger(__name__)
@@ -36,4 +35,20 @@ def resolve_wait_method(func):
return func(self, method, *args, **kwargs)
return wrapper
return wrapper
def exception_capture(func):
"""
仅在原子动作失败时,触发 BasePage 层的业务截图逻辑
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except Exception as e:
# 自动捕获:调用 BasePage 层的 log_screenshot
if hasattr(self, "log_screenshot"):
self.log_screenshot(f"自动异常捕获{func.__name__}")
raise e
return wrapper