How to Use
Introduction:
This example demonstrates how to use Total Control to synchronously execute the same App operation workflow across multiple Android devices. The script can automatically complete app launching, foreground verification, tapping, text input, page scrolling, returning to the home screen, and closing the app. During execution, it records step execution time, automatically captures screenshots and preserves failure states, and collects key runtime context information. This makes it suitable for multi-device automated testing, batch operations, and as a reference for beginners.
Before Running:
sigmaLoad("D:/scripts/multi_device_flow.js");
Source Code
/**
* ============================================================
* Unified execution of a single App operation workflow across multiple devices (ready to copy and run)
* ============================================================
* For each device: launch App → verify foreground → click → input → scroll → return to home → close App
*
* 1) Get devices: only use getDevices() (from sigma/device)
* 2) Each step prints: start / end / duration (stepRun)
* 3) Automatic screenshot on failure (sigma/image -> device.screenshot)
* 4) Automatic failure context logging: foreground package + activity + battery + device info (writeFailContext)
* 5) The code contains “detailed commented documentation” for easy sharing with beginners for learning/debugging
*
* ------------------------------------------------------------
* Module responsibilities
* - device : get devices, read device properties
* - app : runApp/isAppForeground/getForegroundApp/getActivity/closeApp
* - input : click/click2/send(system keys HOME/BACK/ENTER)/scroll/swipe, etc.
* - navKeys: pgUp/pgDn/up/down/shiftPgDn... (paging/directional navigation, non-system keys)
* - image : screenshot/screenshotToMemory/seekImage, etc. (used here for failure evidence screenshots)
* - tests : inputText/inputForm/getClipboardText (used in this version for “input”)
* ------------------------------------------------------------
*
* Prerequisites:
* - In the Total Control UI, first “select/check” the devices you want to operate
* - getDevices() will return these “selected devices”
*
* Common tuning points (beginners only need to change these):
* - TARGET_PKG target App package name
* - SEARCH_TEXT input text (for teaching, it is recommended to start with English/numbers)
* - SEARCH_X/Y relative coordinates of the search box (0~1)
* - LOG_DIR log/screenshot output directory
* - WAIT_LAUNCH_MS App launch wait time
*/
// ===================== Imports & Injection =====================
var tcConst = global.tcConst || {};
var sigmaConst = global.sigmaConst || {};
// device: only use getDevices() to obtain devices (as requested)
var { getDevices, Device } = require("sigma/device");
// app/input/navKeys/image/tests are all attached to Device.prototype
// So after require, you can directly use 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"); // Added: tests input module (inputText/inputForm/getClipboardText)
// ===================== Configuration =====================
// Target package name (replace as needed)
// var TARGET_PKG = "com.tencent.qqmusic";
var TARGET_PKG = "com.sigma_rt.sigmatestapp";
// Input text
var SEARCH_TEXT = "500";
// Launch wait time (varies by device performance, recommended 2000~6000ms)
var WAIT_LAUNCH_MS = 2500;
// Relative coordinates of the search box: 0~1 (more stable across resolutions)
// var SEARCH_X = 0.5;
// var SEARCH_Y = 0.12;
var SEARCH_X = 0.5352;
var SEARCH_Y = 0.135;
// Log/screenshot output directory (modify according to your computer)
var LOG_DIR = "D:/tc_logs";
// Image format constant: prefer JPG (smaller); fall back to PNG if unavailable
// Note: your environment may use sigmaConst.IMG_JPG or tcConst.IMG_JPG
var IMG_TYPE = (sigmaConst.IMG_JPG || tcConst.IMG_JPG || sigmaConst.IMG_PNG || tcConst.IMG_PNG);
// ===================== Common Utility Functions =====================
/** Return current millisecond timestamp (for duration statistics) */
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) {}
}
/** Device display name (prefer name, then getName, then SN) */
function devName(d) {
return d.name || (d.getName && d.getName()) || d.SN || "UnknownDevice";
}
/** Zero-pad to two digits (for timestamp formatting) */
function pad2(n) { return (n < 10 ? "0" : "") + n; }
/** Generate YYYYMMDD_HHMMSS timestamp (for log/screenshot filenames) */
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())
);
}
/** Convert string to a safe filename fragment (remove illegal characters) */
function safeFilePart(s) {
return String(s).replace(/[^\w\-.]+/g, "_");
}
// ===================== File Writing: write if supported, otherwise console only =====================
var fs = null;
try { fs = require("fs"); } catch (e) { fs = null; }
/** Try to create a directory (create if supported, otherwise return false) */
function ensureDir(path) {
if (!fs) return false;
try {
if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive: true });
return true;
} catch (e) {
return false;
}
}
/** Append to a file (write if supported, otherwise return false) */
function appendFile(path, text) {
if (!fs) return false;
try {
fs.appendFileSync(path, text, { encoding: "utf8" });
return true;
} catch (e) {
return false;
}
}
// ===================== Key: Step Runner (start/end/duration) =====================
// Purpose: unify output format for business steps; any exception can be precisely located.
// Usage: 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: System Keys =====================
/**
* press: send a system key (HOME/BACK/ENTER, etc.)
* - input.send(code, state) -> returns 0 on success, -1 on failure (check lastError() for details)
*/
function press(d, keyConst) {
var ret = d.send(keyConst, tcConst.STATE_PRESS);
if (ret !== 0) throw new Error("send failed: " + lastError());
}
// ===================== Failure Handling: Context Collection + Screenshot =====================
/** Safe getter: prevents secondary failures caused by errors on some devices */
function safeGet(fn, fallback) {
try { return fn(); } catch (e) { return fallback; }
}
/**
* Write failure context to log:
* - 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 || ""));
// Foreground package / Activity (app module methods injected into 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 || ""));
// Battery (device property)
var battery = safeGet(function () { return d.battery; }, null);
ctx.log("battery=" + (battery !== null && battery !== undefined ? battery : ""));
// Basic device info (device properties)
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 ----");
}
/**
* Failure screenshot:
* - Uses device.screenshot(filePath, imageType) injected by the image module
* - Filename includes: device name + step + timestamp
*/
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());
}
// ===================== Device-level Context (Logger Object) =====================
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");
}
};
}
// ===================== Single-Device Flow (Core Logic) =====================
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 || "";
// Failure context + screenshot
writeFailContext(ctx, d, stepName, errText);
takeFailScreenshot(ctx, d, stepName);
ctx.log("=== DEVICE END (FAIL) ===");
return result;
}
// ---------------- Step 1: Launch App ----------------
var s1 = stepRun(ctx, "Step1 app.runApp(" + TARGET_PKG + ")", function () {
var r = d.runApp(TARGET_PKG);
if (r !== 0) throw new Error(lastError());
// Wait for stabilization after launch
sleep(WAIT_LAUNCH_MS);
return r;
});
if (!s1.ok) return failAt("runApp", s1.err);
// ---------------- Step 2: Verify foreground + read 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: Click search box (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: Input (tests.inputText) + Enter ----------------
// Change: use tests module for input instead of character-by-character KEYCODEs
// Notes:
// - d.inputText(content) inputs text into the currently focused field
// - Appending '\n' is equivalent to “Enter/Submit”
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: Paging (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: Back + Home (system keys -> 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: Close 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);
// Full workflow succeeded
result.ok = true;
result.step = "done";
ctx.log("=== DEVICE END (OK) ===");
return result;
}
// ===================== main: only use getDevices() to obtain devices =====================
function main() {
if (Device.connectAll) Device.connectAll();
// Use getDevices()
var devices = getDevices();
if (!devices || devices.length === 0) {
print("No selected devices. Please select devices first.");
return;
}
// Try to create log directory
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();