misoai-android
Version:
Appium-based Android automation library for misoAI
1,522 lines (1,519 loc) • 55.8 kB
JavaScript
// src/page/appium-device.ts
import { NodeType } from "misoai-shared/constants";
import { getDebug } from "misoai-shared/logger";
import { remote } from "webdriverio";
var debugDevice = getDebug("android:appium-device");
var AppiumDevice = class {
/**
* Creates a new AppiumDevice instance
*
* @param serverConfig - Appium server configuration
* @param capabilities - Appium capabilities
*/
constructor(serverConfig, capabilities) {
/**
* Page type identifier
*/
this.pageType = "android";
/**
* WebdriverIO browser instance
*/
this.driver = null;
/**
* Screen size cache
*/
this.screenSize = null;
this.serverConfig = serverConfig;
this.capabilities = capabilities;
}
/**
* Connects to the Appium server and starts a session
*/
async connect() {
if (this.driver) {
debugDevice("Already connected to Appium server");
return this.driver;
}
try {
debugDevice(
"Connecting to Appium server at %s://%s:%d%s",
this.serverConfig.protocol || "http",
this.serverConfig.hostname,
this.serverConfig.port,
this.serverConfig.path || "/wd/hub"
);
const options = {
hostname: this.serverConfig.hostname,
port: this.serverConfig.port,
path: this.serverConfig.path || "/wd/hub",
protocol: this.serverConfig.protocol || "http",
capabilities: this.capabilities,
logLevel: "info",
connectionRetryTimeout: 12e4,
connectionRetryCount: 3
};
debugDevice("Starting Appium session with capabilities: %O", this.capabilities);
this.driver = await remote(options);
debugDevice("Successfully connected to Appium server, session ID: %s", this.driver.sessionId);
return this.driver;
} catch (error) {
debugDevice("Failed to connect to Appium server: %s", error.message);
throw new Error(`Failed to connect to Appium server: ${error.message}`, {
cause: error
});
}
}
/**
* Disconnects from the Appium server and ends the session
*/
async disconnect() {
if (!this.driver) {
debugDevice("No active Appium session to disconnect");
return;
}
try {
debugDevice("Ending Appium session");
const isSauceLabs = this.serverConfig.hostname?.includes("saucelabs.com");
if (isSauceLabs) {
debugDevice("Detected Sauce Labs session, ensuring proper termination");
try {
await this.driver.executeScript("sauce:job-result", [{
passed: true
}]);
} catch (e) {
debugDevice("Could not set Sauce Labs job result: %s", e.message);
}
await this.driver.deleteSession();
} else {
await this.driver.deleteSession();
}
this.driver = null;
debugDevice("Successfully ended Appium session");
} catch (error) {
debugDevice("Error ending Appium session: %s", error.message);
throw new Error(`Failed to end Appium session: ${error.message}`, {
cause: error
});
}
}
/**
* Launches an app or opens a URL
*
* @param uri - App package, activity, or URL to launch
*/
async launch(uri) {
const driver = await this.getDriver();
this.uri = uri;
try {
if (uri.startsWith("http://") || uri.startsWith("https://") || uri.includes("://")) {
debugDevice("Opening URL: %s", uri);
await driver.url(uri);
} else if (uri.includes("/")) {
const [appPackage, appActivity] = uri.split("/");
debugDevice("Starting activity: %s/%s", appPackage, appActivity);
await this.startActivity(appPackage, appActivity);
} else {
debugDevice("Activating app: %s", uri);
await driver.activateApp(uri);
}
debugDevice("Successfully launched: %s", uri);
} catch (error) {
debugDevice("Error launching %s: %s", uri, error.message);
throw new Error(`Failed to launch ${uri}: ${error.message}`, {
cause: error
});
}
return this;
}
/**
* Gets the WebdriverIO driver instance, connecting if necessary
*/
async getDriver() {
if (!this.driver) {
await this.connect();
}
if (!this.driver) {
throw new Error("Failed to initialize WebdriverIO driver");
}
return this.driver;
}
/**
* Takes a screenshot and returns it as a base64-encoded string
*/
async screenshotBase64() {
debugDevice("Taking screenshot");
const driver = await this.getDriver();
try {
const screenshot = await driver.takeScreenshot();
return `data:image/png;base64,${screenshot}`;
} catch (error) {
debugDevice("Error taking screenshot: %s", error.message);
throw new Error(`Failed to take screenshot: ${error.message}`, {
cause: error
});
}
}
/**
* Gets the element tree for the current screen
* @returns Promise resolving to the element tree
*/
async getElementsNodeTree() {
debugDevice("Getting element tree");
const driver = await this.getDriver();
try {
await driver.getPageSource();
const rootElement = {
id: "root",
indexId: 0,
nodeHashId: "root",
locator: "/",
attributes: {
nodeType: NodeType.CONTAINER
},
nodeType: NodeType.CONTAINER,
content: "Root Element",
rect: { left: 0, top: 0, width: 0, height: 0 },
center: [0, 0]
};
return {
node: rootElement,
children: []
};
} catch (error) {
debugDevice("Error getting element tree: %s", error.message);
throw new Error(`Failed to get element tree: ${error.message}`, {
cause: error
});
}
}
/**
* Gets the current URL
*/
async url() {
try {
const driver = await this.getDriver();
const currentPackage = await driver.getCurrentPackage();
const currentActivity = await driver.getCurrentActivity();
return `${currentPackage}/${currentActivity}`;
} catch (error) {
debugDevice("Error getting URL: %s", error.message);
return "";
}
}
/**
* Gets the screen size
*/
async size() {
if (this.screenSize) {
return this.screenSize;
}
try {
const driver = await this.getDriver();
const { width, height } = await driver.getWindowSize();
this.screenSize = { width, height };
return this.screenSize;
} catch (error) {
debugDevice("Error getting screen size: %s", error.message);
throw new Error(`Failed to get screen size: ${error.message}`, {
cause: error
});
}
}
/**
* Starts an activity
*
* @param appPackage - Package name
* @param appActivity - Activity name
* @param opts - Additional options
*/
async startActivity(appPackage, appActivity, opts) {
debugDevice("Starting activity: %s/%s", appPackage, appActivity);
const driver = await this.getDriver();
try {
await driver.startActivity(appPackage, appActivity, opts);
debugDevice("Successfully started activity: %s/%s", appPackage, appActivity);
} catch (error) {
debugDevice("Error starting activity %s/%s: %s", appPackage, appActivity, error.message);
throw new Error(`Failed to start activity ${appPackage}/${appActivity}: ${error.message}`, {
cause: error
});
}
}
/**
* Opens a URL
*
* @param url - URL to open
*/
async openUrl(url) {
debugDevice("Opening URL: %s", url);
const driver = await this.getDriver();
try {
await driver.url(url);
debugDevice("Successfully opened URL: %s", url);
} catch (error) {
debugDevice("Error opening URL %s: %s", url, error.message);
throw new Error(`Failed to open URL ${url}: ${error.message}`, {
cause: error
});
}
}
/**
* Closes the current app
*/
async closeApp() {
debugDevice("Closing app");
try {
await this.home();
debugDevice("Successfully closed app by pressing home button");
} catch (error) {
debugDevice("Error closing app: %s", error.message);
throw new Error(`Failed to close app: ${error.message}`, {
cause: error
});
}
}
/**
* Terminates an app
*
* @param appId - App package name
*/
async terminateApp(appId) {
debugDevice("Terminating app: %s", appId);
const driver = await this.getDriver();
try {
await driver.terminateApp(appId, {});
debugDevice("Successfully terminated app: %s", appId);
return true;
} catch (error) {
debugDevice("Error terminating app %s: %s", appId, error.message);
throw new Error(`Failed to terminate app ${appId}: ${error.message}`, {
cause: error
});
}
}
/**
* Installs an app
*
* @param appPath - Path to the app
*/
async installApp(appPath) {
debugDevice("Installing app: %s", appPath);
const driver = await this.getDriver();
try {
await driver.installApp(appPath);
debugDevice("Successfully installed app: %s", appPath);
} catch (error) {
debugDevice("Error installing app %s: %s", appPath, error.message);
throw new Error(`Failed to install app ${appPath}: ${error.message}`, {
cause: error
});
}
}
/**
* Checks if an app is installed
*
* @param appId - App package name
*/
async isAppInstalled(appId) {
debugDevice("Checking if app is installed: %s", appId);
const driver = await this.getDriver();
try {
const result = await driver.isAppInstalled(appId);
debugDevice("App %s installed: %s", appId, result);
return result;
} catch (error) {
debugDevice("Error checking if app %s is installed: %s", appId, error.message);
throw new Error(`Failed to check if app ${appId} is installed: ${error.message}`, {
cause: error
});
}
}
/**
* Removes an app
*
* @param appId - App package name
*/
async removeApp(appId) {
debugDevice("Removing app: %s", appId);
const driver = await this.getDriver();
try {
await driver.removeApp(appId);
debugDevice("Successfully removed app: %s", appId);
} catch (error) {
debugDevice("Error removing app %s: %s", appId, error.message);
throw new Error(`Failed to remove app ${appId}: ${error.message}`, {
cause: error
});
}
}
/**
* Gets the current activity
*/
async getCurrentActivity() {
debugDevice("Getting current activity");
const driver = await this.getDriver();
try {
const activity = await driver.getCurrentActivity();
debugDevice("Current activity: %s", activity);
return activity;
} catch (error) {
debugDevice("Error getting current activity: %s", error.message);
throw new Error(`Failed to get current activity: ${error.message}`, {
cause: error
});
}
}
/**
* Gets the current package
*/
async getCurrentPackage() {
debugDevice("Getting current package");
const driver = await this.getDriver();
try {
const pkg = await driver.getCurrentPackage();
debugDevice("Current package: %s", pkg);
return pkg;
} catch (error) {
debugDevice("Error getting current package: %s", error.message);
throw new Error(`Failed to get current package: ${error.message}`, {
cause: error
});
}
}
/**
* Gets the screen orientation
*/
async getScreenOrientation() {
debugDevice("Getting screen orientation");
try {
const size = await this.size();
const orientation = size.width > size.height ? "LANDSCAPE" : "PORTRAIT";
debugDevice("Screen orientation: %s (based on dimensions %dx%d)", orientation, size.width, size.height);
return orientation;
} catch (error) {
debugDevice("Error getting screen orientation: %s", error.message);
throw new Error(`Failed to get screen orientation: ${error.message}`, {
cause: error
});
}
}
/**
* Sets the screen orientation
*
* @param orientation - Orientation to set
*/
async setScreenOrientation(orientation) {
debugDevice("Setting screen orientation to: %s", orientation);
const driver = await this.getDriver();
try {
if (orientation === "LANDSCAPE") {
await driver.rotateDevice(0, 0, 90);
} else {
await driver.rotateDevice(0, 0, 0);
}
debugDevice("Successfully set screen orientation to: %s", orientation);
} catch (error) {
debugDevice("Error setting screen orientation to %s: %s", orientation, error.message);
try {
if (orientation === "LANDSCAPE") {
await driver.pressKeyCode(168);
} else {
await driver.pressKeyCode(169);
}
debugDevice("Successfully set screen orientation using key events");
} catch (keyError) {
debugDevice("Key event rotation also failed: %s", keyError.message);
throw new Error(`Failed to set screen orientation to ${orientation}: ${error.message}`, {
cause: error
});
}
}
}
/**
* Gets the device time
*/
async getDeviceTime() {
debugDevice("Getting device time");
const driver = await this.getDriver();
try {
const time = await driver.getDeviceTime();
debugDevice("Device time: %s", time);
return time;
} catch (error) {
debugDevice("Error getting device time: %s", error.message);
throw new Error(`Failed to get device time: ${error.message}`, {
cause: error
});
}
}
/**
* Hides the keyboard
*/
async hideKeyboard() {
debugDevice("Hiding keyboard");
const driver = await this.getDriver();
try {
await driver.hideKeyboard();
debugDevice("Successfully hid keyboard");
} catch (error) {
debugDevice("Error hiding keyboard: %s", error.message);
throw new Error(`Failed to hide keyboard: ${error.message}`, {
cause: error
});
}
}
/**
* Checks if the keyboard is shown
*/
async isKeyboardShown() {
debugDevice("Checking if keyboard is shown");
const driver = await this.getDriver();
try {
const isShown = await driver.isKeyboardShown();
debugDevice("Keyboard is shown: %s", isShown);
return isShown;
} catch (error) {
debugDevice("Error checking if keyboard is shown: %s", error.message);
throw new Error(`Failed to check if keyboard is shown: ${error.message}`, {
cause: error
});
}
}
/**
* Presses a key code
*
* @param keycode - Key code to press
* @param metastate - Meta state
* @param flags - Flags
*/
async pressKeyCode(keycode, metastate, flags) {
debugDevice("Pressing key code: %d", keycode);
const driver = await this.getDriver();
try {
await driver.pressKeyCode(keycode, metastate, flags);
debugDevice("Successfully pressed key code: %d", keycode);
} catch (error) {
debugDevice("Error pressing key code %d: %s", keycode, error.message);
throw new Error(`Failed to press key code ${keycode}: ${error.message}`, {
cause: error
});
}
}
/**
* Long presses a key code
*
* @param keycode - Key code to press
* @param metastate - Meta state
* @param flags - Flags
*/
async longPressKeyCode(keycode, metastate, flags) {
debugDevice("Long pressing key code: %d", keycode);
const driver = await this.getDriver();
try {
await driver.longPressKeyCode(keycode, metastate, flags);
debugDevice("Successfully long pressed key code: %d", keycode);
} catch (error) {
debugDevice("Error long pressing key code %d: %s", keycode, error.message);
throw new Error(`Failed to long press key code ${keycode}: ${error.message}`, {
cause: error
});
}
}
/**
* Gets available contexts
*/
async getContexts() {
debugDevice("Getting available contexts");
const driver = await this.getDriver();
try {
const contexts = await driver.getContexts();
const contextStrings = contexts.map((ctx) => typeof ctx === "string" ? ctx : ctx.id);
debugDevice("Available contexts: %O", contextStrings);
return contextStrings;
} catch (error) {
debugDevice("Error getting contexts: %s", error.message);
throw new Error(`Failed to get contexts: ${error.message}`, {
cause: error
});
}
}
/**
* Gets the current context
*/
async getCurrentContext() {
debugDevice("Getting current context");
const driver = await this.getDriver();
try {
const context = await driver.getContext();
const contextString = typeof context === "string" ? context : context.id;
debugDevice("Current context: %s", contextString);
return contextString;
} catch (error) {
debugDevice("Error getting current context: %s", error.message);
throw new Error(`Failed to get current context: ${error.message}`, {
cause: error
});
}
}
/**
* Switches to a context
*
* @param contextName - Context to switch to
*/
async switchContext(contextName) {
debugDevice("Switching to context: %s", contextName);
const driver = await this.getDriver();
try {
await driver.switchContext(contextName);
debugDevice("Successfully switched to context: %s", contextName);
} catch (error) {
debugDevice("Error switching to context %s: %s", contextName, error.message);
throw new Error(`Failed to switch to context ${contextName}: ${error.message}`, {
cause: error
});
}
}
/**
* Executes a script
*
* @param script - Script to execute
* @param args - Arguments for the script
*/
async executeScript(script, args) {
debugDevice("Executing script");
const driver = await this.getDriver();
try {
const result = await driver.executeScript(script, args || []);
debugDevice("Successfully executed script");
return result;
} catch (error) {
debugDevice("Error executing script: %s", error.message);
throw new Error(`Failed to execute script: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls to the top of the screen
*
* @param startingPoint - Optional starting point for the scroll
*/
async scrollUntilTop(startingPoint) {
debugDevice("Scrolling to top");
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const end = { x: start.x, y: 0 };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
await this.swipe(size.width / 2, size.height * 0.8, size.width / 2, size.height * 0.2);
}
debugDevice("Successfully scrolled to top");
} catch (error) {
debugDevice("Error scrolling to top: %s", error.message);
throw new Error(`Failed to scroll to top: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls to the bottom of the screen
*
* @param startingPoint - Optional starting point for the scroll
*/
async scrollUntilBottom(startingPoint) {
debugDevice("Scrolling to bottom");
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const end = { x: start.x, y: size.height };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
await this.swipe(size.width / 2, size.height * 0.2, size.width / 2, size.height * 0.8);
}
debugDevice("Successfully scrolled to bottom");
} catch (error) {
debugDevice("Error scrolling to bottom: %s", error.message);
throw new Error(`Failed to scroll to bottom: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls to the left of the screen
*
* @param startingPoint - Optional starting point for the scroll
*/
async scrollUntilLeft(startingPoint) {
debugDevice("Scrolling to left");
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const end = { x: 0, y: start.y };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
await this.swipe(size.width * 0.8, size.height / 2, size.width * 0.2, size.height / 2);
}
debugDevice("Successfully scrolled to left");
} catch (error) {
debugDevice("Error scrolling to left: %s", error.message);
throw new Error(`Failed to scroll to left: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls to the right of the screen
*
* @param startingPoint - Optional starting point for the scroll
*/
async scrollUntilRight(startingPoint) {
debugDevice("Scrolling to right");
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const end = { x: size.width, y: start.y };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
await this.swipe(size.width * 0.2, size.height / 2, size.width * 0.8, size.height / 2);
}
debugDevice("Successfully scrolled to right");
} catch (error) {
debugDevice("Error scrolling to right: %s", error.message);
throw new Error(`Failed to scroll to right: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls up by a specified distance
*
* @param distance - Distance to scroll (default: 200)
* @param startingPoint - Optional starting point for the scroll
*/
async scrollUp(distance = 200, startingPoint) {
debugDevice("Scrolling up by %d pixels", distance);
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const end = { x: start.x, y: Math.max(0, start.y - distance) };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
const startY = size.height / 2;
const endY = Math.max(0, startY - distance);
await this.swipe(size.width / 2, startY, size.width / 2, endY);
}
debugDevice("Successfully scrolled up");
} catch (error) {
debugDevice("Error scrolling up: %s", error.message);
throw new Error(`Failed to scroll up: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls down by a specified distance
*
* @param distance - Distance to scroll (default: 200)
* @param startingPoint - Optional starting point for the scroll
*/
async scrollDown(distance = 200, startingPoint) {
debugDevice("Scrolling down by %d pixels", distance);
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const end = { x: start.x, y: Math.min(size.height, start.y + distance) };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
const startY = size.height / 2;
const endY = Math.min(size.height, startY + distance);
await this.swipe(size.width / 2, startY, size.width / 2, endY);
}
debugDevice("Successfully scrolled down");
} catch (error) {
debugDevice("Error scrolling down: %s", error.message);
throw new Error(`Failed to scroll down: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls left by a specified distance
*
* @param distance - Distance to scroll (default: 200)
* @param startingPoint - Optional starting point for the scroll
*/
async scrollLeft(distance = 200, startingPoint) {
debugDevice("Scrolling left by %d pixels", distance);
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const end = { x: Math.max(0, start.x - distance), y: start.y };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
const startX = size.width / 2;
const endX = Math.max(0, startX - distance);
await this.swipe(startX, size.height / 2, endX, size.height / 2);
}
debugDevice("Successfully scrolled left");
} catch (error) {
debugDevice("Error scrolling left: %s", error.message);
throw new Error(`Failed to scroll left: ${error.message}`, {
cause: error
});
}
}
/**
* Scrolls right by a specified distance
*
* @param distance - Distance to scroll (default: 200)
* @param startingPoint - Optional starting point for the scroll
*/
async scrollRight(distance = 200, startingPoint) {
debugDevice("Scrolling right by %d pixels", distance);
const size = await this.size();
try {
if (startingPoint) {
const start = { x: startingPoint.left, y: startingPoint.top };
const endX = Math.min(size.width, start.x + distance);
const end = { x: endX, y: start.y };
await this.swipe(start.x, start.y, end.x, end.y);
} else {
const startX = size.width / 2;
const endX = Math.min(size.width, startX + distance);
await this.swipe(startX, size.height / 2, endX, size.height / 2);
}
debugDevice("Successfully scrolled right");
} catch (error) {
debugDevice("Error scrolling right: %s", error.message);
throw new Error(`Failed to scroll right: ${error.message}`, {
cause: error
});
}
}
/**
* Performs a swipe gesture using W3C Actions API
*
* @param startX - Starting X coordinate
* @param startY - Starting Y coordinate
* @param endX - Ending X coordinate
* @param endY - Ending Y coordinate
* @param duration - Duration of the swipe in milliseconds (default: 800)
*/
async swipe(startX, startY, endX, endY, duration = 800) {
debugDevice("Swiping from (%d, %d) to (%d, %d)", startX, startY, endX, endY);
const driver = await this.getDriver();
try {
await driver.performActions([{
type: "pointer",
id: "finger1",
parameters: { pointerType: "touch" },
actions: [
{ type: "pointerMove", duration: 0, x: startX, y: startY },
{ type: "pointerDown", button: 0 },
{ type: "pause", duration: 100 },
{ type: "pointerMove", duration, x: endX, y: endY },
{ type: "pointerUp", button: 0 }
]
}]);
debugDevice("Successfully performed swipe");
} catch (error) {
debugDevice("Error performing swipe: %s", error.message);
throw new Error(`Failed to perform swipe: ${error.message}`, {
cause: error
});
}
}
/**
* Presses the back button
*/
async back() {
debugDevice("Pressing back button");
const driver = await this.getDriver();
try {
await driver.back();
debugDevice("Successfully pressed back button");
} catch (error) {
debugDevice("Error pressing back button: %s", error.message);
throw new Error(`Failed to press back button: ${error.message}`, {
cause: error
});
}
}
/**
* Presses the home button
*/
async home() {
debugDevice("Pressing home button");
const driver = await this.getDriver();
try {
await driver.pressKeyCode(3);
debugDevice("Successfully pressed home button");
} catch (error) {
debugDevice("Error pressing home button: %s", error.message);
throw new Error(`Failed to press home button: ${error.message}`, {
cause: error
});
}
}
/**
* Opens the recent apps screen
*/
async recentApps() {
debugDevice("Opening recent apps");
const driver = await this.getDriver();
try {
await driver.pressKeyCode(187);
debugDevice("Successfully opened recent apps");
} catch (error) {
debugDevice("Error opening recent apps: %s", error.message);
throw new Error(`Failed to open recent apps: ${error.message}`, {
cause: error
});
}
}
/**
* Gets the elements info (deprecated, use getElementsNodeTree instead)
*/
async getElementsInfo() {
debugDevice("Getting elements info (deprecated)");
const tree = await this.getElementsNodeTree();
const elements = [];
const traverse = (node) => {
if (node.node) {
elements.push(node.node);
}
for (const child of node.children) {
traverse(child);
}
};
traverse(tree);
return elements;
}
/**
* Mouse actions
*/
get mouse() {
return {
click: async (x, y) => {
debugDevice("Mouse click at (%d, %d)", x, y);
await this.tap(x, y);
},
wheel: async (deltaX, deltaY) => {
debugDevice("Mouse wheel with deltaX: %d, deltaY: %d", deltaX, deltaY);
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX > 0) {
await this.scrollRight(Math.abs(deltaX));
} else {
await this.scrollLeft(Math.abs(deltaX));
}
} else {
if (deltaY > 0) {
await this.scrollDown(Math.abs(deltaY));
} else {
await this.scrollUp(Math.abs(deltaY));
}
}
},
move: async (x, y) => {
debugDevice("Mouse move to (%d, %d)", x, y);
},
drag: async (from, to) => {
debugDevice("Mouse drag from (%d, %d) to (%d, %d)", from.x, from.y, to.x, to.y);
await this.swipe(from.x, from.y, to.x, to.y);
}
};
}
/**
* Keyboard actions using W3C Actions API
*/
get keyboard() {
return {
type: async (text) => {
debugDevice("Keyboard type: %s", text);
if (!text)
return;
const driver = await this.getDriver();
const isChinese = /[\p{Script=Han}\p{sc=Hani}]/u.test(text);
if (!isChinese) {
try {
await driver.keys(text);
} catch (error) {
await driver.performActions([{
type: "key",
id: "keyboard",
actions: text.split("").flatMap((char) => [
{ type: "keyDown", value: char },
{ type: "keyUp", value: char }
])
}]);
}
} else {
try {
await driver.keys(text);
} catch (error) {
debugDevice("Error typing Chinese text: %s", error.message);
try {
const inputElements = await driver.$$("//android.widget.EditText");
if (inputElements.length > 0) {
await inputElements[0].setValue(text);
} else {
throw new Error("No input elements found");
}
} catch (setValueError) {
debugDevice("setValue also failed: %s", setValueError.message);
throw error;
}
}
}
try {
await this.hideKeyboard();
} catch (error) {
debugDevice("Could not hide keyboard: %s", error.message);
}
},
press: async (action) => {
debugDevice("Keyboard press: %O", action);
const driver = await this.getDriver();
const pressKey = async (key) => {
const keyCodeMap = {
"Enter": 66,
"Tab": 61,
"Backspace": 67,
"Delete": 112,
"Escape": 111,
"ArrowUp": 19,
"ArrowDown": 20,
"ArrowLeft": 21,
"ArrowRight": 22,
"Home": 122,
"End": 123,
"PageUp": 92,
"PageDown": 93,
"Space": 62
};
if (key in keyCodeMap) {
await driver.pressKeyCode(keyCodeMap[key]);
} else if (key.length === 1) {
await driver.performActions([{
type: "key",
id: "keyboard",
actions: [
{ type: "keyDown", value: key },
{ type: "keyUp", value: key }
]
}]);
}
};
if (Array.isArray(action)) {
for (const act of action) {
await pressKey(act.key);
}
} else {
await pressKey(action.key);
}
}
};
}
/**
* Clears input in an element
*
* @param element - Element to clear
*/
async clearInput(element) {
debugDevice("Clearing input in element: %s", element.id);
try {
const driver = await this.getDriver();
await this.tap(element.center[0], element.center[1]);
try {
if (element.attributes["resource-id"]) {
const elem = await driver.$(`[resource-id="${element.attributes["resource-id"]}"]`);
if (await elem.isExisting()) {
await elem.clearValue();
debugDevice("Successfully cleared input using clearValue");
return;
}
}
} catch (error) {
debugDevice("clearValue method failed: %s", error.message);
}
try {
await driver.pressKeyCode(29, 1);
await new Promise((resolve) => setTimeout(resolve, 100));
await driver.pressKeyCode(67);
debugDevice("Successfully cleared input using key events");
} catch (error) {
debugDevice("Key events method failed: %s", error.message);
try {
for (let i = 0; i < 50; i++) {
await driver.pressKeyCode(67);
}
debugDevice("Successfully cleared input using multiple backspaces");
} catch (backspaceError) {
debugDevice("Backspace method also failed: %s", backspaceError.message);
throw new Error("All clear input methods failed");
}
}
} catch (error) {
debugDevice("Error clearing input: %s", error.message);
throw new Error(`Failed to clear input: ${error.message}`, {
cause: error
});
}
}
/**
* Performs a tap at the specified coordinates using W3C Actions API
*
* @param x - X coordinate
* @param y - Y coordinate
*/
async tap(x, y) {
debugDevice("Tapping at (%d, %d)", x, y);
const driver = await this.getDriver();
try {
await driver.performActions([{
type: "pointer",
id: "finger1",
parameters: { pointerType: "touch" },
actions: [
{ type: "pointerMove", duration: 0, x, y },
{ type: "pointerDown", button: 0 },
{ type: "pause", duration: 100 },
{ type: "pointerUp", button: 0 }
]
}]);
debugDevice("Successfully tapped at (%d, %d)", x, y);
} catch (error) {
debugDevice("Error tapping at (%d, %d): %s", x, y, error.message);
throw new Error(`Failed to tap at (${x}, ${y}): ${error.message}`, {
cause: error
});
}
}
/**
* Gets XPaths for elements with the specified ID
*
* @param id - Element ID to search for
*/
async getXpathsById(id) {
debugDevice("Getting XPaths for ID: %s", id);
const driver = await this.getDriver();
try {
const elements = await driver.$$(`[resource-id="${id}"]`);
const xpaths = [];
for (let i = 0; i < elements.length; i++) {
xpaths.push(`//*[@resource-id="${id}"][${i + 1}]`);
}
debugDevice("Found %d XPaths for ID %s", xpaths.length, id);
return xpaths;
} catch (error) {
debugDevice("Error getting XPaths for ID %s: %s", id, error.message);
throw new Error(`Failed to get XPaths for ID ${id}: ${error.message}`, {
cause: error
});
}
}
/**
* Gets element info by XPath
*
* @param xpath - XPath to search for
*/
async getElementInfoByXpath(xpath) {
debugDevice("Getting element info for XPath: %s", xpath);
const driver = await this.getDriver();
try {
const element = await driver.$(xpath);
if (!await element.isExisting()) {
throw new Error(`Element not found for XPath: ${xpath}`);
}
const location = await element.getLocation();
const size = await element.getSize();
const text = await element.getText();
const resourceId = await element.getAttribute("resource-id");
const className = await element.getAttribute("class");
const contentDesc = await element.getAttribute("content-desc");
const elementInfo = {
id: resourceId || `xpath-${Date.now()}`,
indexId: 0,
nodeHashId: resourceId || xpath,
locator: xpath,
attributes: {
nodeType: this.getNodeTypeFromClassName(className),
"resource-id": resourceId,
"class": className,
"content-desc": contentDesc
},
nodeType: this.getNodeTypeFromClassName(className),
content: text || contentDesc || "",
rect: {
left: location.x,
top: location.y,
width: size.width,
height: size.height
},
center: [location.x + size.width / 2, location.y + size.height / 2]
};
debugDevice("Successfully got element info for XPath: %s", xpath);
return elementInfo;
} catch (error) {
debugDevice("Error getting element info for XPath %s: %s", xpath, error.message);
throw new Error(`Failed to get element info for XPath ${xpath}: ${error.message}`, {
cause: error
});
}
}
/**
* Helper method to determine node type from class name
*/
getNodeTypeFromClassName(className) {
if (!className)
return NodeType.CONTAINER;
const lowerClassName = className.toLowerCase();
if (lowerClassName.includes("button"))
return NodeType.BUTTON;
if (lowerClassName.includes("text") || lowerClassName.includes("edit"))
return NodeType.TEXT;
if (lowerClassName.includes("image"))
return NodeType.IMG;
if (lowerClassName.includes("input") || lowerClassName.includes("edit"))
return NodeType.FORM_ITEM;
return NodeType.CONTAINER;
}
/**
* Gets device information including screen size and orientation
*/
async getDeviceInfo() {
debugDevice("Getting device information");
try {
const [screenSize, orientation, deviceTime, currentPackage, currentActivity] = await Promise.all([
this.size(),
this.getScreenOrientation(),
this.getDeviceTime(),
this.getCurrentPackage(),
this.getCurrentActivity()
]);
const deviceInfo = {
screenSize,
orientation,
deviceTime,
currentPackage,
currentActivity
};
debugDevice("Device info: %O", deviceInfo);
return deviceInfo;
} catch (error) {
debugDevice("Error getting device info: %s", error.message);
throw new Error(`Failed to get device info: ${error.message}`, {
cause: error
});
}
}
/**
* Waits for an element to appear on screen
*
* @param selector - Element selector
* @param timeout - Timeout in milliseconds (default: 10000)
*/
async waitForElement(selector, timeout = 1e4) {
debugDevice("Waiting for element: %s (timeout: %dms)", selector, timeout);
const driver = await this.getDriver();
try {
const element = await driver.$(selector);
await element.waitForExist({ timeout });
debugDevice("Element found: %s", selector);
return element;
} catch (error) {
debugDevice("Element not found within timeout: %s", selector);
throw new Error(`Element not found within ${timeout}ms: ${selector}`, {
cause: error
});
}
}
/**
* Waits for an element to disappear from screen
*
* @param selector - Element selector
* @param timeout - Timeout in milliseconds (default: 10000)
*/
async waitForElementToDisappear(selector, timeout = 1e4) {
debugDevice("Waiting for element to disappear: %s (timeout: %dms)", selector, timeout);
const driver = await this.getDriver();
try {
const element = await driver.$(selector);
await element.waitForExist({ timeout, reverse: true });
debugDevice("Element disappeared: %s", selector);
} catch (error) {
debugDevice("Element did not disappear within timeout: %s", selector);
throw new Error(`Element did not disappear within ${timeout}ms: ${selector}`, {
cause: error
});
}
}
/**
* Performs a long press at the specified coordinates
*
* @param x - X coordinate
* @param y - Y coordinate
* @param duration - Duration of the long press in milliseconds (default: 1000)
*/
async longPress(x, y, duration = 1e3) {
debugDevice("Long pressing at (%d, %d) for %dms", x, y, duration);
const driver = await this.getDriver();
try {
await driver.performActions([{
type: "pointer",
id: "finger1",
parameters: { pointerType: "touch" },
actions: [
{ type: "pointerMove", duration: 0, x, y },
{ type: "pointerDown", button: 0 },
{ type: "pause", duration },
{ type: "pointerUp", button: 0 }
]
}]);
debugDevice("Successfully performed long press at (%d, %d)", x, y);
} catch (error) {
debugDevice("Error performing long press at (%d, %d): %s", x, y, error.message);
throw new Error(`Failed to perform long press at (${x}, ${y}): ${error.message}`, {
cause: error
});
}
}
/**
* Performs a double tap at the specified coordinates
*
* @param x - X coordinate
* @param y - Y coordinate
*/
async doubleTap(x, y) {
debugDevice("Double tapping at (%d, %d)", x, y);
try {
await this.tap(x, y);
await new Promise((resolve) => setTimeout(resolve, 100));
await this.tap(x, y);
debugDevice("Successfully performed double tap at (%d, %d)", x, y);
} catch (error) {
debugDevice("Error performing double tap at (%d, %d): %s", x, y, error.message);
throw new Error(`Failed to perform double tap at (${x}, ${y}): ${error.message}`, {
cause: error
});
}
}
/**
* Destroys the device connection
*/
async destroy() {
debugDevice("Destroying device connection");
await this.disconnect();
}
};
// src/agent/index.ts
import { PageAgent } from "misoai-web/agent";
import { vlLocateMode } from "misoai-shared/env";
var AndroidAgent = class extends PageAgent {
constructor(page, opts) {
super(page, opts);
if (!vlLocateMode()) {
throw new Error(
"Android Agent only supports vl-model. https://acabai.com/choose-a-model.html"
);
}
}
async launch(uri) {
const device = this.page;
await device.launch(uri);
}
};
async function agentFromAppiumServer(config, capabilities, agentOpts) {
const device = new AppiumDevice(config, capabilities);
try {
await device.connect();
return new AndroidAgent(device, agentOpts);
} catch (error) {
debugDevice("Failed to connect to Appium server: %s", error.message);
throw new Error(`Failed to connect to Appium server: ${error.message}`, {
cause: error
});
}
}
async function agentFromLocalAppium(capabilities, agentOpts) {
const localServerConfig = {
hostname: "127.0.0.1",
port: 4723,
protocol: "http"
};
return agentFromAppiumServer(localServerConfig, capabilities, agentOpts);
}
async function agentFromSauceLabs(slConfig, capabilities, agentOpts) {
const sauceServerConfig = {
hostname: `ondemand.${slConfig.region}.saucelabs.com`,
port: 443,
protocol: "https",
path: "/wd/hub"
};
if (!capabilities["sauce:options"]) {
capabilities["sauce:options"] = {};
}
capabilities["sauce:options"].username = slConfig.user;
capabilities["sauce:options"].accessKey = slConfig.key;
return agentFromAppiumServer(sauceServerConfig, capabilities, agentOpts);
}
// src/index.ts
import { overrideAIConfig } from "misoai-shared/env";
// src/performance/index.ts
var PerformanceMonitor = class {
/**
* Creates a new PerformanceMonitor instance
*
* @param device - AppiumDevice instance
* @param defaultPackageName - Optional default package name to use if active package detection fails
*/
constructor(device, defaultPackageName) {
this.metrics = [];
this.monitoringInterval = null;
this.availableMetrics = [];
this.lastActivePackage = "";
this.device = device;
this.defaultPackageName = defaultPackageName;
}
/**
* Gets the currently active package name
*/
async getActivePackage() {
try {
const currentPackage = await this.device.getCurrentPackage();
if (currentPackage) {
this.lastActivePackage = currentPackage;
return currentPackage;
}
} catch (error) {
debugDevice("Error getting active package: %s", error.message);
}
if (this.lastActivePackage) {
return this.lastActivePackage;
}
if (this.defaultPackageName) {
return this.defaultPackageName;
}
return "unknown.package";
}
/**
* Initializes the performance monitor
*/
async initialize() {
debugDevice("Initializing performance monitor");
const driver = await this.device["getDriver"]();
this.availableMetrics = await driver.getPerformanceDataTypes();
debugDevice("Available performance metrics: %O", this.availableMetrics);
return this.availableMetrics;
}
/**
* Gets device information
*/
async getDeviceInfo() {
debugDevice("Getting device information");
const driver = await this.device["getDriver"]();
const executeShellCommand = async (command) => {
return await driver.executeScript("mobile: shell", [{
command
}]);
};
const model = await executeShellCommand("getprop ro.product.model");
const manufacturer = await executeShellCommand("getprop ro.product.manufacturer");
const androidVersion = await executeShellCommand("getprop ro.build.version.release");
const cpuArchitecture = await executeShellCommand("uname -m");
const cpuCores = parseInt(await executeShellCommand("cat /proc/cpuinfo | grep processor | wc -l"), 10);
const totalRam = await executeShellCommand("cat /proc/meminfo | grep MemTotal");
const screenDensity = await executeShellCommand("wm density");
const deviceInfo = {
model: model.trim(),
manufacturer: manufacturer.trim(),
androidVersion: androidVersion.trim(),
cpuArchitecture: cpuArchitecture.trim(),
cpuCores: isNaN(cpuCores) ? 0 : cpuCores,
totalRam: totalRam.trim(),
screenDensity: screenDensity.trim()
};
debugDevice("Device information: %O", deviceInfo);
return deviceInfo;
}
/**
* Gets current performance metrics
*/
async getCurrentMetrics() {
debugDevice("Getting current performance metrics");
const driver = await this.device["getDriver"]();
const activePackage = await this.getActivePackage();
debugDevice("Getting performance metrics for active package: %s", activePackage);
const metrics = {
timestamp: Date.now(),
packageName: activePackage
};
try {
if (this.availableMetrics.includes("cpuinfo")) {
const cpuData = await driver.getPerformanceData(activePackage, "cpuinfo", 1);
if (cpuData && cpuData.length > 1) {
const headers = cpuData[0];
const values = cpuData[1];
const userIndex = headers.indexOf("user");
const systemIndex = headers.indexOf("system");
const idleIndex = headers.indexOf("idle");
const totalIndex = headers.indexOf("total");
metrics.cpuInfo = {
user: userIndex >= 0 ? parseFloat(values[userIndex]) : 0,
system: systemIndex >= 0 ? parseFloat(values[systemIndex]) : 0,
idle: idleIndex >= 0 ? parseFloat(values[idleIndex]) : 0,
total: totalIndex >= 0 ? parseFloat(values[totalIndex]) : 0
};
}
}
if (this.availableMetrics.includes("memoryinfo")) {
const memData = await driver.getPerformanceData(activePackage, "memoryinfo", 1);
if (memData && memData.length > 1) {
const headers = memData[0];
const values = memData[1];
const totalIndex = headers.indexOf("totalMem");
const freeIndex = headers.indexOf("freeMem");
const totalMem = totalIndex >= 0 ? parseInt(values[totalIndex], 10) : 0;
const freeMem = freeIndex >= 0 ? parseInt(values[freeIndex], 10) : 0;
const usedMem = totalMem - freeMem;
const usedMemPercent = totalMem > 0 ? usedMem / totalMem * 100 : 0;
metrics.memoryInfo = {
totalMem,
freeMem,
usedMem,
usedMemPercent
};
}
}
if (this.availableMetrics.includes("batteryinfo")) {
const batteryData = await driver.getPerformanceData(activePackage, "batteryinfo", 1);
if (batteryData && batteryData.length > 1) {
const headers = batteryData[0];
const values = batteryData[1];
const levelIndex = headers.indexOf("level");
const statusIndex = headers.indexOf("status");
const tempIndex = headers.indexOf("temperature");
metrics.batteryInfo = {
level: levelIndex >= 0 ? parseInt(values[levelIndex], 10) : 0,
status: statusIndex >= 0 ? values[statusIndex] : "",
temperature: tempIndex >= 0 ? parseInt(values[tempIndex], 10) / 10 : 0
};
}
}
if (this.availableMetrics.includes("networkinfo")) {
const networkData = await driver.getPerformanceData(activePackage, "networkinfo", 1);
if (networkData && networkData.length > 1) {
const headers = networkData[0];
const values = networkData[1];
const rxBytesIndex = headers.indexOf("rxBytes");
const txBytesIndex = headers.indexOf("txBytes");
const rxPacketsIndex = headers.indexOf("rxPackets");
const