30 KiB
Web + App UI 自动化测试最佳实践
版本: v1.0 | 日期: 2026-07-15 | 维护者: QE Fleet Team
定位: 面向测试工程师和 AI Agent 的统一 UI 自动化实践指南。人类工程师可按章查阅、逐条落地;AI Agent(
web-executor、mobile-executor)可将本文档规则作为脚本生成依据。前置阅读: 建议先阅读
knowledge_base/01_standards/test_case_template.md(用例规范)和definition_of_done.md(完成定义)。
目录
1. 总则与适用范围
1.1 适用场景
| 场景 | Web (Playwright) | App (Appium) | 说明 |
|---|---|---|---|
| PC Web 管理后台 | ✅ | — | 平台端、商家端等中后台系统 |
| 移动 H5 页面 | ✅ | — | 微信 H5、手机浏览器页面 |
| Android 原生 APP | — | ✅ | UiAutomator2 驱动 |
| iOS 原生 APP | — | ✅ | XCUITest 驱动 |
| 微信小程序 | ✅ | — | 小程序本质是 WebView,用 Playwright 操作 |
| 混合场景(Web + App 联动) | ✅ | ✅ | 如后台发起操作 → APP 端验证结果 |
1.2 与项目文档体系的关系
本文档(最佳实践)
├── 引用 agents/execute/web_executor.md(Web 执行规则)
├── 引用 agents/execute/mobile_executor.md(App 执行规则)
├── 符合 knowledge_base/01_standards/test_case_template.md(用例规范)
├── 符合 knowledge_base/01_standards/definition_of_done.md(完成定义)
└── 补充 knowledge_base/03_best_practices/(本目录)的自动化工程视角
1.3 基础原则
- 自动化不是目标,质量才是。 只自动化那些需要频繁回归、手工执行成本高的用例。
- 稳定性 > 覆盖率。 100 个可靠的用例比 500 个频繁误报的用例有价值。
- 可维护性优先。 定位策略、数据准备、断言逻辑必须清晰可读。
- 失败必须可追溯。 每次失败必须产出截图 + 日志 + 操作序列,让接手者无需复现即可定位。
2. 技术栈与框架选型
2.1 框架选择决策表
| 被测端 | 推荐框架 | 当前版本 | 驱动/引擎 |
|---|---|---|---|
| PC Web | Playwright (Python) | ≥1.40 | Chromium / Firefox / WebKit |
| 移动 H5 | Playwright (Python) | ≥1.40 | Chromium (移动视口模拟) |
| Android APP | Appium (Python) | ≥3.0 | UiAutomator2 |
| iOS APP | Appium (Python) | ≥3.0 | XCUITest |
| 微信小程序 | Playwright (Python) | ≥1.40 | Chromium (特定 User-Agent) |
2.2 为什么选 Playwright 而非 Selenium?
| 维度 | Playwright | Selenium |
|---|---|---|
| 自动等待 | 内置(actionability checks) | 需手动 WebDriverWait |
| 网络拦截 | page.route() 原生支持 |
需 CDP 或代理 |
| 多浏览器 | 单 API,同代码 | 各浏览器 driver 独立 |
| 调试工具 | --debug、Trace Viewer、Codegen |
较弱的原生日志 |
| 执行速度 | 快(单一 WebSocket 连接) | 慢(HTTP 往返) |
团队决策: 本项目统一使用 Playwright。旧 Selenium 用例应逐步迁移。
2.3 为什么选 Appium 而非专用框架?
| 维度 | Appium | Espresso / XCUITest 原生 |
|---|---|---|
| 跨平台复用 | 高(一套 API 双平台) | 低(两套独立代码) |
| 学习成本 | 低(WebDriver 协议) | 高(平台特有 API) |
| CI 集成 | 成熟(Server-Client 架构) | 需额外工具链 |
| 脚本语言 | Python / Java / JS 等 | Java/Kotlin (Android) / Swift (iOS) |
团队决策: 本项目统一使用 Appium (Python Client)。除非有性能压测或白盒测试需求,不引入原生框架。
3. Web UI 自动化最佳实践
3.1 定位策略(Element Locator)
优先级从高到低:
| 优先级 | 策略 | 示例 | 稳定性 |
|---|---|---|---|
| 1 | data-testid |
page.getByTestId('submit-btn') |
⭐⭐⭐⭐⭐ |
| 2 | role + name |
page.getByRole('button', {name: '提交'}) |
⭐⭐⭐⭐ |
| 3 | text |
page.getByText('确认删除') |
⭐⭐⭐ |
| 4 | placeholder |
page.getByPlaceholder('请输入手机号') |
⭐⭐⭐ |
| 5 | label |
page.getByLabel('用户名') |
⭐⭐⭐ |
| 6 | CSS selector | page.locator('.form .submit-btn') |
⭐⭐ |
| 7 | XPath | page.locator('//div[@class="foo"]//button') |
⭐ |
原则:
- ✅ 永远优先使用语义化定位(
data-testid、role、text)。它们不随 UI 重构而断裂。 - ❌ 避免使用 CSS class(尤其是
_hash_abc123这类编译期生成的 class)。 - ❌ 避免 XPath 长链(
/html/body/div[3]/div[2]/span[1]),改一个嵌套层级就全挂。 - ⚠️ 不确定的 selector 用注释标注
# ⚠️ 需确认 selector,不要捏造。
反例 vs 正例:
# ❌ 反例:CSS class 不稳定,UI 改动就失效
page.locator('.ant-btn.ant-btn-primary.btn-submit-form-v2').click()
# ❌ 反例:深层 XPath,嵌套改动就挂
page.locator('/html/body/div[3]/div[2]/div[1]/form/div[4]/button[1]').click()
# ✅ 正例:data-testid,与视觉样式解耦
page.getByTestId('order-submit-btn').click()
# ✅ 正例:语义化 role + name
page.getByRole('button', name='提交订单').click()
# ✅ 正例:文本匹配
page.getByText('确认收货').click()
3.2 等待策略
Playwright 内置自动等待(actionability checks),大多数场景不需要手动等待。只有在以下情况才显式加等待:
| 场景 | 推荐写法 | 说明 |
|---|---|---|
| 页面跳转后等待元素 | page.waitForSelector('[data-testid="result"]') |
等待关键元素出现 |
| 等待网络请求完成 | page.waitForResponse(lambda r: '/api/order' in r.url) |
等待 API 返回 |
| 等待特定状态文本 | expect(page.getByText('提交成功')).toBeVisible() |
等待 UI 状态变化 |
| 等待元素消失 | expect(page.locator('.loading')).toBeHidden() |
等待 Loading 结束 |
反例 vs 正例:
# ❌ 反例:硬编码 sleep——浪费时间且不可靠
import time
page.click('button')
time.sleep(3) # 网速慢时 3 秒不够,快时浪费
assert page.getByText('成功').is_visible()
# ✅ 正例:使用 Playwright 自动等待 + 显式等待结合
page.getByTestId('submit-btn').click()
expect(page.getByText('提交成功')).toBeVisible(timeout=10000) # 最多等 10 秒
3.3 Page Object 模式
将页面元素定位和操作封装为独立的 Page Object 类,实现用例逻辑与页面结构的分离。
结构约定:
# pages/order_page.py
from playwright.sync_api import Page, expect
class OrderPage:
"""订单确认页"""
def __init__(self, page: Page):
self.page = page
# 所有定位器集中定义在 __init__
self.submit_btn = page.getByTestId('order-submit-btn')
self.amount_text = page.getByTestId('order-total-amount')
self.success_tip = page.getByText('下单成功')
def get_total_amount(self) -> str:
"""获取订单总金额"""
return self.amount_text.inner_text()
def submit(self) -> None:
"""提交订单并等待成功提示"""
self.submit_btn.click()
expect(self.success_tip).toBeVisible(timeout=10000)
def verify_amount(self, expected: str) -> None:
"""验证订单金额"""
expect(self.amount_text).toContainText(expected)
原则:
- 每个 Page Object 对应一个页面或一个功能区域(弹窗、抽屉可独立)。
- 定位器集中声明在
__init__(),方法只调用已声明的定位器。 - 断言尽可能封装在 Page Object 方法内(如
submit()自带成功断言)。 - 跨页面复用操作封装为公共组件(如
LoginComponent、NavBar),而不是在每个 Page Object 中重复。
3.4 截图策略
本项目的截图策略与 agents/execute/web_executor.md 对齐:
| 策略 | 配置参数 | 说明 |
|---|---|---|
| 失败截图(默认开启) | screenshot_on_failure: true |
断言失败/异常时自动截图 |
| 每步截图(默认关闭) | screenshot_on_step: false |
量大,仅在调试阶段开启 |
| 关键节点截图 | 始终开启 | P0 用例的关键验证点 |
# ✅ 正例:失败自动截图(conftest.py 全局 fixture)
@pytest.fixture(autouse=True)
def screenshot_on_failure(request, page):
yield
if request.node.rep_call.failed:
path = f"output/screenshots/{request.node.name}_{datetime.now():%Y%m%d_%H%M%S}.png"
page.screenshot(path=path, full_page=True)
# ✅ 正例:关键节点显式截图
page.getByTestId('pay-btn').click()
page.screenshot(path=f"output/screenshots/after_payment_{order_id}.png")
截图命名规范: {用例编号}_{操作描述}_{时间戳}.png,如 ORDER_001_click_submit_20260715_143020.png。
3.5 重试与容错
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 失败用例重试 | 1 次 | retry_on_failure: 1 |
| 操作超时 | 30 秒 | 单次操作(点击/输入)的超时 |
| 断言超时 | 10 秒 | expect().toBeVisible() 的等待上限 |
| 页面加载超时 | 60 秒 | page.goto() 的导航超时 |
# ✅ 正例:关键操作加重试(仅在网络相关失败时)
from playwright.sync_api import TimeoutError as PlaywrightTimeout
def safe_click(locator, max_retries=2):
for attempt in range(max_retries):
try:
locator.click(timeout=30000)
return
except PlaywrightTimeout:
if attempt == max_retries - 1:
raise
time.sleep(2)
⚠️ 注意: 重试仅适用于网络抖动、临时渲染问题。断言失败(业务逻辑错误)不应重试,直接标记失败。
3.6 多浏览器覆盖
# conftest.py — 按 fleet_config.yml 配置的 browsers 参数化
import pytest
from playwright.sync_api import sync_playwright
BROWSERS = ["chromium", "firefox", "webkit"] # 或从 fleet_config.yml 读取
@pytest.fixture(params=BROWSERS)
def browser_context(request):
with sync_playwright() as p:
browser = getattr(p, request.param).launch(headless=True)
context = browser.new_context(viewport={"width": 1920, "height": 1080})
yield context
context.close()
browser.close()
建议: P0 用例覆盖 Chromium + 至少一个额外浏览器;P1 用例默认仅 Chromium。
4. App UI 自动化最佳实践
4.1 定位策略(Element Locator)
优先级从高到低:
| 优先级 | 策略 | Android 示例 | iOS 示例 | 稳定性 |
|---|---|---|---|---|
| 1 | accessibility_id |
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "submit") |
同 | ⭐⭐⭐⭐⭐ |
| 2 | id |
driver.find_element(AppiumBy.ID, "com.app:id/btn") |
driver.find_element(AppiumBy.ID, "submitBtn") |
⭐⭐⭐⭐ |
| 3 | class_name + index |
driver.find_elements(AppiumBy.CLASS_NAME, "android.widget.Button")[0] |
同 | ⭐⭐ |
| 4 | XPath | driver.find_element(AppiumBy.XPATH, "//*[@text='确认']") |
同 | ⭐ |
原则:
- ✅ 优先让开发在关键元素上添加
content-desc(Android)或accessibilityIdentifier(iOS),对应accessibility_id定位。 - ❌ 避免 XPath,移动端 UI 层级深且频繁变动。
- ❌ 避免坐标点击(
tap([x, y])),不同分辨率/设备上位置完全不同。 - ⚠️ 不确定的定位方式用注释标注
# ⚠️ 需确认定位方式。
反例 vs 正例:
# ❌ 反例:坐标点击——换一台设备就挂
driver.tap([(300, 500)])
# ❌ 反例:深层 XPath
driver.find_element(AppiumBy.XPATH, "//android.view.View[3]/android.widget.Button[2]")
# ✅ 正例:accessibility_id 最稳定
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "submit_order_btn").click()
# ✅ 正例:id 定位(Android 资源 ID)
driver.find_element(AppiumBy.ID, "com.example.app:id/btn_pay").click()
4.2 Android vs iOS 双平台差异
# ✅ 正例:平台感知的 Driver Factory
import platform
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.options.ios import XCUITestOptions
def create_driver():
if platform.system() == "Android":
options = UiAutomator2Options()
options.device_name = "Android Emulator"
options.app_package = "com.example.app" # ⚠️ 必改
options.app_activity = ".MainActivity" # ⚠️ 必改
options.no_reset = True
options.automation_name = "UiAutomator2"
return webdriver.Remote("http://localhost:4723", options=options)
else:
options = XCUITestOptions()
options.device_name = "iPhone 15"
options.bundle_id = "com.example.app" # ⚠️ 必改
options.no_reset = True
options.automation_name = "XCUITest"
return webdriver.Remote("http://localhost:4723", options=options)
| 差异点 | Android | iOS |
|---|---|---|
| 驱动 | UiAutomator2 | XCUITest |
| 应用标识 | appPackage + appActivity |
bundleId |
| 等待策略 | WebDriverWait + expected_conditions |
同,但部分 condition 支持有限 |
| 手势 API | driver.swipe() |
driver.swipe()(API 一致) |
| 截图 | driver.save_screenshot(path) |
同 |
4.3 手势操作
# 滑动(从下往上)
def swipe_up(driver, duration_ms=500):
size = driver.get_window_size()
driver.swipe(
start_x=size['width'] // 2,
start_y=int(size['height'] * 0.8),
end_x=size['width'] // 2,
end_y=int(size['height'] * 0.2),
duration=duration_ms
)
# 长按
from appium.webdriver.common.appiumby import AppiumBy
from appium.webdriver.common.touch_action import TouchAction
def long_press(driver, element, duration_ms=1000):
TouchAction(driver).long_press(element, duration=duration_ms).release().perform()
# 双指缩放(地图/图片)
def pinch(driver, scale=0.5):
# ⚠️ 推荐使用 MultiAction API,因设备差异较大,需实际调试
pass
4.4 等待策略
移动端网络不稳定,等待策略比 Web 更关键:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from appium.webdriver.common.appiumby import AppiumBy
# ✅ 正例:显式等待元素可见后再操作
wait = WebDriverWait(driver, timeout=30)
element = wait.until(
EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "order_status"))
)
assert element.text == "已完成"
# ❌ 反例:硬编码 sleep
import time
time.sleep(5) # 网络差时 5 秒不够
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "order_status")
4.5 截图策略
| 策略 | 说明 |
|---|---|
| 失败截图(默认开启) | 断言失败/异常时立即截图 |
| 关键页面截图 | 登录后首页、核心操作页、结果页 |
| 状态变化截图 | 状态流转前后各截一张 |
def take_screenshot(driver, name: str):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
path = f"output/screenshots/{name}_{timestamp}.png"
driver.save_screenshot(path)
return path
# 失败自动截图(conftest.py)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
if outcome.get_result().failed and "driver" in item.funcargs:
driver = item.funcargs["driver"]
take_screenshot(driver, f"FAIL_{item.name}")
5. 通用工程实践
以下实践同时适用于 Web 和 App 自动化。
5.1 用例独立性
每个测试用例必须是独立且可任意顺序执行的。
# ✅ 正例:每个用例独立准备数据 + 独立清理
class TestOrderSubmit:
@pytest.fixture(autouse=True)
def setup_and_teardown(self):
self.test_data = create_test_order() # 每个用例创建独立数据
yield
cleanup_test_order(self.test_data['id']) # 用例结束后清理
def test_normal_submit(self):
result = submit_order(self.test_data['id'])
assert result.status == "SUCCESS"
def test_duplicate_submit(self):
submit_order(self.test_data['id']) # 第一次提交
result = submit_order(self.test_data['id']) # 重复提交
assert result.status == "ALREADY_SUBMITTED"
# ❌ 反例:用例之间存在数据依赖
def test_a_create_order():
global order_id # 全局变量,换执行顺序就挂
order_id = create_order()
def test_b_pay_order():
pay_order(order_id) # 依赖 test_a 先执行,且 order_id 不为空
5.2 数据驱动
将测试数据与用例逻辑分离,同一套用例覆盖多组数据:
# ✅ 正例:参数化测试数据
import pytest
TEST_DATA = [
# (支付方式, 金额, 预期结果)
("支付宝", 100, "支付成功"),
("微信支付", 50, "支付成功"),
("银行卡", 1000, "支付成功"),
("支付宝", 0, "金额不能为0"),
("支付宝", -10, "金额不能为负"),
]
class TestPayment:
@pytest.mark.parametrize("pay_method,amount,expected", TEST_DATA)
def test_payment(self, pay_method, amount, expected):
result = pay(method=pay_method, amount=amount)
assert result.message == expected
5.3 环境配置管理
所有环境相关信息统一管理,不硬编码在测试脚本中:
# config/test_config.yaml
base_url: "https://test.example.com"
timeout_seconds: 120
accounts:
admin: { phone: "13800138000", password: "Test123456" }
user: { phone: "13800138001", password: "Test123456" }
browsers: ["chromium", "firefox"]
# ✅ 正例:从配置读取
import yaml
from pathlib import Path
def load_config():
with open(Path(__file__).parent / "config/test_config.yaml", encoding="utf-8") as f:
return yaml.safe_load(f)
config = load_config()
page.goto(config["base_url"])
page.getByPlaceholder("手机号").fill(config["accounts"]["admin"]["phone"])
# ❌ 反例:硬编码
BASE_URL = "https://test.example.com" # 换环境要改代码
ADMIN_PHONE = "13800138000" # 换账号要改代码
5.4 报告与日志
import logging
from datetime import datetime
# 日志配置
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(f"output/logs/test_{datetime.now():%Y%m%d_%H%M%S}.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class TestOrder:
def test_submit(self):
logger.info(f"开始执行: 验证正常下单")
try:
# ...
logger.info(f"✅ 通过: 验证正常下单")
except Exception as e:
logger.error(f"❌ 失败: 验证正常下单 — {e}")
raise
报告输出要求:
- 每个用例执行后产出: 通过/失败/跳过 + 耗时 + 失败原因(如有)+ 截图路径
- 批量运行结束后产出汇总:
通过 X / 失败 Y / 跳过 Z,列出所有失败用例的截图链接
5.5 CI 集成
# CI 配置示例 (GitHub Actions / Gitea Actions)
jobs:
web-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- run: playwright install chromium
- run: pytest tests/web/ --screenshot on_failure --junitxml=report.xml
app-tests:
runs-on: macos-latest # iOS 测试必须 macOS
steps:
- uses: actions/checkout@v4
- run: |
# 启动 Appium Server
appium --log-level info --log appium.log &
sleep 5
- run: pytest tests/app/ --platform android --junitxml=report.xml
6. UI 自动化用例范例
以下范例直接从
knowledge_base/03_best_practices/下的业务用例中提取自动化实现,展示"好的手工用例"如何映射为"可运行的自动化代码"。
6.1 Web — 支付流程 (Playwright)
来源用例: payment_flow_cases.md — PAY_01_001(验证正常支付流程成功)
# tests/web/test_payment.py
import pytest
from playwright.sync_api import Page, expect
class TestPaymentFlow:
"""支付流程自动化测试"""
@pytest.fixture(autouse=True)
def setup(self, page: Page):
"""每个用例前登录 + 准备可售商品"""
self.page = page
login(page, user="user_A") # 封装好的登录函数
self.item_id = add_item_to_cart(page, sku="SKU-001") # 添加商品到购物车
yield
cancel_remaining_orders(page) # 清理未支付订单
def test_normal_payment_alipay(self, page: Page):
"""PAY_01_001: 验证正常支付流程成功"""
# Step 1-2: 进入收银台,选择支付宝
page.getByTestId("checkout-btn").click()
expect(page.getByTestId("cashier-page")).toBeVisible()
page.getByTestId("pay-method-alipay").click()
# Step 3: 完成支付
page.getByTestId("confirm-pay-btn").click()
# 预期 1: 页面跳转支付成功页
expect(page.getByText("支付成功")).toBeVisible(timeout=10000)
# 预期 2: 订单状态变更为"待发货"
page.getByTestId("nav-orders").click()
order_status = page.getByTestId(f"order-{self.item_id}-status")
expect(order_status).toHaveText("待发货")
# 预期 3: 支付流水金额与订单实付一致
page.getByTestId(f"order-{self.item_id}-detail").click()
pay_amount = page.getByTestId("pay-record-amount").inner_text()
order_amount = page.getByTestId("order-actual-amount").inner_text()
assert pay_amount == order_amount, f"支付金额 {pay_amount} ≠ 订单金额 {order_amount}"
6.2 Web — 异常流程 (Playwright)
def test_payment_balance_insufficient_switch_method(self, page: Page):
"""PAY_01_003: 验证支付渠道返回余额不足时可切换支付方式"""
page.getByTestId("checkout-btn").click()
# 选择银行卡支付
page.getByTestId("pay-method-bankcard").click()
# 模拟支付渠道返回余额不足
page.getByTestId("confirm-pay-btn").click()
expect(page.getByText("余额不足")).toBeVisible(timeout=10000)
# 订单仍保持待支付状态
page.getByTestId("nav-orders").click()
order_status = page.getByTestId(f"order-{self.item_id}-status")
expect(order_status).toHaveText("待支付")
# 可切换支付方式继续支付
page.getByTestId(f"order-{self.item_id}-pay").click()
page.getByTestId("pay-method-alipay").click()
page.getByTestId("confirm-pay-btn").click()
expect(page.getByText("支付成功")).toBeVisible(timeout=10000)
6.3 Web — 表单验证 (Playwright)
# tests/web/test_login.py
class TestLogin:
"""登录功能自动化测试"""
@pytest.mark.parametrize("phone,password,expected_error", [
("", "Test123456", "请输入手机号"),
("13800138000", "", "请输入密码"),
("13800138000", "wrong", "密码错误"),
("13800000000", "Test123456", "账号不存在"),
])
def test_login_validation(self, page: Page, phone, password, expected_error):
"""验证登录表单校验"""
page.goto("/login")
if phone:
page.getByPlaceholder("请输入手机号").fill(phone)
if password:
page.getByPlaceholder("请输入密码").fill(password)
page.getByRole("button", name="登录").click()
expect(page.getByText(expected_error)).toBeVisible()
6.4 App — 登录流程 (Appium)
# tests/app/test_login.py
import pytest
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestAppLogin:
"""APP 登录自动化测试"""
def test_login_success(self, driver):
"""验证正常登录成功"""
wait = WebDriverWait(driver, timeout=30)
# 输入手机号
phone_input = wait.until(
EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "login_phone_input"))
)
phone_input.send_keys("13800138000")
# 输入密码
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_password_input").send_keys("Test123456")
# 点击登录
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_submit_btn").click()
# 验证跳转主页
home_tab = wait.until(
EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "main_home_tab"))
)
assert home_tab.is_displayed()
6.5 App — 订单列表滑动与加载 (Appium)
# tests/app/test_order_list.py
class TestOrderList:
"""APP 订单列表测试"""
def test_pull_to_refresh(self, driver):
"""验证下拉刷新订单列表"""
wait = WebDriverWait(driver, timeout=30)
# 等待列表加载
wait.until(
EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "order_list"))
)
# 下拉刷新
size = driver.get_window_size()
driver.swipe(
start_x=size['width'] // 2,
start_y=int(size['height'] * 0.3),
end_x=size['width'] // 2,
end_y=int(size['height'] * 0.7),
duration=500
)
# 验证刷新成功提示
toast = wait.until(
EC.visibility_of_element_located((AppiumBy.XPATH, "//*[contains(@text,'刷新成功')]"))
)
assert toast is not None
def test_scroll_load_more(self, driver):
"""验证上滑加载更多"""
wait = WebDriverWait(driver, timeout=30)
# 获取当前列表项数量
initial_items = len(driver.find_elements(AppiumBy.ACCESSIBILITY_ID, "order_card"))
# 滑动到底部
size = driver.get_window_size()
driver.swipe(
start_x=size['width'] // 2,
start_y=int(size['height'] * 0.8),
end_x=size['width'] // 2,
end_y=int(size['height'] * 0.2),
duration=500
)
# 验证加载更多
time.sleep(1) # 等待渲染完成
after_items = len(driver.find_elements(AppiumBy.ACCESSIBILITY_ID, "order_card"))
assert after_items >= initial_items, f"加载更多后应不少于加载前: {after_items} < {initial_items}"
7. 附录
7.1 Selector 速查表
Playwright (Web)
| 场景 | 写法 |
|---|---|
| 按 data-testid 点击 | page.getByTestId('my-btn').click() |
| 按文本点击 | page.getByText('确 定').click() |
| 按角色+名称点击 | page.getByRole('button', name='提交').click() |
| 按 placeholder 填写 | page.getByPlaceholder('请输入').fill('hello') |
| 按 label 填写 | page.getByLabel('手机号').fill('13800138000') |
| 等待可见 | expect(page.getByText('成功')).toBeVisible() |
| 等待隐藏 | expect(page.locator('.loading')).toBeHidden() |
| 下拉框选择 | page.selectOption('#type', 'value1') |
| 文件上传 | page.setInputFiles('input[type=file]', 'path/to/file') |
| 确认弹窗 | page.on('dialog', lambda d: d.accept()) |
| iframe 内操作 | page.frameLocator('#iframe').getByTestId('btn') |
Appium (App)
| 场景 | Android 写法 | iOS 写法 |
|---|---|---|
| 按 accessibility_id | driver.find_element(AppiumBy.ACCESSIBILITY_ID, "xxx") |
同 |
| 按 id | driver.find_element(AppiumBy.ID, "com.app:id/xxx") |
driver.find_element(AppiumBy.ID, "xxx") |
| 按文本 | driver.find_element(AppiumBy.XPATH, "//*[@text='xxx']") |
driver.find_element(AppiumBy.XPATH, "//*[@label='xxx']") |
| 滑动 | driver.swipe(x1,y1,x2,y2,duration) |
同 |
| 截图 | driver.save_screenshot(path) |
同 |
| 上下文切换 | driver.switch_to.context('WEBVIEW') |
同 |
| 后台运行 | driver.background_app(5) |
同 |
7.2 常见坑与解决方案
| 坑 | 现象 | 解决方案 |
|---|---|---|
| CSS Module hash | locator('.btn_abc123') 找不到 |
用 getByTestId 或 getByText |
| iframe 跨域 | page.locator() 在 iframe 内失效 |
用 page.frameLocator('#iframe') |
| 非固定等待时间 | time.sleep(3) 不够或浪费 |
用 expect(...).toBeVisible(timeout=N) |
| 元素被遮挡 | click() 报 Element not clickable |
locator.click(force=True) 或先滚动到可见区域 |
| 新标签页 | 点击后在新 tab 打开,操作还在旧 tab | page.context().pages[1] 获取新 tab |
| Appium 连接超时 | Connection refused |
检查 Appium Server 是否启动: appium --log-level info |
| iOS 签名过期 | XCUITest 启动失败 | 检查开发者证书有效期;模拟器无需签名 |
| 设备屏幕尺寸差异 | 坐标点击错位 | 用相对坐标: size['width'] // 2 |
| Toast 消息难定位 | 一闪而过,无法截图 | 用 //*[contains(@text,'关键字')] 或加短延时后截图 |
7.3 与 Agent 执行规则的对应关系
| 本文档章节 | 对应 Agent 文件 | 关系 |
|---|---|---|
| §3 Web UI 自动化 | agents/execute/web_executor.md |
本文档是其执行规则的详细实践扩展 |
| §4 App UI 自动化 | agents/execute/mobile_executor.md |
同上 |
| §5.1 用例独立性 | definition_of_done.md §3 |
本文档给出代码层面的实现方式 |
| §3.4 / §4.5 截图策略 | web_executor.md / mobile_executor.md 截图节 |
本文档与 Agent 配置参数对齐 |
| §6 范例 | knowledge_base/03_best_practices/ |
本文档补充了自动化代码视角 |
7.4 参考资源
- Playwright Python API
- Appium Python Client
- Appium UiAutomator2 Driver
- Appium XCUITest Driver
- 本项目:
docs/EXECUTE_SETUP_GUIDE.md— 执行环境搭建指南 - 本项目:
knowledge_base/01_standards/test_case_template.md— 用例编写规范 - 本项目:
knowledge_base/01_standards/definition_of_done.md— 完成定义