multi-automator
Version:
Multi terminal automation
433 lines (432 loc) • 13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
/**
* @desc: 设备操控类
* @author: john_chen
* @date: 2023.03.14
*/
const fs_1 = require("fs");
const xml_js_1 = require("xml-js");
const config_1 = require("./config");
const time_1 = require("./utils/time");
/**
* 设备操控类
*/
class Device {
/**
* 构造函数
*
* @param {string} deviceId 设备 ID
* @param {string} deviceType 设备类型
*/
constructor(options) {
const { deviceId, deviceType } = options;
this.id = deviceId || 'web-device-id';
this.type = deviceType;
this.handler = null;
}
/**
* 初始化函数
*
* @param {Promise{any}} handler 设备处理对象
*/
async init(handler) {
this.handler = handler;
}
/**
* 跳转页面
*
* @param {string} path url 地址
* @returns {string}
*/
async goto(path) {
if (this.handler.goto) {
config_1.logger.info(`[device.goto] ${path}`);
return await this.handler.goto(path);
}
throw new Error('goto method not implemented');
}
/**
* home 键
*/
async home() {
if (this.handler.home) {
config_1.logger.info('[device.home]');
await this.handler.home();
}
}
/**
* 获取当前设备页面 dom 树
*
* @param {string} path 存储路径
* @param {string} format 返回格式
* @param {number} timeout 操作超时时间(ms)
* @return {Promise{string}}
*/
async source(options = {}) {
config_1.logger.info('[device.source]');
const { timeout = 30000, format = 'string', path = '' } = options;
try {
let res = await this.handler.source(timeout);
if (path) {
(0, fs_1.writeFileSync)(path, res);
}
return this.formatResponse(res, format);
}
catch (err) {
config_1.logger.error(`[device.source] ${err instanceof Error ? err.stack : err}`);
throw err;
}
}
/**
* 通过 xpath 获取设备元素
*
* @param {string} expression XPath表达式
* @param {object} options
* @param {number} options.loop 轮询次数,默认 3
* @param {number} options.duration 轮询时间间隔(ms),默认 1000
* @param {number} options.retry 查询异常重试次数,默认 3
* @returns {Promise{Array{WebElement}}}
*/
async $x(expression, options = {
loop: 3,
duration: 1000,
retry: 3,
}) {
config_1.logger.info(`[device.$x] ${expression}`);
let { loop = 6, duration = 1000, retry = 3 } = options;
let retryCount = 0;
let elements = [];
for (let count = -1; count < loop; count++) {
try {
elements = await this.handler.$x(expression);
}
catch (err) {
retryCount++;
config_1.logger.error(`[device.$x] retry: ${retryCount}, error: ${err.message}`);
if (retryCount > retry) {
throw new Error(`find elements error: ${err.message}`);
}
}
if (elements.length > 0) {
break;
}
config_1.logger.info(`[device.$x] retry: ${retryCount}, duration: ${duration}`);
await (0, time_1.delay)(duration);
}
return elements;
}
/**
* 通过 CSS 选择器获取元素操作对象 - 仅支持 web 设备
*
* @param selector CSS 选择器
* @param {object} options
* @param {number} options.loop 轮询次数,默认 3
* @param {number} options.duration 轮询时间间隔(ms),默认 1000
* @param {number} options.retry 查询异常重试次数,默认 3
* @returns @returns {Promise{WebElement|null}}}
*/
async $(selector, options = {
loop: 3,
duration: 1000,
retry: 3,
}) {
if (this.type !== 'web') {
throw new Error('not support');
}
config_1.logger.info('[device.$]');
let { loop = 6, duration = 1000, retry = 3 } = options;
let retryCount = 0;
let element = null;
for (let count = -1; count < loop; count++) {
try {
element = await this.handler.$(selector);
}
catch (err) {
retryCount++;
if (retryCount > retry) {
throw new Error(`寻找元素异常:${err.message}`);
}
}
if (null !== element) {
break;
}
await (0, time_1.delay)(duration);
}
return element;
}
/**
* 通过 CSS 选择器获取元素操作对象列表 - 仅支持 web 设备
*
* @param selector CSS 选择器
* @param {object} options
* @param {number} options.loop 轮询次数,默认 3
* @param {number} options.duration 轮询时间间隔(ms),默认 1000
* @param {number} options.retry 查询异常重试次数,默认 3
* @returns @returns {Promise{Array{WebElement}}}
*/
async $$(selector, options = {
loop: 3,
duration: 1000,
retry: 3,
}) {
if (this.type !== 'web') {
throw new Error('not support');
}
config_1.logger.info('[device.$$]');
let { loop = 6, duration = 1000, retry = 3 } = options;
let retryCount = 0;
let elements = [];
for (let count = -1; count < loop; count++) {
try {
elements = await this.handler.$$(selector);
}
catch (err) {
retryCount++;
if (retryCount > retry) {
throw new Error(`寻找元素异常:${err.message}`);
}
}
if (elements.length > 0) {
break;
}
await (0, time_1.delay)(duration);
}
return elements;
}
/**
* 设备截图
*
* @param {string} path 图片路径
* @returns {Promise{Buffer|String}}
*/
async screenshot(options = {}) {
if (this.handler.screenshot) {
config_1.logger.info('[device.screenshot]');
const { path = '' } = options;
return await this.handler.screenshot(path);
}
throw new Error('screenshot method not implemented');
}
/**
* 获取设备屏幕宽高
*
* @return {object} screenInfo
* @return {number} screenInfo.width 真实宽
* @return {number} screenInfo.height 真实高
*/
async getScreenSize() {
config_1.logger.info('[device.getScreenSize]');
try {
return await this.handler.getScreenSize();
}
catch (err) {
config_1.logger.error(`[device.getScreenSize] ${err.stack}`);
throw err;
}
}
/**
* 屏幕点击
*
* @param {number} x 横坐标
* @param {number} y 纵坐标
* @return {Promise}
*/
async tap(x, y) {
if (!this.handler.tap) {
throw new Error('tap method not implemented');
}
await this.handler.tap(x, y);
config_1.logger.info(`[device.tap] ${x}, ${y}`);
}
/**
* 长按屏幕
*
* @param {number} x 横坐标
* @param {number} y 纵坐标
* @param {number} duration 长按时间(ms)
*/
async longpress(x, y, duration = 3000) {
if (!this.handler.longpress) {
throw new Error('longpress method not implemented');
}
await this.handler.longpress(x, y, duration);
config_1.logger.info(`[device.longpress] ${x}, ${y}, ${duration}`);
}
/**
* 屏幕滑动
*
* @param {number} fx 起点横坐标
* @param {number} fy 起点纵坐标
* @param {number} tx 终点横坐标
* @param {number} ty 终点纵坐标
* @param {Object} options
* @return {Promise}
*/
async swipe(fx, fy, tx, ty, options = {
duration: 300,
startPressDuration: 0
}) {
let res = await this.handler.swipe(fx, fy, tx, ty, options);
config_1.logger.info(`[device.swipe] ${fx}, ${fy}, ${tx}, ${ty}`);
return res;
}
/**
* 输入文本
*
* @param {string} text 文本
*/
async input(text) {
if (this.handler.input) {
return await this.handler.input(text);
}
throw new Error('input method not implemented');
}
/**
* 获取版本信息
*
* @returns {Promise}
*/
async version() {
if (this.handler.version) {
let res = await this.handler.version();
config_1.logger.info(`[device.version] ${res}`);
return res;
}
throw new Error('version method not implemented');
}
/**
* 获取应用列表
*
* @returns {Promise{Array{AppInfo}}}
*/
async appList() {
if (this.handler.appList) {
return await this.handler.appList();
}
throw new Error('appList method not implemented');
}
/**
* 获取应用信息
*
* @param {string} packageName 包名
* @returns {Promise{AppInfo}}
*/
async appInfo(packageName) {
if (this.handler.appInfo) {
return await this.handler.appInfo(packageName);
}
throw new Error('appInfo method not implemented');
}
/**
* 判断应用是否已安装
*
* @param {string} appId 应用ID
* @returns {Promise{boolean}}
*/
async isInstalled(packageName) {
if (this.type === 'android') {
// android 使用 abd 判断,效率更高
return await this.handler.isInstalled(packageName);
}
const appList = await this.appList();
return appList.some(app => app.appId === packageName);
}
/**
* 安装应用
*
* @param {string} appPath 应用路径
* @returns {Promise}
*/
async install(appPath) {
if (this.handler.installApp) {
config_1.logger.info(`[device.install] ${appPath}`);
return await this.handler.installApp(appPath);
}
throw new Error('install method not implemented');
}
/**
* 卸载应用
*
* @param {string} appId 应用ID
* @returns {Promise}
*/
async uninstall(appId) {
if (this.handler.uninstallApp) {
config_1.logger.info(`[device.uninstall] ${appId}`);
return await this.handler.uninstallApp(appId);
}
throw new Error('uninstall method not implemented');
}
/**
* 启动 APP
*
* @param packageName 包名
* @param {string} activity 启动 Activity
* @returns {Promise}
*/
async launchApp(packageName, activity) {
if (this.handler.launchApp) {
config_1.logger.info(`[device.launchApp] ${packageName}`);
if (this.type === 'android') {
return await this.handler.launchApp(packageName, activity);
}
return await this.handler.launchApp(packageName);
}
throw new Error('launchApp method not implemented');
}
/**
* 终止 APP
*
* @param packageName 包名
*/
async terminateApp(packageName) {
if (this.handler.terminateApp) {
config_1.logger.info(`[device.terminateApp] ${packageName}`);
return await this.handler.terminateApp(packageName);
}
throw new Error('terminateApp method not implemented');
}
/**
* 激活 APP
*
* @param packageName 包名
*/
async activateApp(packageName) {
if (this.handler.activateApp) {
config_1.logger.info(`[device.activateApp] ${packageName}`);
return await this.handler.activateApp(packageName);
}
throw new Error('activateApp method not implemented');
}
/**
* 重新启动 APP
*
* @param packageName 包名
*/
async relaunchApp(packageName) {
config_1.logger.info(`[device.relaunchApp] ${packageName}`);
await this.terminateApp(packageName);
await this.launchApp(packageName);
}
/**
* 关闭设备操控实例
*/
async close() {
if (this.handler.close) {
config_1.logger.info('[device.close]');
await this.handler.close();
}
}
formatResponse(res, format) {
switch (format) {
case 'json':
return (0, xml_js_1.xml2json)(res, { compact: false });
case 'object':
return JSON.parse((0, xml_js_1.xml2json)(res, { compact: false }));
default:
return res;
}
}
}
exports.default = Device;