Files
zhouqi a94eb5dbbe feat: 新增JoyHub UI自动化测试目录
1. 新增 Joyhub_ui_auto_test/ 目录:
   - tests/ - 测试用例目录
   - pages/ - 页面元素定位
   - config/ - 配置文件
   - utils/ - 工具类
   - test_data/ - 测试数据
   - reports/ - 测试报告
   - webapp-testing/ - WebApp测试相关

2. 配置文件:
   - pytest.ini - pytest配置
   - requirements.txt - 依赖列表
   - README.md - 项目说明
2026-05-13 16:01:25 +08:00

237 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}