b822428a2a
驱动方式: 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
165 lines
6.1 KiB
JavaScript
165 lines
6.1 KiB
JavaScript
/**
|
|
* 运八司机端小程序 — 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);
|
|
}
|
|
})();
|