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:
@@ -28,24 +28,22 @@ admin:
|
|||||||
storage_state_dir: "tests/web/auth"
|
storage_state_dir: "tests/web/auth"
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# ── 司机端小程序 (Web/Playwright,模拟微信 WebView) ──
|
# ── 司机端小程序 (miniprogram-automator / Node.js) ──
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# 原理: Python pytest → subprocess → node runner.js → miniprogram-automator
|
||||||
|
# 操控微信开发者工具中的小程序,返回 JSON 结果
|
||||||
|
#
|
||||||
|
# 前置条件:
|
||||||
|
# 1. 安装微信开发者工具 + 开启服务端口(设置 → 安全 → 服务端口)
|
||||||
|
# 2. uni-app 编译为微信小程序: npm run build:mp-weixin
|
||||||
|
# 3. 安装依赖: cd tests/web/mini_program/automator && npm install
|
||||||
mini_program:
|
mini_program:
|
||||||
base_url: "https://ybxcx.ynyun8.com:8000/mini" # ⚠️ 请确认小程序 WebView 实际地址
|
# uni-app 编译出的小程序项目路径(.js/.json/.wxml 所在目录)
|
||||||
|
project_path: "E:/test/QaAutomationHub/mini_program/dist" # ⚠️ 请确认实际路径
|
||||||
|
# 微信开发者工具 CLI 路径(Windows 默认如下,macOS 为 /Applications/...)
|
||||||
|
dev_tool_path: "C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat"
|
||||||
timeout_seconds: 120
|
timeout_seconds: 120
|
||||||
screenshot_on_failure: true
|
screenshot_on_failure: true
|
||||||
screenshot_on_step: false
|
|
||||||
browsers:
|
|
||||||
- chromium
|
|
||||||
# 模拟微信内置浏览器 UA
|
|
||||||
user_agent: "Mozilla/5.0 MicroMessenger/8.0.0"
|
|
||||||
viewport:
|
|
||||||
width: 375
|
|
||||||
height: 812
|
|
||||||
|
|
||||||
# 小程序本质是 WebView,同样支持 storage_state 复用
|
|
||||||
skip_login: true
|
|
||||||
storage_state_dir: "tests/web/auth"
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# ── 司机安卓端 (App/Appium) ──
|
# ── 司机安卓端 (App/Appium) ──
|
||||||
@@ -74,8 +72,8 @@ shipper_android:
|
|||||||
skip_login: true
|
skip_login: true
|
||||||
|
|
||||||
device_name: "Android Emulator"
|
device_name: "Android Emulator"
|
||||||
app_package: "com.yunba.shipper" # ⚠️ 请确认实际包名
|
app_package: "com.arpa.ynChengGangShipper" # ⚠️ 请确认实际包名
|
||||||
app_activity: ".MainActivity" # ⚠️ 请确认实际启动 Activity
|
app_activity: "com.arpa.wuche_shipper.MainActivity" # ⚠️ 请确认实际启动 Activity
|
||||||
no_reset: true
|
no_reset: true
|
||||||
new_command_timeout: 120
|
new_command_timeout: 120
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 司机端小程序 — miniprogram-automator
|
||||||
|
|
||||||
|
## 环境搭建
|
||||||
|
|
||||||
|
### 1. 安装微信开发者工具
|
||||||
|
|
||||||
|
- [下载地址](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
|
||||||
|
- 安装后打开**设置 → 安全 → 安全**,开启**服务端口**
|
||||||
|
|
||||||
|
### 2. 安装 Node.js 依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tests/web/mini_program/automator
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 编译 uni-app 为微信小程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 回到项目根目录(即 uni-app 源码所在目录)
|
||||||
|
npm run build:mp-weixin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 配置 test_config.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mini_program:
|
||||||
|
project_path: "E:/path/to/your/uniapp/dist/dev/mp-weixin" # uni-app 编译产物目录
|
||||||
|
dev_tool_path: "C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/web/mini_program/ -v -m p0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 原理
|
||||||
|
|
||||||
|
```
|
||||||
|
pytest 用例
|
||||||
|
↓ conftest.py: subprocess
|
||||||
|
↓
|
||||||
|
node runner.js --action smoke --project-path ...
|
||||||
|
↓ miniprogram-automator API
|
||||||
|
↓
|
||||||
|
微信开发者工具(开启服务端口)
|
||||||
|
↓
|
||||||
|
小程序运行
|
||||||
|
↓
|
||||||
|
返回 JSON { ok, message, screenshot }
|
||||||
|
↓
|
||||||
|
Python 断言
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可用 actions
|
||||||
|
|
||||||
|
| action | 参数 | 说明 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `smoke` | — | 启动小程序并检查首页 |
|
||||||
|
| `navigate` | `page=/pages/xxx/xxx` | 导航到指定页面 |
|
||||||
|
| `element` | `element-action=tap/exists` `selector=.class` | 操作/检查元素 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- **登录**:首次在开发者工具中手动登录,后续 `miniprogram-automator` 启动默认复用登录态
|
||||||
|
- **截图**:输出到 `output/screenshots/mini_program/`
|
||||||
|
- **CI**:微信开发者工具必须安装在 CI 机器上,且需要 GUI 环境(不能纯 headless)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "yunba-miniprogram-automator",
|
||||||
|
"description": "运八司机端小程序自动化测试 — miniprogram-automator 封装",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"test": "node runner.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"miniprogram-automator": "^0.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/** 小程序 Page Object(Node 版)— 首页 */
|
||||||
|
|
||||||
|
class HomePage {
|
||||||
|
constructor(miniProgram) {
|
||||||
|
this.mp = miniProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToOrderList() {
|
||||||
|
const page = await this.mp.currentPage();
|
||||||
|
const el = await page.$(".order-entry, .order-item");
|
||||||
|
if (el) await el.tap();
|
||||||
|
await this.mp.waitFor(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToMine() {
|
||||||
|
const page = await this.mp.currentPage();
|
||||||
|
const el = await page.$(".mine-tab, .my-entry");
|
||||||
|
if (el) await el.tap();
|
||||||
|
await this.mp.waitFor(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async screenshot(name) {
|
||||||
|
const path = require("path");
|
||||||
|
const file = path.resolve(__dirname, `../../../../output/screenshots/mini_program/${name}_${Date.now()}.png`);
|
||||||
|
await this.mp.screenshot({ path: file });
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { HomePage };
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* 运八司机端小程序 — miniprogram-automator 测试执行器。
|
||||||
|
*
|
||||||
|
* 由 Python conftest.py 通过 subprocess 驱动,接收 JSON 指令,返回 JSON 结果。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* node runner.js --action smoke --project-path "E:/xxx/dist"
|
||||||
|
*
|
||||||
|
* 返回 JSON:
|
||||||
|
* { "ok": true/false, "message": "...", "screenshot": "path/to.png" }
|
||||||
|
*/
|
||||||
|
|
||||||
|
const automator = require("miniprogram-automator");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 参数解析
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
function getArg(name) {
|
||||||
|
const idx = args.indexOf(`--${name}`);
|
||||||
|
return idx >= 0 ? args[idx + 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION = getArg("action") || "smoke";
|
||||||
|
const PROJECT_PATH = getArg("project-path") || ".";
|
||||||
|
const DEV_TOOL_PATH =
|
||||||
|
getArg("dev-tool-path") || "C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat";
|
||||||
|
const TIMEOUT = parseInt(getArg("timeout")) || 30000;
|
||||||
|
|
||||||
|
const SCREENSHOTS_DIR = path.resolve(__dirname, "../../../../output/screenshots/mini_program");
|
||||||
|
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 辅助
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
function ok(msg, screenshot) {
|
||||||
|
console.log(JSON.stringify({ ok: true, message: msg, screenshot: screenshot || null }));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(msg, screenshot) {
|
||||||
|
console.error(JSON.stringify({ ok: false, message: msg, screenshot: screenshot || null }));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function screenshot(miniProgram, name) {
|
||||||
|
const file = path.join(SCREENSHOTS_DIR, `${name}_${Date.now()}.png`);
|
||||||
|
await miniProgram.screenshot({ path: file });
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Actions
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async function actionSmoke() {
|
||||||
|
let miniProgram;
|
||||||
|
try {
|
||||||
|
miniProgram = await automator.launch({
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
cliPath: DEV_TOOL_PATH,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等小程序启动
|
||||||
|
await miniProgram.waitFor(5000);
|
||||||
|
|
||||||
|
// 检查是否启动成功
|
||||||
|
const currentPage = await miniProgram.currentPage();
|
||||||
|
if (!currentPage) {
|
||||||
|
return fail("小程序启动后未获取到页面", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证首页元素
|
||||||
|
const elements = await currentPage.$$("view");
|
||||||
|
const ss = await screenshot(miniProgram, "smoke_home");
|
||||||
|
|
||||||
|
return ok(`首页加载成功,检测到 ${elements.length} 个 view 元素`, ss);
|
||||||
|
} catch (e) {
|
||||||
|
return fail(`小程序启动失败: ${e.message}`, null);
|
||||||
|
} finally {
|
||||||
|
if (miniProgram) await miniProgram.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function actionNavigate(pagePath) {
|
||||||
|
let miniProgram;
|
||||||
|
try {
|
||||||
|
miniProgram = await automator.launch({
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
cliPath: DEV_TOOL_PATH,
|
||||||
|
});
|
||||||
|
await miniProgram.waitFor(3000);
|
||||||
|
|
||||||
|
const currentPage = await miniProgram.currentPage();
|
||||||
|
await currentPage.callMethod("navigateTo", { url: pagePath });
|
||||||
|
await miniProgram.waitFor(2000);
|
||||||
|
|
||||||
|
const newPage = await miniProgram.currentPage();
|
||||||
|
const ss = await screenshot(miniProgram, `nav_${pagePath.replace(/\//g, "_")}`);
|
||||||
|
const currentPath = await newPage.path();
|
||||||
|
|
||||||
|
return ok(`导航到 ${pagePath},当前路径: ${currentPath}`, ss);
|
||||||
|
} catch (e) {
|
||||||
|
return fail(`页面导航失败 ${pagePath}: ${e.message}`, null);
|
||||||
|
} finally {
|
||||||
|
if (miniProgram) await miniProgram.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function actionElement(actionType, selector) {
|
||||||
|
let miniProgram;
|
||||||
|
try {
|
||||||
|
miniProgram = await automator.launch({
|
||||||
|
projectPath: PROJECT_PATH,
|
||||||
|
cliPath: DEV_TOOL_PATH,
|
||||||
|
});
|
||||||
|
await miniProgram.waitFor(3000);
|
||||||
|
|
||||||
|
const currentPage = await miniProgram.currentPage();
|
||||||
|
|
||||||
|
if (actionType === "exists") {
|
||||||
|
const el = await currentPage.$(selector);
|
||||||
|
const exists = el !== null;
|
||||||
|
const ss = exists ? null : await screenshot(miniProgram, `missing_${selector}`);
|
||||||
|
return ok(exists ? `元素 "${selector}" 存在` : `元素 "${selector}" 不存在`, ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionType === "tap") {
|
||||||
|
const el = await currentPage.$(selector);
|
||||||
|
if (!el) {
|
||||||
|
return fail(`元素 "${selector}" 不存在,无法点击`, null);
|
||||||
|
}
|
||||||
|
await el.tap();
|
||||||
|
await miniProgram.waitFor(1000);
|
||||||
|
const ss = await screenshot(miniProgram, `tap_${selector}`);
|
||||||
|
return ok(`点击 "${selector}" 成功`, ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fail(`未知子操作: ${actionType}`, null);
|
||||||
|
} catch (e) {
|
||||||
|
return fail(`操作失败 ${actionType}/${selector}: ${e.message}`, null);
|
||||||
|
} finally {
|
||||||
|
if (miniProgram) await miniProgram.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 入口
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
(async () => {
|
||||||
|
switch (ACTION) {
|
||||||
|
case "smoke":
|
||||||
|
return actionSmoke();
|
||||||
|
case "navigate":
|
||||||
|
return actionNavigate(getArg("page") || "/pages/index/index");
|
||||||
|
case "element":
|
||||||
|
return actionElement(getArg("element-action") || "exists", getArg("selector") || "view");
|
||||||
|
default:
|
||||||
|
return fail(`未知 action: ${ACTION}`, null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,52 +1,90 @@
|
|||||||
"""司机端小程序 fixture — Playwright(模拟微信 WebView)。
|
"""司机端小程序 fixture — 通过 subprocess 驱动 miniprogram-automator (Node.js)。
|
||||||
|
|
||||||
核心机制:
|
原理:
|
||||||
模拟微信内置浏览器 UA,微信 WebView 仍需登录态。
|
Python pytest → subprocess → node runner.js → miniprogram-automator
|
||||||
首次使用 python tests/web/admin/save_auth.py 生成后用在此端也可以。
|
→ 微信开发者工具 → 小程序
|
||||||
|
返回 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 datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
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")
|
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
|
@pytest.fixture
|
||||||
def page(mini_program_config, request) -> Page:
|
def mini_program_config(config):
|
||||||
"""微信 WebView 模拟 Page。"""
|
"""小程序配置。"""
|
||||||
viewport = mini_program_config.get("viewport", {"width": 375, "height": 812})
|
return config["mini_program"]
|
||||||
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"]
|
|
||||||
|
|
||||||
with sync_playwright() as p:
|
|
||||||
browser = p.chromium.launch(headless=True)
|
@pytest.fixture
|
||||||
context = browser.new_context(
|
def run_mini_action(mini_program_config):
|
||||||
viewport={"width": viewport["width"], "height": viewport["height"]},
|
"""调用 miniprogram-automator 执行一个操作并返回结果。
|
||||||
user_agent=user_agent,
|
|
||||||
locale="zh-CN",
|
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
|
return _run
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
def save_failure_screenshot(name: str) -> str:
|
||||||
def pytest_runtest_makereport(item, call):
|
"""保存 Python 端的失败截图(如 runner 调用超时)。"""
|
||||||
outcome = yield
|
scenario_dir = SCREENSHOTS_DIR / "mini_program"
|
||||||
rep = outcome.get_result()
|
scenario_dir.mkdir(parents=True, exist_ok=True)
|
||||||
setattr(item, f"rep_{rep.when}", rep)
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
path = str(scenario_dir / f"FAIL_{name}_{ts}.png")
|
||||||
|
# 此层无法截图小程序,创建空标记文件用于日志追溯
|
||||||
|
Path(path).touch()
|
||||||
|
return path
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
"""小程序 Page Object 基类。"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from playwright.sync_api import Page, Locator, expect
|
|
||||||
|
|
||||||
SCREENSHOTS_DIR = Path("output/screenshots")
|
|
||||||
|
|
||||||
|
|
||||||
class MiniProgramBasePage:
|
|
||||||
"""小程序 Page Object 基类。"""
|
|
||||||
|
|
||||||
def __init__(self, page: Page, timeout_ms: int = 30_000):
|
|
||||||
self.page = page
|
|
||||||
self.timeout = timeout_ms
|
|
||||||
|
|
||||||
def wait_visible(self, locator: Locator, timeout_ms: int | None = None) -> None:
|
|
||||||
expect(locator).to_be_visible(timeout=timeout_ms or self.timeout)
|
|
||||||
|
|
||||||
def click_when_ready(self, locator: Locator) -> None:
|
|
||||||
self.wait_visible(locator)
|
|
||||||
locator.click()
|
|
||||||
|
|
||||||
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}_{ts}.png")
|
|
||||||
self.page.screenshot(path=path)
|
|
||||||
return path
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""小程序首页 — Page Object。
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from tests.web.mini_program.pages import MiniProgramBasePage
|
|
||||||
"""
|
|
||||||
|
|
||||||
from playwright.sync_api import Page
|
|
||||||
from tests.web.mini_program.pages import MiniProgramBasePage
|
|
||||||
|
|
||||||
|
|
||||||
class MiniHomePage(MiniProgramBasePage):
|
|
||||||
"""司机端小程序首页。"""
|
|
||||||
|
|
||||||
def __init__(self, page: Page):
|
|
||||||
super().__init__(page)
|
|
||||||
# ⚠️ 定位器需根据实际小程序 UI 确认
|
|
||||||
self.order_list_tab = page.get_by_text("运单")
|
|
||||||
self.mine_tab = page.get_by_text("我的")
|
|
||||||
|
|
||||||
def go_to_order_list(self) -> None:
|
|
||||||
self.click_when_ready(self.order_list_tab)
|
|
||||||
|
|
||||||
def go_to_mine(self) -> None:
|
|
||||||
self.click_when_ready(self.mine_tab)
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
"""小程序冒烟测试。"""
|
"""小程序冒烟测试 — 通过 miniprogram-automator 驱动。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
pytest tests/web/mini_program/ -m p0
|
||||||
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -6,11 +10,34 @@ pytestmark = [pytest.mark.mini_program]
|
|||||||
|
|
||||||
|
|
||||||
class TestMiniProgramSmoke:
|
class TestMiniProgramSmoke:
|
||||||
"""小程序基础验证。"""
|
"""小程序启动 + 基础页面验证。"""
|
||||||
|
|
||||||
@pytest.mark.p0
|
@pytest.mark.p0
|
||||||
@pytest.mark.smoke
|
@pytest.mark.smoke
|
||||||
def test_mini_program_accessible(self, page, mini_program_config):
|
def test_mini_program_launches(self, run_mini_action):
|
||||||
"""验证小程序首页可正常加载。"""
|
"""验证小程序能正常启动并展示首页。"""
|
||||||
page.goto(mini_program_config["base_url"])
|
result = run_mini_action("smoke")
|
||||||
assert page.title(), "小程序首页应显示页面标题"
|
assert result["ok"], f"小程序启动失败: {result.get('message')}"
|
||||||
|
|
||||||
|
@pytest.mark.p0
|
||||||
|
@pytest.mark.smoke
|
||||||
|
def test_navigate_to_pages(self, run_mini_action):
|
||||||
|
"""验证关键页面可正常导航。"""
|
||||||
|
pages = ["/pages/order/index", "/pages/mine/index"]
|
||||||
|
for p in pages:
|
||||||
|
result = run_mini_action("navigate", page=p)
|
||||||
|
assert result["ok"], f"导航到 {p} 失败: {result.get('message')}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMiniProgramElements:
|
||||||
|
"""关键元素验证。"""
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
@pytest.mark.parametrize("selector,label", [
|
||||||
|
pytest.param(".order-entry", "运单入口", id="order_entry"),
|
||||||
|
pytest.param(".mine-tab", "我的tab", id="mine_tab"),
|
||||||
|
])
|
||||||
|
def test_key_element_exists(self, run_mini_action, selector, label):
|
||||||
|
"""验证关键 UI 元素存在。"""
|
||||||
|
result = run_mini_action("element", element_action="exists", selector=selector)
|
||||||
|
assert result["ok"], f"{label} ({selector}) 不存在: {result.get('message')}"
|
||||||
|
|||||||
Reference in New Issue
Block a user