feat(mini_program): 小程序端从 Playwright 切换为 miniprogram-automator
驱动方式: Python pytest → subprocess → node runner.js → miniprogram-automator → 微信开发者工具 架构: tests/web/mini_program/automator/ (Node.js) + conftest.py (Python subprocess 桥接) 前置条件: 微信开发者工具 + 开启服务端口 + npm install + uni-app build:mp-weixin
This commit is contained in:
@@ -1,52 +1,90 @@
|
||||
"""司机端小程序 fixture — Playwright(模拟微信 WebView)。
|
||||
"""司机端小程序 fixture — 通过 subprocess 驱动 miniprogram-automator (Node.js)。
|
||||
|
||||
核心机制:
|
||||
模拟微信内置浏览器 UA,微信 WebView 仍需登录态。
|
||||
首次使用 python tests/web/admin/save_auth.py 生成后用在此端也可以。
|
||||
原理:
|
||||
Python pytest → subprocess → node runner.js → miniprogram-automator
|
||||
→ 微信开发者工具 → 小程序
|
||||
返回 JSON: {"ok": true/false, "message": "...", "screenshot": "path"}
|
||||
|
||||
前置条件:
|
||||
1. 微信开发者工具已安装 + 开启服务端口
|
||||
2. npm install(在 tests/web/mini_program/automator/ 下)
|
||||
3. uni-app 已编译为微信小程序
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import sync_playwright, Page
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
||||
RUNNER = Path(__file__).parent / "automator" / "runner.js"
|
||||
SCREENSHOTS_DIR = Path("output/screenshots")
|
||||
|
||||
|
||||
def _run_runner(project_path: str, action: str, **kwargs) -> dict:
|
||||
"""调用 node runner.js,返回解析后的 JSON。"""
|
||||
cmd = [
|
||||
"node", str(RUNNER),
|
||||
"--action", action,
|
||||
"--project-path", project_path,
|
||||
"--timeout", str(kwargs.get("timeout", 30000)),
|
||||
]
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and key not in ("timeout",):
|
||||
cmd.extend([f"--{key.replace('_', '-')}", str(value)])
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, cwd=str(PROJECT_ROOT))
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout.strip())
|
||||
except json.JSONDecodeError:
|
||||
data = {"ok": False, "message": result.stderr or result.stdout, "screenshot": None}
|
||||
|
||||
data["_raw_stdout"] = result.stdout
|
||||
data["_raw_stderr"] = result.stderr
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(mini_program_config, request) -> Page:
|
||||
"""微信 WebView 模拟 Page。"""
|
||||
viewport = mini_program_config.get("viewport", {"width": 375, "height": 812})
|
||||
timeout = mini_program_config.get("timeout_seconds", 120) * 1000
|
||||
user_agent = mini_program_config.get("user_agent", "Mozilla/5.0 MicroMessenger/8.0.0")
|
||||
base_url = mini_program_config["base_url"]
|
||||
def mini_program_config(config):
|
||||
"""小程序配置。"""
|
||||
return config["mini_program"]
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context(
|
||||
viewport={"width": viewport["width"], "height": viewport["height"]},
|
||||
user_agent=user_agent,
|
||||
locale="zh-CN",
|
||||
|
||||
@pytest.fixture
|
||||
def run_mini_action(mini_program_config):
|
||||
"""调用 miniprogram-automator 执行一个操作并返回结果。
|
||||
|
||||
Usage:
|
||||
result = run_mini_action("smoke")
|
||||
assert result["ok"], result["message"]
|
||||
# result["screenshot"] 是截图路径
|
||||
"""
|
||||
|
||||
def _run(action: str, **kwargs) -> dict:
|
||||
project_path = mini_program_config.get("project_path", ".")
|
||||
dev_tool = mini_program_config.get("dev_tool_path", "")
|
||||
timeout = mini_program_config.get("timeout_seconds", 120) * 1000
|
||||
|
||||
return _run_runner(
|
||||
project_path,
|
||||
action,
|
||||
dev_tool_path=dev_tool,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
page = context.new_page()
|
||||
page.set_default_timeout(timeout)
|
||||
|
||||
yield page
|
||||
|
||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
scenario_dir = SCREENSHOTS_DIR / request.node.name
|
||||
scenario_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
path = str(scenario_dir / f"FAIL_mini_{ts}.png")
|
||||
page.screenshot(path=path)
|
||||
|
||||
context.close()
|
||||
browser.close()
|
||||
return _run
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
setattr(item, f"rep_{rep.when}", rep)
|
||||
def save_failure_screenshot(name: str) -> str:
|
||||
"""保存 Python 端的失败截图(如 runner 调用超时)。"""
|
||||
scenario_dir = SCREENSHOTS_DIR / "mini_program"
|
||||
scenario_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
path = str(scenario_dir / f"FAIL_{name}_{ts}.png")
|
||||
# 此层无法截图小程序,创建空标记文件用于日志追溯
|
||||
Path(path).touch()
|
||||
return path
|
||||
|
||||
Reference in New Issue
Block a user