如何使用
简介:
本示例展示如何通过 Total Control 在多台 Android 设备上同步执行同一套 App 操作流程。脚本可自动完成应用启动、前台校验、点击、输入、翻页、返回主页及关闭应用,并在执行过程中记录步骤耗时、自动截图并保存失败现场,同时采集关键运行上下文信息,适合用于多设备自动化测试、批量操作及新手学习参考。
运行前:
sigmaLoad("D:/scripts/multi_device_flow.js");
源代码
/**
* ============================================================
* 多设备统一执行一套 App 操作流程(可直接复制运行)
* ============================================================
* 对每台设备执行:启动 App → 校验前台 → 点击 → 输入 → 翻页 → 返回主页 → 关闭 App
*
* 1) 获取设备:只用 getDevices()(来自 sigma/device)
* 2) 每一步都打印:开始 / 结束 / 耗时(stepRun)
* 3) 失败自动截图(sigma/image -> device.screenshot)
* 4) 失败自动写上下文:前台包名 + activity + 电量 + 设备信息(writeFailContext)
* 5) 代码内含“详细注释文档”,方便直接拷贝给新人学习/排错
*
* ------------------------------------------------------------
* 模块职责
* - device : 获取设备、读取设备属性
* - app : runApp/isAppForeground/getForegroundApp/getActivity/closeApp
* - input : click/click2/send(系统按键 HOME/BACK/ENTER)/scroll/swipe 等
* - navKeys: pgUp/pgDn/up/down/shiftPgDn...(翻页/方向移动,非系统键)
* - image : screenshot/screenshotToMemory/seekImage 等(这里用于失败留证截图)
* - tests : inputText/inputForm/getClipboardText(本版本用于“输入”)
* ------------------------------------------------------------
*
* 运行前提:
* - 在 Total Control UI 中先“选中/勾选”你要操作的设备
* - getDevices() 会返回这些“已选设备”
*
* 常见调参位置(新手只需改这里):
* - TARGET_PKG 目标 App 包名
* - SEARCH_TEXT 输入内容(教学建议先用英文/数字)
* - SEARCH_X/Y 搜索框相对坐标(0~1)
* - LOG_DIR 日志/截图输出目录
* - WAIT_LAUNCH_MS App 启动等待时间
*/
// ===================== 引入与注入 =====================
var tcConst = global.tcConst || {};
var sigmaConst = global.sigmaConst || {};
// device:只用 getDevices() 获取设备(按你的要求)
var { getDevices, Device } = require("sigma/device");
// app/input/navKeys/image/tests 都会挂载到 Device.prototype
// 所以 require 后就能直接 device.runApp()/device.click2()/device.pgDn()/device.screenshot()/device.inputText() ...
require("sigma/app");
require("sigma/input");
require("sigma/navKeys");
require("sigma/image");
require("sigma/tests"); // 新增:tests 输入模块(inputText/inputForm/getClipboardText)
// ===================== 可配置区 =====================
// 目标包名(按需替换)
// var TARGET_PKG = "com.tencent.qqmusic";
var TARGET_PKG = "com.sigma_rt.sigmatestapp";
// 输入内容
var SEARCH_TEXT = "500";
// 启动等待(设备性能不同,建议 2000~6000ms)
var WAIT_LAUNCH_MS = 2500;
// 搜索框相对坐标:0~1(跨分辨率更稳定)
// var SEARCH_X = 0.5;
// var SEARCH_Y = 0.12;
var SEARCH_X = 0.5352;
var SEARCH_Y = 0.135;
// 日志/截图输出目录(按你的电脑改)
var LOG_DIR = "D:/tc_logs";
// 图片格式常量:优先 JPG(小),没有就用 PNG
// 注意:你们环境可能使用 sigmaConst.IMG_JPG 或 tcConst.IMG_JPG
var IMG_TYPE = (sigmaConst.IMG_JPG || tcConst.IMG_JPG || sigmaConst.IMG_PNG || tcConst.IMG_PNG);
// ===================== 通用工具函数 =====================
/** 返回当前毫秒时间戳(用于耗时统计) */
function nowMs() { return Date.now(); }
function sleep(ms) {
if (typeof global.sleep === "function") return global.sleep(ms);
var t = Date.now();
while (Date.now() - t < ms) {}
}
/** 打印用的设备名(优先 name,其次 getName,再次 SN) */
function devName(d) {
return d.name || (d.getName && d.getName()) || d.SN || "UnknownDevice";
}
/** 两位补零(用于时间戳格式化) */
function pad2(n) { return (n < 10 ? "0" : "") + n; }
/** 生成 YYYYMMDD_HHMMSS 时间戳(用于日志/截图文件名) */
function ts() {
var d = new Date();
return (
d.getFullYear() +
pad2(d.getMonth() + 1) +
pad2(d.getDate()) + "_" +
pad2(d.getHours()) +
pad2(d.getMinutes()) +
pad2(d.getSeconds())
);
}
/** 把字符串转成安全文件名片段(去掉非法字符) */
function safeFilePart(s) {
return String(s).replace(/[^\w\-.]+/g, "_");
}
// ===================== 文件写入:有则落盘,无则退化仅控制台 =====================
var fs = null;
try { fs = require("fs"); } catch (e) { fs = null; }
/** 尝试创建目录(支持则创建,不支持则返回 false) */
function ensureDir(path) {
if (!fs) return false;
try {
if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive: true });
return true;
} catch (e) {
return false;
}
}
/** 追加写文件(支持则写入,不支持则返回 false) */
function appendFile(path, text) {
if (!fs) return false;
try {
fs.appendFileSync(path, text, { encoding: "utf8" });
return true;
} catch (e) {
return false;
}
}
// ===================== 关键:步骤执行器(开始/结束/耗时) =====================
// 目的:让业务步骤统一输出格式;任何步骤异常都能精准定位。
// 使用方式:stepRun(ctx, "StepName", function(){ ... })
function stepRun(ctx, stepName, fn) {
var start = nowMs();
ctx.log(">> START " + stepName);
try {
var ret = fn();
var cost = nowMs() - start;
ctx.log("<< END " + stepName + " (cost " + cost + " ms)");
return { ok: true, ret: ret, cost: cost };
} catch (e) {
var cost2 = nowMs() - start;
ctx.log("<< FAIL " + stepName + " (cost " + cost2 + " ms) err=" + String(e && e.message ? e.message : e));
return { ok: false, err: String(e && e.message ? e.message : e), cost: cost2 };
}
}
// ===================== input:系统键 =====================
/**
* press:发送系统按键(HOME/BACK/ENTER 等)
* - input.send(code, state) -> 成功返回 0,失败返回 -1(用 lastError() 查看原因)
*/
function press(d, keyConst) {
var ret = d.send(keyConst, tcConst.STATE_PRESS);
if (ret !== 0) throw new Error("send failed: " + lastError());
}
// ===================== 失败处理:上下文采集 + 截图 =====================
/** 安全取值:防止某些属性/方法在个别设备上报错导致二次失败 */
function safeGet(fn, fallback) {
try { return fn(); } catch (e) { return fallback; }
}
/**
* 失败上下文写入日志:
* - foregroundApp / activity
* - battery
* - manufacturer/model/SN/IP/DPI/resolution/androidVersion...
*/
function writeFailContext(ctx, d, stepName, errText) {
ctx.log("---- FAIL CONTEXT BEGIN ----");
ctx.log("step=" + stepName);
ctx.log("error=" + (errText || ""));
// 前台包名/Activity(app 模块方法,注入到 device)
var fg = safeGet(function () { return d.getForegroundApp(); }, null);
var act = safeGet(function () { return d.getActivity(); }, null);
ctx.log("foregroundApp=" + (fg || ""));
ctx.log("activity=" + (act || ""));
// 电量(device 属性)
var battery = safeGet(function () { return d.battery; }, null);
ctx.log("battery=" + (battery !== null && battery !== undefined ? battery : ""));
// 设备基础信息(device 属性)
ctx.log("name=" + safeGet(function () { return d.name; }, ""));
ctx.log("manufacturer=" + safeGet(function () { return d.manufacturer; }, ""));
ctx.log("model=" + safeGet(function () { return d.model; }, ""));
ctx.log("SN=" + safeGet(function () { return d.SN; }, ""));
ctx.log("IP=" + safeGet(function () { return d.IP; }, ""));
ctx.log("DPI=" + safeGet(function () { return d.DPI; }, ""));
ctx.log("resolution=" + safeGet(function () { return d.width; }, "") + "x" + safeGet(function () { return d.height; }, ""));
ctx.log("androidVersionRelease=" + safeGet(function () { return d.androidVersionRelease; }, ""));
ctx.log("androidVersionSdkInt=" + safeGet(function () { return d.androidVersionSdkInt; }, ""));
ctx.log("---- FAIL CONTEXT END ----");
}
/**
* 失败截图:
* - 使用 image 模块注入的 device.screenshot(filePath, imageType)
* - 文件名包含:设备名 + step + 时间戳
*/
function takeFailScreenshot(ctx, d, stepName) {
var file = LOG_DIR + "/" + safeFilePart(ctx.deviceName) + "__" + safeFilePart(stepName) + "__" + ts() + ".jpg";
var ret = d.screenshot(file, IMG_TYPE);
if (ret === 0) ctx.log("!! Screenshot saved: " + file);
else ctx.log("!! Screenshot failed: " + lastError());
}
// ===================== 设备级上下文(日志对象) =====================
function makeCtx(d) {
var name = devName(d);
var logFile = LOG_DIR + "/" + safeFilePart(name) + "__" + ts() + ".log";
var canWrite = ensureDir(LOG_DIR);
return {
deviceName: name,
logFile: logFile,
canWrite: canWrite && !!fs,
log: function (line) {
var msg = "[" + name + "] " + line;
print(msg);
if (this.canWrite) appendFile(this.logFile, msg + "\n");
}
};
}
// ===================== 单设备流程(核心业务) =====================
function runOnDevice(d) {
var ctx = makeCtx(d);
ctx.log("=== DEVICE BEGIN ===");
ctx.log("info: " +
safeGet(function(){return d.manufacturer;}, "") + " " +
safeGet(function(){return d.model;}, "") + " " +
safeGet(function(){return d.width;}, "") + "x" +
safeGet(function(){return d.height;}, "")
);
var result = { ok: false, step: "", err: "", fg: "", act: "" };
function failAt(stepName, errText) {
result.ok = false;
result.step = stepName;
result.err = errText || "";
// 失败上下文 + 截图
writeFailContext(ctx, d, stepName, errText);
takeFailScreenshot(ctx, d, stepName);
ctx.log("=== DEVICE END (FAIL) ===");
return result;
}
// ---------------- Step 1:启动 App ----------------
var s1 = stepRun(ctx, "Step1 app.runApp(" + TARGET_PKG + ")", function () {
var r = d.runApp(TARGET_PKG);
if (r !== 0) throw new Error(lastError());
// 启动后等待稳定
sleep(WAIT_LAUNCH_MS);
return r;
});
if (!s1.ok) return failAt("runApp", s1.err);
// ---------------- Step 2:校验前台 + 读 Activity ----------------
var s2 = stepRun(ctx, "Step2 app.isAppForeground + getForegroundApp/getActivity", function () {
var r = d.isAppForeground(TARGET_PKG);
if (r !== 0) throw new Error("not foreground: " + lastError());
result.fg = d.getForegroundApp() || "";
result.act = d.getActivity() || "";
ctx.log("foregroundApp=" + result.fg);
ctx.log("activity=" + result.act);
return 0;
});
if (!s2.ok) return failAt("foregroundCheck", s2.err);
// ---------------- Step 3:点击搜索框(input.click2) ----------------
var s3 = stepRun(ctx, "Step3 input.click2(searchBox)", function () {
var r = d.click2(SEARCH_X, SEARCH_Y, { dx: 0.08, dy: 0.04 });
if (r !== 0) throw new Error(lastError());
sleep(300);
return r;
});
if (!s3.ok) return failAt("clickSearch", s3.err);
// ---------------- Step 4:输入(tests.inputText)+ 回车 ----------------
// 修改点:用 tests 模块输入,不再用逐字符 KEYCODE
// 说明:
// - d.inputText(content) 往“当前焦点输入框”输入
// - 末尾加 '\n' 等价“回车/提交”
var s4 = stepRun(ctx, "Step4 tests.inputText(SEARCH_TEXT + \\n)", function () {
var r = d.inputText(SEARCH_TEXT + "\n");
if (r !== 0) throw new Error("inputText failed: " + lastError());
sleep(1000);
return 0;
});
if (!s4.ok) return failAt("typeEnter", s4.err);
// ---------------- Step 5:翻页(navKeys) ----------------
var s5 = stepRun(ctx, "Step5 navKeys.pgDn + navKeys.shiftPgDn", function () {
var r1 = d.pgDn();
if (r1 !== 0) ctx.log("WARN pgDn ret=" + r1);
sleep(250);
var r2 = d.shiftPgDn();
if (r2 !== 0) ctx.log("WARN shiftPgDn ret=" + r2);
sleep(400);
return 0;
});
if (!s5.ok) return failAt("navKeysPaging", s5.err);
// ---------------- Step 6:返回 + 主页(系统键 -> input.send) ----------------
var s6 = stepRun(ctx, "Step6 input.send(KEY_BACK) + input.send(KEY_HOME)", function () {
press(d, tcConst.KEY_BACK);
sleep(200);
press(d, tcConst.KEY_HOME);
sleep(400);
return 0;
});
if (!s6.ok) return failAt("backHome", s6.err);
// ---------------- Step 7:关闭 App ----------------
var s7 = stepRun(ctx, "Step7 app.closeApp(" + TARGET_PKG + ")", function () {
var r = d.closeApp(TARGET_PKG);
if (r !== 0) ctx.log("WARN closeApp ret=" + r + " err=" + lastError());
return r;
});
if (!s7.ok) return failAt("closeApp", s7.err);
// 全流程成功
result.ok = true;
result.step = "done";
ctx.log("=== DEVICE END (OK) ===");
return result;
}
// ===================== main:只用 getDevices() 获取设备 =====================
function main() {
if (Device.connectAll) Device.connectAll();
// 用 getDevices()
var devices = getDevices();
if (!devices || devices.length === 0) {
print("No selected devices. Please select devices first.");
return;
}
// 尝试创建日志目录
ensureDir(LOG_DIR);
print("Selected devices: " + devices.length);
var ok = 0;
for (var i = 0; i < devices.length; i++) {
var d = devices[i];
var name = devName(d);
print("\n==============================");
print("RUN " + (i + 1) + "/" + devices.length + " : " + name);
print("==============================");
var r = runOnDevice(d);
if (r.ok) {
ok++;
print("[" + name + "] OK");
} else {
print("[" + name + "] FAIL @" + r.step + " | " + r.err);
}
}
print("\n===== SUMMARY =====");
print("Success: " + ok + " / " + devices.length);
}
main();