多设备统一执行 App 操作流程


更多信息请参阅 JavaScript Modules


如何使用

简介:

本示例展示如何通过 Total Control 在多台 Android 设备上同步执行同一套 App 操作流程。脚本可自动完成应用启动、前台校验、点击、输入、翻页、返回主页及关闭应用,并在执行过程中记录步骤耗时、自动截图并保存失败现场,同时采集关键运行上下文信息,适合用于多设备自动化测试、批量操作及新手学习参考。

运行前:

  1. 下载并安装 Total Control 11.0(Update 20)及以上版本(下载
  2. 连接 Android 设备(支持 USB 连接 TCP 连接
  3. 在 Android 设备上安装 SigmaTestApp,作为测试应用使用 (如何安装
  4. 在 Total Control 界面中勾选测试设备 (请注意:脚本中的 getDevices() 只会返回当前已选中的设备)
  5. 将示例代码保存为一个 .js 文件(例如 MultiDeviceFlow.js),并放置到 Total Control 默认脚本目录
  6. 根据实际测试场景,调整以下配置项:
    • TARGET_PKG:目标 App 包名
    • SEARCH_TEXT:输入内容
    • SEARCH_X/Y:搜索框相对坐标(0~1)
    • LOG_DIR:日志/截图输出目录
    • WAIT_LAUNCH_MS App:启动等待时间
  7. 打开脚本终端(主面板 → 脚本 → 脚本终端),执行:
    >> sigmaLoad("multiDeviceFlow.js");


源代码

/**
 * 多设备统一执行 App 操作流程(可直接复制运行)
 * 启动 → 校验前台 → 点击 → 输入 → 翻页 → 返回主页 → 关闭
 */

// ===================== 导入与注入 =====================
var { getDevices, Device } = require("sigma/device");

// ===================== 配置 =====================
var TARGET_PKG = "com.sigma_rt.sigmatestapp";
var SEARCH_TEXT = "500";
var WAIT_LAUNCH_MS = 2500;
var SEARCH_X = 0.5352;
var SEARCH_Y = 0.135;
var LOG_DIR = "C:/tc_logs";
var IMG_TYPE = tcConst.IMG_JPG || tcConst.IMG_PNG; // 如果 tcConst 中包含这些常量;否则可保留 sigmaConst

// ===================== fs 辅助函数(可选) =====================
var fs = null;

try {
    fs = require("fs");
} catch (e) {
    fs = null;
}

function ensureDir(path) {
    if (!fs) return false;

    try {
        if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive: true });
        return true;
    } catch (e) {
        return false;
    }
}

function appendFile(path, text) {
    if (!fs) return false;

    try {
        fs.appendFileSync(path, text, { encoding: "utf8" });
        return true;
    } catch (e) {
        return false;
    }
}

// ===================== 小工具函数 =====================
function nowMs() {
    return Date.now();
}

function ts() {
    // fewer helpers: build a compact timestamp quickly
    var d = new Date();
    function p(n) {
        return (n < 10 ? "0" : "") + n;
    }

    return (
        d.getFullYear() +
        p(d.getMonth() + 1) +
        p(d.getDate()) +
        "_" +
        p(d.getHours()) +
        p(d.getMinutes()) +
        p(d.getSeconds())
    );
}

function safeFilePart(s) {
    return String(s).replace(/[^\w\-.]+/g, "_");
}

function devName(d) {
    return d.name || (d.getName && d.getName()) || d.SN || "UnknownDevice";
}

// 安全获取属性/方法的包装器
function safe(fn, fallback) {
    try {
        return fn();
    } catch (e) {
        return fallback;
    }
}

// 系统按键发送封装
function press(d, keyConst) {
    var ret = d.send(keyConst, tcConst.STATE_PRESS);
    if (ret !== 0) throw new Error("send failed: " + lastError());
}

// 步骤封装(计时 + 统一日志)
function stepRun(ctx, name, fn) {
    var t0 = nowMs();
    ctx.log(">> START  " + name);

    try {
        var ret = fn();
        ctx.log("<< END    " + name + "  (cost " + (nowMs() - t0) + " ms)");
        return { ok: true, ret: ret };
    } catch (e) {
        var err = String(e && e.message ? e.message : e);
        ctx.log(
            "<< FAIL   " +
                name +
                "  (cost " +
                (nowMs() - t0) +
                " ms) err=" +
                err,
        );

        return { ok: false, err: err };
    }
}

// 单步执行或快速失败的辅助函数
function runStepOrFail(ctx, d, result, stepKey, stepName, fn) {
    var s = stepRun(ctx, stepName, fn);
    if (s.ok) return true;
    return failAt(ctx, d, result, stepKey, s.err);
}

// 失败上下文信息(数据驱动,减少重复)
function writeFailContext(ctx, d, stepKey, errText) {
    ctx.log("---- FAIL CONTEXT BEGIN ----");
    ctx.log("step=" + stepKey);
    ctx.log("error=" + (errText || ""));

    // 前台应用信息
    ctx.log("foregroundApp=" + (safe(() => d.getForegroundApp(), "") || ""));
    ctx.log("activity=" + (safe(() => d.getActivity(), "") || ""));

    // 常用设备字段(可在此增删)
    var fields = [
        ["battery", () => d.battery],
        ["name", () => d.name],
        ["manufacturer", () => d.manufacturer],
        ["model", () => d.model],
        ["SN", () => d.SN],
        ["IP", () => d.IP],
        ["DPI", () => d.DPI],
        ["resolution", () => d.width + "x" + d.height],
        ["androidVersionRelease", () => d.androidVersionRelease],
        ["androidVersionSdkInt", () => d.androidVersionSdkInt],
    ];

    for (var i = 0; i < fields.length; i++) {
        var k = fields[i][0];
        var v = safe(fields[i][1], "");
        ctx.log(k + "=" + (v === undefined || v === null ? "" : String(v)));
    }

    ctx.log("---- FAIL CONTEXT END ----");
}

function takeFailScreenshot(ctx, d, stepKey) {
    var file =
        LOG_DIR +
        "/" +
        safeFilePart(ctx.deviceName) +
        "__" +
        safeFilePart(stepKey) +
        "__" +
        ts() +
        ".jpg";
    var ret = d.screenshot(file, IMG_TYPE);
    if (ret === 0) ctx.log("!! Screenshot saved: " + file);
    else ctx.log("!! Screenshot failed: " + lastError());
}

// 统一失败处理器(避免重复代码)
function failAt(ctx, d, result, stepKey, errText) {
    result.ok = false;
    result.step = stepKey;
    result.err = errText || "";
    writeFailContext(ctx, d, stepKey, errText);
    takeFailScreenshot(ctx, d, stepKey);
    ctx.log("=== DEVICE END (FAIL) ===");
    return false;
}

// 单设备日志上下文
function makeCtx(d) {
    var name = devName(d);
    var canWrite = ensureDir(LOG_DIR) && !!fs;
    var logFile = LOG_DIR + "/" + safeFilePart(name) + "__" + ts() + ".log";
    return {
        deviceName: name,
        logFile: logFile,
        canWrite: canWrite,
        log: function (line) {
            var msg = "[" + name + "] " + line;
            print(msg);
            if (this.canWrite) appendFile(this.logFile, msg + "\n");
        },
    };
}

// ===================== 单设备执行流程 =====================
function runOnDevice(d) {
    var ctx = makeCtx(d);
    var result = { ok: false, step: "", err: "", fg: "", act: "" };
    ctx.log("=== DEVICE BEGIN ===");
    ctx.log(
        "info: " +
            safe(() => d.manufacturer, "") +
            " " +
            safe(() => d.model, "") +
            " " +
            safe(() => d.width, "") +
            "x" +
            safe(() => d.height, ""),
    );

    // 1) 启动应用
    if (
        !runStepOrFail(
            ctx,
            d,
            result,
            "runApp",
            "Step1 app.runApp(" + TARGET_PKG + ")",
            function () {
                var r = d.runApp(TARGET_PKG);
                if (r !== 0) throw new Error(lastError());
                sleep(WAIT_LAUNCH_MS);
                return 0;
            },
        )
    )
        return result;

    // 2) 前台校验
    if (
        !runStepOrFail(
            ctx,
            d,
            result,
            "foregroundCheck",
            "Step2 verify foreground + 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;
            },
        )
    )
        return result;

    // 3) 点击搜索框
    if (
        !runStepOrFail(
            ctx,
            d,
            result,
            "clickSearch",
            "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 0;
            },
        )
    )
        return result;

    // 4) 输入并回车
    if (
        !runStepOrFail(
            ctx,
            d,
            result,
            "typeEnter",
            "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;
            },
        )
    )
        return result;

    // 5) 翻页
    if (
        !runStepOrFail(
            ctx,
            d,
            result,
            "navKeysPaging",
            "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;
            },
        )
    )
        return result;

    // 6) 返回 + 主页
    if (
        !runStepOrFail(
            ctx,
            d,
            result,
            "backHome",
            "Step6 send(KEY_BACK) + send(KEY_HOME)",
            function () {
                press(d, tcConst.KEY_BACK);
                sleep(200);
                press(d, tcConst.KEY_HOME);
                sleep(400);
                return 0;
            },
        )
    )
        return result;

    // 7) 关闭应用(失败不致命,但仍作为一个步骤)
    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 0;
    });

    result.ok = true;
    result.step = "done";
    ctx.log("=== DEVICE END (OK) ===");
    return result;
}

// ===================== 主入口 =====================
function main() {
    if (Device.connectAll) Device.connectAll();
    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();
		

运行结果

Selected devices: 2

==============================
RUN 1/2 : SAMSUNG-SM-G975U1
==============================
[SAMSUNG-SM-G975U1] === DEVICE BEGIN ===
[SAMSUNG-SM-G975U1] info: samsung SM-G975U1 1440x3040
[SAMSUNG-SM-G975U1] >> START  Step1 app.runApp(com.sigma_rt.sigmatestapp)
[SAMSUNG-SM-G975U1] << END    Step1 app.runApp(com.sigma_rt.sigmatestapp)  (cost 2662 ms)
[SAMSUNG-SM-G975U1] >> START  Step2 verify foreground + getForegroundApp/getActivity
[SAMSUNG-SM-G975U1] foregroundApp=com.sigma_rt.sigmatestapp
[SAMSUNG-SM-G975U1] activity=com.sigma_rt.sigmatestapp/.MainActivity
[SAMSUNG-SM-G975U1] << END    Step2 verify foreground + getForegroundApp/getActivity  (cost 734 ms)
[SAMSUNG-SM-G975U1] >> START  Step3 input.click2(searchBox)
[SAMSUNG-SM-G975U1] << END    Step3 input.click2(searchBox)  (cost 349 ms)
[SAMSUNG-SM-G975U1] >> START  Step4 tests.inputText(SEARCH_TEXT + \n)
[SAMSUNG-SM-G975U1] << END    Step4 tests.inputText(SEARCH_TEXT + \n)  (cost 1031 ms)
[SAMSUNG-SM-G975U1] >> START  Step5 navKeys.pgDn + navKeys.shiftPgDn
[SAMSUNG-SM-G975U1] << END    Step5 navKeys.pgDn + navKeys.shiftPgDn  (cost 693 ms)
[SAMSUNG-SM-G975U1] >> START  Step6 send(KEY_BACK) + send(KEY_HOME)
[SAMSUNG-SM-G975U1] << END    Step6 send(KEY_BACK) + send(KEY_HOME)  (cost 633 ms)
[SAMSUNG-SM-G975U1] >> START  Step7 app.closeApp(com.sigma_rt.sigmatestapp)
[SAMSUNG-SM-G975U1] << END    Step7 app.closeApp(com.sigma_rt.sigmatestapp)  (cost 1661 ms)
[SAMSUNG-SM-G975U1] === DEVICE END (OK) ===
[SAMSUNG-SM-G975U1] OK

==============================
RUN 2/2 : samsung-SM-S938U1
==============================
[samsung-SM-S938U1] === DEVICE BEGIN ===
[samsung-SM-S938U1] info: samsung SM-S938U1 1440x3120
[samsung-SM-S938U1] >> START  Step1 app.runApp(com.sigma_rt.sigmatestapp)
[samsung-SM-S938U1] << END    Step1 app.runApp(com.sigma_rt.sigmatestapp)  (cost 2596 ms)
[samsung-SM-S938U1] >> START  Step2 verify foreground + getForegroundApp/getActivity
[samsung-SM-S938U1] foregroundApp=com.sigma_rt.sigmatestapp
[samsung-SM-S938U1] activity=com.sigma_rt.sigmatestapp/.MainActivity
[samsung-SM-S938U1] << END    Step2 verify foreground + getForegroundApp/getActivity  (cost 600 ms)
[samsung-SM-S938U1] >> START  Step3 input.click2(searchBox)
[samsung-SM-S938U1] << END    Step3 input.click2(searchBox)  (cost 343 ms)
[samsung-SM-S938U1] >> START  Step4 tests.inputText(SEARCH_TEXT + \n)
[samsung-SM-S938U1] << END    Step4 tests.inputText(SEARCH_TEXT + \n)  (cost 1032 ms)
[samsung-SM-S938U1] >> START  Step5 navKeys.pgDn + navKeys.shiftPgDn
[samsung-SM-S938U1] << END    Step5 navKeys.pgDn + navKeys.shiftPgDn  (cost 695 ms)
[samsung-SM-S938U1] >> START  Step6 send(KEY_BACK) + send(KEY_HOME)
[samsung-SM-S938U1] << END    Step6 send(KEY_BACK) + send(KEY_HOME)  (cost 633 ms)
[samsung-SM-S938U1] >> START  Step7 app.closeApp(com.sigma_rt.sigmatestapp)
[samsung-SM-S938U1] << END    Step7 app.closeApp(com.sigma_rt.sigmatestapp)  (cost 232 ms)
[samsung-SM-S938U1] === DEVICE END (OK) ===
[samsung-SM-S938U1] OK

===== SUMMARY =====
Success: 2 / 2

TCHelp