/** * 运八司机端小程序 — 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); } })();