UNPKG

misoai-android

Version:

Appium-based Android automation library for misoAI

1,520 lines (1,515 loc) 58.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { AndroidAgent: () => AndroidAgent, AppiumDevice: () => AppiumDevice, PerformanceMonitor: () => PerformanceMonitor, agentFromAppiumServer: () => agentFromAppiumServer, agentFromLocalAppium: () => agentFromLocalAppium, agentFromSauceLabs: () => agentFromSauceLabs, overrideAIConfig: () => import_env2.overrideAIConfig, startVideoRecording: () => startVideoRecording, stopVideoRecording: () => stopVideoRecording, takeScreenshot: () => takeScreenshot }); module.exports = __toCommonJS(src_exports); // src/page/appium-device.ts var import_constants = require("misoai-shared/constants"); var import_logger = require("misoai-shared/logger"); var import_webdriverio = require("webdriverio"); var debugDevice = (0, import_logger.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 (0, import_webdriverio.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: import_constants.NodeType.CONTAINER }, nodeType: import_constants.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 import_constants.NodeType.CONTAINER; const lowerClassName = className.toLowerCase(); if (lowerClassName.includes("button")) return import_constants.NodeType.BUTTON; if (lowerClassName.includes("text") || lowerClassName.includes("edit")) return import_constants.NodeType.TEXT; if (lowerClassName.includes("image")) return import_constants.NodeType.IMG; if (lowerClassName.includes("input") || lowerClassName.includes("edit")) return import_constants.NodeType.FORM_ITEM; return import_constants.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 var import_agent = require("misoai-web/agent"); var import_env = require("misoai-shared/env"); var AndroidAgent = class extends import_agent.PageAgent { constructor(page, opts) { super(page, opts); if (!(0, import_env.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 var import_env2 = require("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