多设备批量执行与结果收集全解析


更多信息请参阅 JavaScript Modules


如何使用

简介:

本示例演示如何在多设备批量执行流程中使用 ArrayOutput 收集每台设备的执行结果,并通过 ArrayOutputReturn 进行统一读取与分析。示例涵盖结果结构、常用访问方式、错误判断与调试技巧,可直接复制运行并应用于实际项目。

运行前:

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


源代码

/**
 * ArrayOutput 全功能示例:多设备批量执行 + 详细说明(可直接复制运行)
 *
 * 你将学到/看到:
 * 1) ArrayOutput:用于“收集”每台设备的执行结果(value + error)
 * 2) ArrayOutputReturn:用于“读取/查询”结果(只读包装器)
 * 3) global.showOutputFlag:
 *      - 开启后 getReturn() 会自动 print 一份结构化表格
 *      - 并且把最新的 ArrayOutputReturn 赋值给 global.ao(全局调试别名)
 * 4) ret.get() 的三种形态:
 *      - ret.get()                 => 返回完整 records 数组(每条包含 name/object/id/value/error)
 *      - ret.get(device)           => 返回该 device 的 value
 *      - ret.get([device...])      => 返回与输入数组等长的 value 数组(顺序对齐输入)
 * 5) 动态 helpers(objects/values/errors/ids/names):
 *      - 无参:返回整列数组
 *      - 带 1 个参数:返回某个对象对应字段的单值
 * 6) success():所有 error 都为空则为 true
 * 7) lastError():返回最后一个 error 不为空的条目(从尾到头扫描)
 * 8) findObj():
 *      - 既支持传 device 对象
 *      - 也支持传 String(device)(即 id)
 *
 * 注意:
 * - 下面的 runFlow() 用的是你项目里常见的 Total Control API(runApp/isAppForeground/click2/inputText...)
 * - 如果某些 API 在你的环境中名字不同,替换掉即可,不影响 ArrayOutput 的示例逻辑
 */

var { getDevices, Device } = require("sigma/device");

// ===================== 配置:示例要操控的 App 与动作 =====================
var TARGET_PKG = "com.sigma_rt.sigmatestapp";
var SEARCH_TEXT = "500";
var WAIT_LAUNCH_MS = 1200;
var SEARCH_X = 0.5352;
var SEARCH_Y = 0.135;

// ===================== 工具函数:设备名称/日志/按键 =====================
function devName(d) {
    // 兼容不同设备对象字段:name/getName/SN
    return d.name || (d.getName && d.getName()) || d.SN || "UnknownDevice";
}

function logDev(d, s) {
    print("[" + devName(d) + "] " + s);
}

function press(d, keyConst) {
    // Total Control 常见方式:send(KEY_*, STATE_PRESS)
    var ret = d.send(keyConst, tcConst.STATE_PRESS);
    if (ret !== 0) throw new Error("send failed: " + lastError());
}

/**
 * step():一个简单的“步骤包装器”
 * - 作用:把 try/catch + 错误字符串化统一起来,便于流程代码更清晰
 * - 返回:{ ok: true, ret: any } 或 { ok: false, err: string }
 */
function step(label, fn) {
    try {
        var r = fn();
        return { ok: true, ret: r };
    } catch (e) {
        return { ok: false, err: String(e && e.message ? e.message : e) };
    }
}

// ===================== 单设备流程:成功返回 value(对象),失败 throw(由外层收集 error) =====================
/**
 * runFlow(d) 的返回值会作为 ArrayOutput.add(d, value) 的 value。
 * 如果 runFlow 抛异常,外层 catch 后会 out.add(d, null, errorText)。
 */
function runFlow(d) {
    // 这是最终要“返回并收集”的 value(结构化更利于后续分析)
    var result = {
        ok: false,
        step: "", // 记录失败/结束所在步骤
        fg: "", // 前台包名(成功时)
        act: "", // activity(成功时)
    };

    logDev(d, "=== DEVICE BEGIN ===");

    // 1) 启动 App
    var s1 = step("runApp", function () {
        var r = d.runApp(TARGET_PKG);
        if (r !== 0) throw new Error(lastError());
        sleep(WAIT_LAUNCH_MS);
        return 0;
    });
    if (!s1.ok) {
        result.step = "runApp";
        throw new Error("runApp failed: " + s1.err);
    }

    // 2) 校验前台 + 读取前台信息
    var s2 = step("foregroundCheck", function () {
        var r = d.isAppForeground(TARGET_PKG);
        if (r !== 0) throw new Error("not foreground: " + lastError());
        result.fg = d.getForegroundApp() || "";
        result.act = d.getActivity() || "";
        return 0;
    });
    if (!s2.ok) {
        result.step = "foregroundCheck";
        throw new Error("foregroundCheck failed: " + s2.err);
    }

    // 3) 点击搜索框
    var s3 = step("clickSearch", function () {
        var r = d.click2(SEARCH_X, SEARCH_Y, { dx: 0.08, dy: 0.04 });
        if (r !== 0) throw new Error(lastError());
        sleep(250);
        return 0;
    });
    if (!s3.ok) {
        result.step = "clickSearch";
        throw new Error("clickSearch failed: " + s3.err);
    }

    // 4) 输入并回车
    var s4 = step("inputText", function () {
        var r = d.inputText(SEARCH_TEXT + "\n");
        if (r !== 0) throw new Error("inputText failed: " + lastError());
        sleep(600);
        return 0;
    });
    if (!s4.ok) {
        result.step = "inputText";
        throw new Error("inputText failed: " + s4.err);
    }

    // 5) 翻页(示例:失败不致命,这里不 throw,仅演示可以继续)
    step("paging", function () {
        d.pgDn();
        sleep(200);
        d.shiftPgDn();
        sleep(300);
        return 0;
    });

    // 6) 返回/主页
    var s6 = step("backHome", function () {
        press(d, tcConst.KEY_BACK);
        sleep(200);
        press(d, tcConst.KEY_HOME);
        sleep(300);
        return 0;
    });
    if (!s6.ok) {
        result.step = "backHome";
        throw new Error("backHome failed: " + s6.err);
    }

    // 7) 关闭(失败不致命)
    step("closeApp", function () {
        d.closeApp(TARGET_PKG);
        return 0;
    });

    result.ok = true;
    result.step = "done";
    logDev(d, "=== DEVICE END (OK) ===");
    return result;
}

// ===================== 主入口:收集 + 返回包装器 + 全功能读取演示 =====================
function main() {
    /**
     * showOutputFlag 的意义:
     * - out.getReturn() 时模块内部会打印一份简化表:
     *     [{name, object: id, value, error}, ...]
     * - 并把 global.ao 设置为最新返回对象(方便你在控制台直接 ao.values() 查看)
     */
    global.showOutputFlag = true;

    if (Device.connectAll) Device.connectAll();

    var devices = getDevices();
    if (!devices || devices.length === 0) {
        print("No selected devices. Please select devices first.");
        return;
    }

    print("Selected devices: " + devices.length);

    // 1) 创建收集器
    var out = new ArrayOutput();

    // 2) 批量执行:每台设备产生 value 或 error
    for (var i = 0; i < devices.length; i++) {
        var d = devices[i];

        print("\n==============================");
        print("RUN " + (i + 1) + "/" + devices.length + " : " + devName(d));
        print("==============================");

        try {
            // 成功:value 是结构化对象(推荐)
            var value = runFlow(d);
            out.add(d, value); // error 省略默认 null
        } catch (e) {
            // 失败:value 为 null,error 为字符串
            out.add(d, null, String(e && e.message ? e.message : e));
        }
    }

    // 3) 得到只读返回对象(核心)
    var ret = out.getReturn();

    // 4) 验证全局别名
    print("\n-- global alias check --");
    print("ret === global.ao ? " + (ret === global.ao));

    // =========================================================
    // A. 状态类 API:toString / success / lastError
    // =========================================================
    print("\n-- status APIs --");
    print(ret.toString()); // "ArrayOutputReturn: N elements"
    print("success() = " + ret.success()); // 全部无 error 才 true

    var lastErr = ret.lastError(); // {object, id, error} 或 undefined
    if (lastErr) {
        print("lastError.id=" + lastErr.id);
        print("lastError.error=" + lastErr.error);
    } else {
        print("lastError() = undefined (no errors)");
    }

    // =========================================================
    // B. get() 三种形态(最重要的查询入口)
    // =========================================================
    print("\n-- get() forms --");

    // 1) ret.get():取全部 records(每条都包含:name/object/id/value/error)
    var all = ret.get();
    print("get() records length = " + all.length);
    // all[0] 示例结构:
    // {
    //   name: "DeviceName",
    //   object: <deviceObject>,
    //   id: "String(deviceObject)",
    //   value: <你的结果对象或任意值>,
    //   error: <错误字符串或 null>
    // }

    // 2) ret.get(device):取单设备 value(不是 record)
    var d0 = devices[0];
    print("get(device0) value = " + JSON.stringify(ret.get(d0)));

    // 3) ret.get([device...]):批量取 value(顺序对齐输入数组)
    var pick = devices.slice(0, Math.min(3, devices.length));
    print("get([device0..]) values = " + JSON.stringify(ret.get(pick)));

    // =========================================================
    // C. 动态 helpers:objects/values/errors/ids/names
    // =========================================================
    print("\n-- helpers (no args) --");
    // 无参:整列数组
    // 注意:objects() 返回的数组是设备对象本体;打印时可能显示为 [object ...]
    print("objects() = " + ret.objects());
    print("values()  = " + JSON.stringify(ret.values()));
    print("errors()  = " + JSON.stringify(ret.errors()));
    print("ids()     = " + JSON.stringify(ret.ids()));
    print("names()   = " + JSON.stringify(ret.names()));

    print("\n-- helpers (single target) --");
    // 单参数:取某个设备的一项字段
    print("values(device0) = " + JSON.stringify(ret.values(d0)));
    print("errors(device0) = " + JSON.stringify(ret.errors(d0)));	
    print("ids(device0)    = " + JSON.stringify(ret.ids(d0))); 
    print("names(device0)  = " + JSON.stringify(ret.names(d0)));
    print("objects(device0)= " + ret.objects(d0));

    // =========================================================
    // D. findObj:定位索引(object / string id 两种方式)
    // =========================================================
    print("\n-- findObj() --");

    // 1) 传对象:通过 identity 或 String(obj) 匹配
    var idxByObj = ret.findObj(d0);
    print("findObj(device0) = " + idxByObj);

    // 2) 传字符串:按 String(this.output[i].object) 匹配
    //    也就是 add() 时存的 id = String(obj)
    var id0 = String(d0);
    var idxById = ret.findObj(id0);
    print("findObj(String(device0)) = " + idxById + ' (id="' + id0 + '")');

    // =========================================================
    // E. 全局 ao:调试快捷入口(ret 的同一个对象)
    // =========================================================
    print("\n-- global ao usage --");
    print("ao.values()  = " + JSON.stringify(ao.values()));
    print("ao.errors()  = " + JSON.stringify(ao.errors()));
    print("ao.success() = " + ao.success());

    // =========================================================
    // F. 常见实战:做一份可读性更强的汇总输出
    // =========================================================
    print("\n===== SUMMARY (computed) =====");
    var names = ret.names();
    var errs = ret.errors();
    var vals = ret.values();

    var okCount = 0;
    for (var k = 0; k < names.length; k++) {
        if (!errs[k]) {
            okCount++;
            print("[" + names[k] + "] OK step=" + (vals[k] && vals[k].step));
        } else {
            print("[" + names[k] + "] FAIL err=" + errs[k]);
        }
    }
    print("Success: " + okCount + " / " + names.length);

    /**
     * 额外提示(常见用法建议):
     * - 建议 value 保存结构化对象(如 {ok, step, fg, act, ...}),便于后续统计与定位
     * - 建议 error 保存 String(err)
     * - 你可以在失败时把更多信息塞进 error 文本或 value(例如截图路径、耗时、关键参数)
     */
}

main();
		

运行结果

Selected devices: 3
 
==============================
RUN 1/3 : HONOR-HLK-AL00
==============================
[HONOR-HLK-AL00] === DEVICE BEGIN ===
[HONOR-HLK-AL00] === DEVICE END (OK) ===
 
==============================
RUN 2/3 : HONOR-HLK-AL00
==============================
[HONOR-HLK-AL00] === DEVICE BEGIN ===
[HONOR-HLK-AL00] === DEVICE END (OK) ===
 
==============================
RUN 3/3 : HONOR-HLK-AL00
==============================
[HONOR-HLK-AL00] === DEVICE BEGIN ===
[HONOR-HLK-AL00] === DEVICE END (OK) ===
{name: 'HONOR-HLK-AL00', object: 'device@1658225331', value: {ok: true, step: 'done', fg: 'com.sigma_rt.sigmatestapp', act: 'com.huawei.android.launcher/.unihome.UniHomeLauncher'}, error: null},{name: 'HONOR-HLK-AL00', object: 'device@403850225', value: {ok: true, step: 'done', fg: 'com.sigma_rt.sigmatestapp', act: 'com.huawei.android.launcher/.unihome.UniHomeLauncher'}, error: null},{name: 'HONOR-HLK-AL00', object: 'device@2081074872', value: {ok: true, step: 'done', fg: 'com.sigma_rt.sigmatestapp', act: 'com.sigma_rt.sigmatestapp/.MainActivity'}, error: null}
 
-- global alias check --
ret === global.ao ? true
aoAlias === global.ao ? false
 
-- status APIs --
ArrayOutputReturn: 3 elements
success() = true
lastError() = undefined (no errors)
 
-- get() forms --
get() records length = 3
get(device0) value = {"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"}
get([device0..]) values = [{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"},{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"},{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.sigma_rt.sigmatestapp/.MainActivity"}]
 
-- helpers (no args) --
objects() = device@1658225331,device@403850225,device@2081074872
values()  = [{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"},{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"},{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.sigma_rt.sigmatestapp/.MainActivity"}]
errors()  = [null,null,null]
ids()     = ["device@1658225331","device@403850225","device@2081074872"]
names()   = ["HONOR-HLK-AL00","HONOR-HLK-AL00","HONOR-HLK-AL00"]
 
-- helpers (single target) --
values(device0) = {"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"}
errors(device0) = null
ids(device0)    = "device@1658225331"
names(device0)  = "HONOR-HLK-AL00"
objects(device0)= device@1658225331
 
-- findObj() --
findObj(device0) = 0
findObj(String(device0)) = 0 (id="device@1658225331")
 
-- global ao usage --
ao.values()  = [{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"},{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.huawei.android.launcher/.unihome.UniHomeLauncher"},{"ok":true,"step":"done","fg":"com.sigma_rt.sigmatestapp","act":"com.sigma_rt.sigmatestapp/.MainActivity"}]
ao.errors()  = [null,null,null]
ao.success() = true
 
===== SUMMARY (computed) =====
[HONOR-HLK-AL00] OK step=done
[HONOR-HLK-AL00] OK step=done
[HONOR-HLK-AL00] OK step=done
Success: 3 / 3

TCHelp