1. 新增 Joyhub_ui_auto_test/ 目录: - tests/ - 测试用例目录 - pages/ - 页面元素定位 - config/ - 配置文件 - utils/ - 工具类 - test_data/ - 测试数据 - reports/ - 测试报告 - webapp-testing/ - WebApp测试相关 2. 配置文件: - pytest.ini - pytest配置 - requirements.txt - 依赖列表 - README.md - 项目说明
237 lines
7.5 KiB
Python
237 lines
7.5 KiB
Python
import re
|
||
import shutil
|
||
import subprocess
|
||
import time
|
||
from pathlib import Path
|
||
|
||
import allure
|
||
import pytest
|
||
from config.settings import settings
|
||
from playwright.sync_api import Error as PlaywrightError
|
||
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||
from playwright.sync_api import expect
|
||
from utils.logger import get_logger
|
||
|
||
logger = get_logger()
|
||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||
ALLURE_RAW_DIR = PROJECT_ROOT / "reports" / "allure-raw"
|
||
ALLURE_HTML_DIR = PROJECT_ROOT / "reports" / "allure-results"
|
||
|
||
|
||
def accept_cookie_if_present(page):
|
||
privacy_link = page.locator('a[href="/privacy-policy-web"]').last
|
||
try:
|
||
if privacy_link.is_visible(timeout=1_000):
|
||
cookie_container = privacy_link.locator("xpath=ancestor::div[.//button][1]")
|
||
cookie_container.locator("button").first.click()
|
||
return
|
||
except PlaywrightTimeoutError:
|
||
pass
|
||
|
||
accept_button = page.locator("button", has_text=re.compile(r"Acce?pet|Accept", re.I)).first
|
||
try:
|
||
if accept_button.is_visible(timeout=1_000):
|
||
accept_button.click()
|
||
except PlaywrightTimeoutError:
|
||
logger.info("未检测到 Cookie 操作区域,继续执行")
|
||
except PlaywrightError as exc:
|
||
logger.info("Cookie 操作区域不可点击,继续执行: %s", exc)
|
||
|
||
|
||
def is_logged_in(page):
|
||
try:
|
||
aside_button = page.locator("aside button").first
|
||
if not aside_button.is_visible(timeout=3_000):
|
||
return False
|
||
button_text = aside_button.inner_text(timeout=3_000).strip()
|
||
return bool(re.search(r"Logout|Log out", button_text, re.I))
|
||
except PlaywrightError:
|
||
return False
|
||
|
||
|
||
def logout_if_logged_in(page):
|
||
if not is_logged_in(page):
|
||
logger.info("当前为游客状态,无需 Logout")
|
||
return
|
||
|
||
logger.info("当前已登录,点击 Logout 切换为游客状态")
|
||
page.locator("aside button").first.click()
|
||
expect(page.locator("aside button").first).not_to_contain_text(re.compile(r"Logout|Log out", re.I), timeout=settings.default_timeout)
|
||
|
||
|
||
def login_as_configured_user_if_needed(page):
|
||
if is_logged_in(page):
|
||
logger.info("当前已登录,跳过重复登录")
|
||
return
|
||
|
||
logger.info("当前未登录,执行配置用户登录")
|
||
login_inputs = page.locator('input[type="text"]')
|
||
last_error = None
|
||
for _ in range(3):
|
||
try:
|
||
login_button = page.locator("aside button").first
|
||
expect(login_button).to_be_visible(timeout=settings.default_timeout)
|
||
login_button.click()
|
||
expect(login_inputs.first).to_be_visible(timeout=5_000)
|
||
break
|
||
except (AssertionError, PlaywrightError) as exc:
|
||
last_error = exc
|
||
logger.info("登录弹窗未打开,重试点击登录入口: %s", exc)
|
||
page.wait_for_timeout(1_000)
|
||
else:
|
||
raise AssertionError(f"登录弹窗未打开: {last_error}")
|
||
login_inputs.nth(0).fill(settings.login_email)
|
||
login_inputs.nth(1).fill(settings.verification_code)
|
||
|
||
confirmation_items = page.locator("div.inline-flex.cursor-pointer")
|
||
expect(confirmation_items.nth(0)).to_be_visible(timeout=settings.default_timeout)
|
||
expect(confirmation_items.nth(1)).to_be_visible(timeout=settings.default_timeout)
|
||
confirmation_items.nth(0).click()
|
||
confirmation_items.nth(1).click()
|
||
|
||
submit_button = page.locator("button").last
|
||
expect(submit_button).to_be_enabled(timeout=settings.default_timeout)
|
||
submit_button.click()
|
||
|
||
expect(page.locator("aside button").first).to_contain_text(
|
||
re.compile(r"Logout|Log out", re.I), timeout=settings.default_timeout
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def ensure_guest_user(page):
|
||
def _ensure_guest_user():
|
||
accept_cookie_if_present(page)
|
||
logout_if_logged_in(page)
|
||
|
||
return _ensure_guest_user
|
||
|
||
|
||
@pytest.fixture
|
||
def ensure_logged_in_user(page):
|
||
def _ensure_logged_in_user():
|
||
accept_cookie_if_present(page)
|
||
login_as_configured_user_if_needed(page)
|
||
|
||
return _ensure_logged_in_user
|
||
|
||
|
||
def pytest_sessionstart(session):
|
||
for report_dir in (ALLURE_RAW_DIR, ALLURE_HTML_DIR):
|
||
if report_dir.exists():
|
||
shutil.rmtree(report_dir)
|
||
report_dir.mkdir(parents=True, exist_ok=True)
|
||
logger.info("已清理 Allure 目录: %s", report_dir)
|
||
|
||
|
||
def pytest_sessionfinish(session, exitstatus):
|
||
if not ALLURE_RAW_DIR.exists() or not any(ALLURE_RAW_DIR.iterdir()):
|
||
logger.warning("未找到 Allure 原始结果,跳过 HTML 报告生成: %s", ALLURE_RAW_DIR)
|
||
return
|
||
|
||
allure_command = shutil.which("allure") or shutil.which("allure.cmd")
|
||
if not allure_command:
|
||
logger.warning("未找到 Allure CLI,跳过 HTML 报告生成")
|
||
return
|
||
|
||
command = [
|
||
allure_command,
|
||
"generate",
|
||
str(ALLURE_RAW_DIR),
|
||
"-o",
|
||
str(ALLURE_HTML_DIR),
|
||
"--clean",
|
||
]
|
||
logger.info("开始生成 Allure HTML 报告: %s", " ".join(command))
|
||
result = subprocess.run(
|
||
command,
|
||
cwd=PROJECT_ROOT,
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
)
|
||
if result.returncode != 0:
|
||
logger.error("Allure HTML 报告生成失败: %s", result.stderr.strip())
|
||
return
|
||
logger.info("Allure HTML 报告已生成: %s", ALLURE_HTML_DIR / "index.html")
|
||
|
||
|
||
def _safe_attachment_name(name):
|
||
return re.sub(r"[^0-9A-Za-z_.\-\u4e00-\u9fff]+", "_", name).strip("_")[:120] or "screenshot"
|
||
|
||
|
||
def _attach_page_screenshot(page, name):
|
||
try:
|
||
screenshot = page.screenshot(full_page=True)
|
||
allure.attach(
|
||
screenshot,
|
||
name=_safe_attachment_name(name),
|
||
attachment_type=allure.attachment_type.PNG,
|
||
)
|
||
except PlaywrightError as exc:
|
||
logger.info("Allure 截图附件生成失败: %s", exc)
|
||
|
||
|
||
@pytest.fixture
|
||
def attach_page_screenshot(page):
|
||
def _attach(name="页面截图"):
|
||
_attach_page_screenshot(page, name)
|
||
|
||
return _attach
|
||
|
||
|
||
@pytest.hookimpl(hookwrapper=True)
|
||
def pytest_runtest_makereport(item, call):
|
||
outcome = yield
|
||
report = outcome.get_result()
|
||
setattr(item, "rep_" + report.when, report)
|
||
|
||
if report.when != "call" or "page" not in item.fixturenames:
|
||
return
|
||
|
||
page = item.funcargs.get("page")
|
||
if not page:
|
||
return
|
||
|
||
if report.failed:
|
||
_attach_page_screenshot(page, f"失败截图_{item.name}")
|
||
elif report.passed:
|
||
_attach_page_screenshot(page, f"结束截图_{item.name}")
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def test_run_logger(request):
|
||
case_name = request.node.nodeid
|
||
start_time = time.perf_counter()
|
||
logger.info("用例开始: %s", case_name)
|
||
|
||
try:
|
||
yield
|
||
except Exception:
|
||
elapsed = time.perf_counter() - start_time
|
||
logger.exception("用例异常: %s, 耗时: %.2fs", case_name, elapsed)
|
||
raise
|
||
else:
|
||
elapsed = time.perf_counter() - start_time
|
||
logger.info("用例通过: %s, 耗时: %.2fs", case_name, elapsed)
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def browser_context_args(browser_context_args):
|
||
return {
|
||
**browser_context_args,
|
||
"base_url": settings.base_url,
|
||
"viewport": {"width": settings.viewport_width, "height": settings.viewport_height},
|
||
}
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def browser_type_launch_args(browser_type_launch_args):
|
||
return {
|
||
**browser_type_launch_args,
|
||
"headless": True,
|
||
# 可视化执行配置:"headless": False, "slow_mo": 300,
|
||
"timeout": settings.default_timeout,
|
||
}
|