Unified App Operation Workflow Across Multiple Devices


Please refer to JavaScript Modules for more information.


How to Use

Overview:

This example demonstrates how to use Total Control to synchronously execute the same App operation flow across multiple Android devices. The script can automatically perform app launching, foreground verification, clicking, text input, paging, returning to the home screen, and closing the app. During execution, it records step execution time, automatically captures screenshots and saves failure states, and collects key runtime context information. It is suitable for multi-device automated testing, batch operations, and as a learning reference for beginners.

Before Running:

  1. Download and install Total Control 11.0 (Update 20) or later (Download)
  2. Connect Android devices (supports USB connection or TCP connection)
  3. Install SigmaTestApp on the Android devices as the test application (How to install)
  4. Select the test devices in the Total Control interface (Note: getDevices() in the script only returns the currently selected devices)
  5. Save the example code as a .js file (for example, MultiDeviceFlow.js) and place it in the Total Control default script directory
  6. Adjust the following configuration items according to your test scenario:
    • TARGET_PKG: Target app package name
    • SEARCH_TEXT: Text to be entered
    • SEARCH_X/Y: Relative coordinates of the search box (0–1)
    • LOG_DIR: Output directory for logs and screenshots
    • WAIT_LAUNCH_MS: App launch wait time
  7. Open the Script Terminal (Main Panel → Scripts → Script Terminal), then execute:
    >> sigmaLoad("multiDeviceFlow.js");


Source Code

/**
 * Unified App operation flow across multiple devices (copy & run)
 * Launch -> verify foreground -> click -> input -> pgDn -> home -> close
 */

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

// ===================== Config =====================
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; // if tcconst carries these; otherwise keep sigmaConst

// ===================== fs helpers (optional) =====================
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;
    }
}

// ===================== tiny utilities =====================
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";
}

// Safe property/method fetcher
function safe(fn, fallback) {
    try {
        return fn();
    } catch (e) {
        return fallback;
    }
}

// System key press wrapper
function press(d, keyConst) {
    var ret = d.send(keyConst, tcConst.STATE_PRESS);
    if (ret !== 0) throw new Error("send failed: " + lastError());
}

// Step wrapper (timing + uniform logs)
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 };
    }
}

// One helper to “run a step or fail fast”
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);
}

// Failure context (data-driven, less repetition)
function writeFailContext(ctx, d, stepKey, errText) {
    ctx.log("---- FAIL CONTEXT BEGIN ----");
    ctx.log("step=" + stepKey);
    ctx.log("error=" + (errText || ""));

    // foreground info
    ctx.log("foregroundApp=" + (safe(() => d.getForegroundApp(), "") || ""));
    ctx.log("activity=" + (safe(() => d.getActivity(), "") || ""));

    // common device fields (add/remove here)
    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());
}

// Central fail handler (so you don’t repeat it)
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;
}

// Per-device logger
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");
        },
    };
}

// ===================== Single-device flow =====================
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) Launch
    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) Foreground verify
    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) Click search
    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) Input + enter
    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) Paging
    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) Back + Home
    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) Close app (non-fatal if it fails, but keep it as a step)
    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;
}

// ===================== main =====================
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();		

Execution Result

Selected devices: 2
 
==============================
RUN 1/2 : TestDevice03
==============================
[TestDevice03] === DEVICE BEGIN ===
[TestDevice03] info: HUAWEI HLK-AL00 1080x2340
[TestDevice03] >> START  Step1 app.runApp(com.sigma_rt.sigmatestapp)
[TestDevice03] << END    Step1 app.runApp(com.sigma_rt.sigmatestapp)  (cost 2591 ms)
[TestDevice03] >> START  Step2 app.isAppForeground + getForegroundApp/getActivity
[TestDevice03] foregroundApp=com.sigma_rt.sigmatestapp
[TestDevice03] activity=com.sigma_rt.sigmatestapp/.MainActivity
[TestDevice03] << END    Step2 app.isAppForeground + getForegroundApp/getActivity  (cost 405 ms)
[TestDevice03] >> START  Step3 input.click2(searchBox)
[TestDevice03] << END    Step3 input.click2(searchBox)  (cost 364 ms)
[TestDevice03] >> START  Step4 tests.inputText(SEARCH_TEXT + \n)
[TestDevice03] << END    Step4 tests.inputText(SEARCH_TEXT + \n)  (cost 1032 ms)
[TestDevice03] >> START  Step5 navKeys.pgDn + navKeys.shiftPgDn
[TestDevice03] << END    Step5 navKeys.pgDn + navKeys.shiftPgDn  (cost 716 ms)
[TestDevice03] >> START  Step6 input.send(KEY_BACK) + input.send(KEY_HOME)
[TestDevice03] << END    Step6 input.send(KEY_BACK) + input.send(KEY_HOME)  (cost 653 ms)
[TestDevice03] >> START  Step7 app.closeApp(com.sigma_rt.sigmatestapp)
[TestDevice03] << END    Step7 app.closeApp(com.sigma_rt.sigmatestapp)  (cost 254 ms)
[TestDevice03] === DEVICE END (OK) ===
[TestDevice03] OK
 
==============================
RUN 2/2 : T110
==============================
[T110] === DEVICE BEGIN ===
[T110] info: HUAWEI HLK-AL00 1080x2340
[T110] >> START  Step1 app.runApp(com.sigma_rt.sigmatestapp)
[T110] << END    Step1 app.runApp(com.sigma_rt.sigmatestapp)  (cost 2578 ms)
[T110] >> START  Step2 app.isAppForeground + getForegroundApp/getActivity
[T110] foregroundApp=com.sigma_rt.sigmatestapp
[T110] activity=com.huawei.android.launcher/.unihome.UniHomeLauncher
[T110] << END    Step2 app.isAppForeground + getForegroundApp/getActivity  (cost 483 ms)
[T110] >> START  Step3 input.click2(searchBox)
[T110] << END    Step3 input.click2(searchBox)  (cost 343 ms)
[T110] >> START  Step4 tests.inputText(SEARCH_TEXT + \n)
[T110] << END    Step4 tests.inputText(SEARCH_TEXT + \n)  (cost 1032 ms)
[T110] >> START  Step5 navKeys.pgDn + navKeys.shiftPgDn
[T110] << END    Step5 navKeys.pgDn + navKeys.shiftPgDn  (cost 693 ms)
[T110] >> START  Step6 input.send(KEY_BACK) + input.send(KEY_HOME)
[T110] << END    Step6 input.send(KEY_BACK) + input.send(KEY_HOME)  (cost 633 ms)
[T110] >> START  Step7 app.closeApp(com.sigma_rt.sigmatestapp)
[T110] << END    Step7 app.closeApp(com.sigma_rt.sigmatestapp)  (cost 291 ms)
[T110] === DEVICE END (OK) ===
[T110] OK
 
===== SUMMARY =====
Success: 2 / 2
0

TCHelp