feat: 基于 UI 自动化最佳实践的测试框架骨架搭建
- tests/config: 环境配置 (.yaml) + 配置加载器 - tests/utils: 日志工具 + 测试数据生成器 - tests/web: Playwright conftest + Page Object 基类 + 登录页 + 导航栏组件 + 登录用例(6条) - tests/app: Appium conftest + Page Object 基类 + 司机端首页 + 登录用例(3条) - pytest.ini: markers(p0/p1/p2/smoke/web/app) + 运行配置 - 所有用例按最佳实践采用 Page Object 模式 + data-testid 优先定位 + 失败自动截图
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""App Page Object 基类 — 封装移动端页面共享的通用操作。
|
||||
|
||||
每个 App 页面应继承 AppBasePage,在 __init__ 中声明所有定位器。
|
||||
|
||||
Usage:
|
||||
class DriverHomePage(AppBasePage):
|
||||
def __init__(self, driver, platform):
|
||||
super().__init__(driver, platform)
|
||||
self.order_tab = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "tab_orders")
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from appium.webdriver.webdriver import WebDriver
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
SCREENSHOTS_DIR = Path("output/screenshots")
|
||||
|
||||
|
||||
class AppBasePage:
|
||||
"""App Page Object 基类。"""
|
||||
|
||||
def __init__(self, driver: WebDriver, platform: str, timeout_ms: int = 30_000):
|
||||
self.driver = driver
|
||||
self.platform = platform # "android" | "ios"
|
||||
self.timeout = timeout_ms / 1000 # WebDriverWait 使用秒
|
||||
|
||||
# ── 通用等待 ──
|
||||
|
||||
def wait_visible_by_id(self, accessibility_id: str, timeout_s: float | None = None) -> None:
|
||||
"""等待 accessibility_id 对应的元素可见。"""
|
||||
wait = WebDriverWait(self.driver, timeout=timeout_s or self.timeout)
|
||||
wait.until(
|
||||
EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, accessibility_id))
|
||||
)
|
||||
|
||||
def wait_visible_by_text(self, text: str, timeout_s: float | None = None) -> None:
|
||||
"""等待页面上出现指定文本。"""
|
||||
wait = WebDriverWait(self.driver, timeout=timeout_s or self.timeout)
|
||||
|
||||
locator = (
|
||||
AppiumBy.XPATH, f"//*[@text='{text}']"
|
||||
if self.platform == "android"
|
||||
else (AppiumBy.XPATH, f"//*[@label='{text}']")
|
||||
)
|
||||
wait.until(EC.visibility_of_element_located(locator))
|
||||
|
||||
# ── 通用操作 ──
|
||||
|
||||
def tap_by_id(self, accessibility_id: str) -> None:
|
||||
"""按 accessibility_id 点击元素(推荐优先使用)。"""
|
||||
self.wait_visible_by_id(accessibility_id)
|
||||
self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id).click()
|
||||
|
||||
def tap_by_text(self, text: str) -> None:
|
||||
"""按文本点击元素。"""
|
||||
strategy = AppiumBy.XPATH
|
||||
locator = (
|
||||
f"//*[@text='{text}']"
|
||||
if self.platform == "android"
|
||||
else f"//*[@label='{text}']"
|
||||
)
|
||||
wait = WebDriverWait(self.driver, timeout=self.timeout)
|
||||
wait.until(EC.visibility_of_element_located((strategy, locator)))
|
||||
self.driver.find_element(strategy, locator).click()
|
||||
|
||||
def fill_by_id(self, accessibility_id: str, value: str) -> None:
|
||||
"""按 accessibility_id 找到输入框并填入内容。"""
|
||||
self.wait_visible_by_id(accessibility_id)
|
||||
element = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id)
|
||||
element.clear()
|
||||
element.send_keys(value)
|
||||
|
||||
def scroll_down(self) -> None:
|
||||
"""向下滑动(从屏幕 70% 滑到 30%)。"""
|
||||
size = self.driver.get_window_size()
|
||||
self.driver.swipe(
|
||||
start_x=size["width"] // 2,
|
||||
start_y=int(size["height"] * 0.7),
|
||||
end_x=size["width"] // 2,
|
||||
end_y=int(size["height"] * 0.3),
|
||||
duration=500,
|
||||
)
|
||||
|
||||
# ── 截图 ──
|
||||
|
||||
def screenshot(self, name: str) -> str:
|
||||
"""关键节点截图,返回文件路径。"""
|
||||
scenario_dir = SCREENSHOTS_DIR / self.__class__.__name__
|
||||
scenario_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
path = str(scenario_dir / f"{name}_{self.platform}_{ts}.png")
|
||||
self.driver.save_screenshot(path)
|
||||
return path
|
||||
Reference in New Issue
Block a user