const { uploadPic } = require('./captchaSolve');
const { createMyLogger } = require('./myLogger');
const log = createMyLogger();
/*****所有appium自动化都可以用上的动作*****/
/**
* wdio基本操作封装类
* @class
*/
class MyAction {
constructor(client) {
/**
* wdio 创建的实例
* @param {WebdriverIO.Browser} client
*/
this.client = client
}
/**
* UiSelector不同形式的选择器,可以组合。
* 示例['id-class-text','id:aaabbb','android.view.view','文本']
* @param {string} type
* @param {Array<string>} opt
*/
uis(type, opt) {
let baseStr = 'android=new UiSelector()'
let typeArr = type.split('-')
for (let i = 0; i < typeArr.length; i++) {
switch (typeArr[i]) {
case 'class':
baseStr = baseStr + `.className("${opt[i]}")`
break;
case 'id':
baseStr = baseStr + `.resourceId("${opt[i]}")`
break;
case 'text':
baseStr = baseStr + `.text("${opt[i]}")`
break;
case 'desc':
baseStr = baseStr + `.description("${opt[i]}")`
break;
case 'findText':
baseStr = baseStr + `.textContains("${opt[i]}")`
break;
case 'findDesc':
baseStr = baseStr + `.descriptionContains("${opt[i]}")`
break;
case 'textReg':
baseStr = baseStr + `.textMatches("${opt[i]}")`
break;
case 'descReg':
baseStr = baseStr + `.descriptionMatches("${opt[i]}")`
break;
case 'child':
baseStr = baseStr + `.instance(${opt[i]})`
break;
case 'index':
baseStr = baseStr + `.index(${opt[i]})`
break;
default:
throw new Error(`Uis error, unknow type : ${typeArr[i]}`)
}
}
return baseStr
}
/**
* 处理传入的selector。数组通过uis转换为字符串。
* @param {string | any[]} str
*/
handleSelector(str) {
if (typeof str !== 'string') {
let actionArr = []
for (let i = 1; i < str.length; i++) {
actionArr.push(str[i]);
}
return this.uis(str[0], actionArr)
}
else return str
}
/**
* 等待元素出现,返回该元素
* 示例 let element = await act.waitForElement(['id-class', 'cn.some:id', 'android.view.view']);
* @param {string | string[]} selector
*/
async waitForElement(selector, time = 30000, parent = null) {
selector = this.handleSelector(selector);
let target
if (!parent) {
target = await this.client.$(selector);
}
else {
target = await parent.$(selector);
}
await target.waitForExist({ timeout: time });
return target
}
/**
* 等待元素出现并点击
* 示例 await act.clickElement(['id-text', 'cn.some:id', '按钮']);
* @param {string | string[]} selector
*/
async clickElement(selector, time = 30000, parent = null) {
selector = this.handleSelector(selector);
let target
if (!parent) {
target = await this.client.$(selector);
}
else {
target = await parent.$(selector);
}
await target.waitForExist({ timeout: time });
await target.touchAction('tap');
}
/**
* 等待元素出现,返回该元素
* 示例 let element = await act.waitForElement(['id-class', 'cn.some:id', 'android.view.view']);
* @param {string | string[]} selector
* @param {string} type
*/
async getElementAttribute(selector, type, time = 30000, parent = null) {
selector = this.handleSelector(selector);
let target
if (!parent) {
target = await this.client.$(selector);
}
else {
target = await parent.$(selector);
}
await target.waitForExist({ timeout: time });
if (type === 'text') {
return await target.getText();
}
else return await target.getAttribute(type);
}
/**
* 等待input出现并输入文字,是否清除原文字可选,不会触发键盘弹出(某些权限响应)
* 示例 await act.typeInput(["id-text", "cn.some:name", "请填写姓名"],'myName');
* @param {string | string[]} selector
* @param {any} value
*/
async typeInput(selector, value, clear = false, time = 30000) {
selector = this.handleSelector(selector);
let target = await this.client.$(selector);
await target.waitForExist({ timeout: time });
clear && await target.clearValue();
await target.addValue(value);
}
/**
* 获取多个内容,可以输入限定区域已确保都能点击到,限制区域x1,x2 ,y1,y2
* 示例 let ele = await act.getElements(['id', 'com.ss.android.ugc.aweme.lite:id/avatar'],[0,1080,600,1920]);
* @param {string | string[]} selector
* @param {number[]} limitArea
*/
async getElements(selector, limitArea, parent = null, time = 15000) {
selector = this.handleSelector(selector);
try {
await this.waitForElement(selector, time);
}
catch {
return []
}
let targets = []
if (!parent) {
targets = await this.client.$$(selector);
}
else {
targets = await parent.$$(selector);
}
if (limitArea) {
let result = []
for (let i = 0; i < targets.length; i++) {
let box = await this.getElementBox(targets[i])
let pass = (box[0] > limitArea[0]) && (box[0] < limitArea[1]) && (box[1] > limitArea[2]) && (box[1] < limitArea[3])
if (pass) result.push(targets[i])
// else console.log('不在区域内', box, limitArea);
}
return result
}
else return targets
}
/**
* 通过wdio的属性转换成元素坐标。返货盒子外圈坐标[x1,y1,x2,y2]或者中心点
* 示例 let box= await act.getElementBox(["id-text", "cn.some:name", "请填写姓名"], true)
* @param {string | string[]} selector
* @param {boolean | undefined} [outer]
*/
async getElementBox(selector, outer) {
let boxBounds
if (Array.isArray(selector) || typeof selector === 'string') {
let boxElement = await this.waitForElement(selector);
boxBounds = await boxElement.getAttribute('bounds');
}
else {
boxBounds = await selector.getAttribute('bounds');
}
let regex = /\[(\d+),(\d+)\]\[(\d+),(\d+)\]/;
let matches = boxBounds.match(regex);
if (matches) {
let box = [parseInt(matches[1]), parseInt(matches[2]), parseInt(matches[3]), parseInt(matches[4])];
if (outer) return box
else return [(box[0] + box[2]) / 2, (box[1] + box[3]) / 2]
} else {
return []
}
}
/**
* 滑动屏幕,可以分段执行。durations要么只传一个,要么和end坐标数量相同
* 示例 await act.screenScroll([startX,startY,[endX1,endY1],[endX2,endY2],[endX3,endY3]...] , [800,700,600])
* @param {string | any[]} axies
*/
async screenScroll(axies, durations = [700]) {
let action = [
{
"type": "pointer",
"id": "finger1",
"parameters": {
"pointerType": "touch"
},
"actions": [
{
"type": "pointerMove",
"duration": 0,
"x": axies[0],
"y": axies[1]
},
{
"type": "pointerDown",
"button": 0
},
]
}
]
for (let i = 2; i < axies.length; i++) {
action[0].actions.push({
"type": "pointerMove",
"duration": durations.length > 1 ? durations[i - 2] : durations[0],
"origin": "viewport",
"x": axies[i][0],
"y": axies[i][1],
})
}
action[0].actions.push({
"type": "pointerUp",
"button": 0
})
await this.client.performActions(action);
await this.client.releaseActions();
}
/**
* 滑动元素,从元素底部划到元素顶部,reverse为是否反向,offset正值为向内收缩范围
* 示例 await act.elementScroll(["id-text", "cn.some:name", "请填写姓名"], true, 1000, [20,20], 'horizontal')
* @param {string | string[]} selector
*/
async elementScroll(selector, reverse = true, durations = 700, offset = [10, 10], direction = 'vertical') {
let box = await this.getElementBox(selector, true);
if (direction === 'horizontal') {
if (reverse) {//右~左
await this.screenScroll([box[2] - offset[0], (box[3] + box[1]) / 2, [box[0] + offset[1], (box[3] + box[1]) / 2]], [durations]);
}
else {//左~右
await this.screenScroll([box[0] + offset[0], (box[3] + box[1]) / 2, [box[2] + offset[1], (box[3] + box[1]) / 2]], [durations]);
}
}
else {
if (reverse) {//下~上
await this.screenScroll([(box[2] + box[0]) / 2, box[3] - offset[0], [(box[2] + box[0]) / 2, box[1] + offset[1]]], [durations]);
}
else {//上~下
await this.screenScroll([(box[2] + box[0]) / 2, box[1] + offset[0], [(box[2] + box[0]) / 2, box[3] - offset[1]]], [durations]);
}
}
}
/**
* 获取某个元素的子元素,可选isPlural:第一个/获取所有, 可选超时, 可选useRule自定义规则
* 示例 await act.getParentChild(["id", 'id:app'], ["class", "android.view.View"], true, 20000, {func: findBtn, param: '123456'})
* @param {string | string[]} selector
* @param {string} child
*/
async getParentChild(selector, child, isPlural = false, time = 30000, useRule = null) {
selector = this.handleSelector(selector);
child = this.handleSelector(child);
let parent = await this.client.$(selector);
await parent.waitForExist({ timeout: time });
if (useRule) {
return useRule.func(parent, child, useRule.param);
}
else if (!isPlural) {
let childElement = await parent.$(child);
return childElement
}
else {
let childElements = await parent.$$(child);
return childElements
}
}
/**
* 获取某个元素的子元素,可选isPlural:第一个/获取所有, 可选超时, 可选useRule自定义规则
* 示例 await act.getBrother(['desc', '我的订单'], 'className("android.view.View").index(1)');
* @param {string | string[]} selector
* @param {any} target
*/
async getBrother(selector, target, time = 30000) {
let baseStr = this.handleSelector(selector);
let finalStr = `${baseStr}.fromParent(${target})`
let ele = await this.client.$(finalStr);
await ele.waitForExist({ timeout: time });
return ele
}
/**
* 通过属性查找元素。
* 示例 let element= await act.searchWithAttribute(['class', 'android.widget.EditText'], 'hint', '请输入姓名')
* @param {string | string[]} selector
* @param {any} attribute
* @param {any} value
*/
async searchWithAttribute(selector, attribute, value) {
selector = this.handleSelector(selector);
let result = ''
for (let i = 0; i < 3; i++) {
result = await this.client.$$(selector)
.filter(async (el) => {
let ee = await el.getAttribute(attribute);
return ee === value
});
if (result.length === 1) return result[0]
await sleep(0.5);
}
throw new Error(`Get ${value} error: ${result.length} `)
}
}
/**
* 延时等待函数,等待基础时间加上随机时间。
* @async
* @param {number} baseTimeSec - 基础等待时间(秒)。
* @param {number} [randomTimeSec=0] - 额外的随机等待时间(秒)。
* @returns {Promise<number>} - 返回实际等待的总时间(秒)。
* @description
* 延时等待,基础时间+随机时间 await sleep(1.2, 4); 1.2s~5.2s
*/
const sleep = async (baseTimeSec, randomTimeSec) => {
let time = 0
if (randomTimeSec) time = Math.floor(Math.random() * (randomTimeSec * 1000) + (baseTimeSec * 1000))
else time = baseTimeSec * 1000
await new Promise(resolve => setTimeout(resolve, time));
return time / 1000
}
/**
* 搜集简化的报错信息上传,主要用来输出选择器的报错。
* @param {string} tag - 标签,用于标识错误来源。
* @param {string} str - 原始错误信息。
* @returns {string} - 简化后的错误信息。
* @description
* 搜集简化的报错信息上传,主要用来输出选择器的报错
*/
const getError = (tag, str) => {
console.log(`${tag}报错==>>`, str);
if (typeof str !== 'string') return 'Error => message is null'
if (str.includes('still not existing')) {
let regex = /element\s(.+?)\sstill not existing after/;
let match = str.match(regex);
if (match && match.length > 1) {
let extractedText = match[1];
let errorStr = extractedText.replace('("android=new UiSelector().', '').replace('resourceId', '').replace('className', '').replace('text', '')
return `${tag} Error =>${errorStr}`
} else {
return `${tag} Error => unknown error`
}
}
else return `${tag} Error => unknown error`
}
/**
* 截取元素或整个屏幕的截图,上传成功会返回生成的文件名。
* @async
* @param {Object} act - 操作对象。
* @param {Object} [selector=null] - 要截图的元素选择器。
* @param {string} fileName - 文件名。
* @param {string} [suffix='_default'] - 文件名后缀。
* @returns {Promise<boolean|string>} - 返回上传结果,成功返回文件名,失败返回false。
* @description
* 截取元素或整个屏幕的截图,上传成功会返回生成的文件名
*/
const screenShot = async (act, selector, fileName, suffix = '_default') => {
try {
if (selector) {
var flag = false, times = 0, picID = ''
if (selector?.elementId) picID = selector.elementId
else {
while (!flag && times < 10) {
await sleep(1);
let element = await act.waitForElement(selector);
if (element?.elementId) {
picID = element.elementId
flag = true
}
else {
times++
}
}
if (!picID) return false
}
const elementScreenshot = await act.client.takeElementScreenshot(picID);
let res = await uploadPic(elementScreenshot, fileName + suffix);
console.log('图片上传完毕', res);
return res
}
else {
const elementScreenshot = await act.client.takeScreenshot();
let res = await uploadPic(elementScreenshot, fileName + suffix);
console.log('图片上传完毕', res);
return res
}
}
catch (err) {
console.log(err);
return false
}
}
/**
* 调用安卓返回键,直到某页。
* @async
* @param {Object} act - 操作对象。
* @param {Object} target - 目标元素选择器,用于判断是否到达目标页面。
* @param {number} [limit=10] - 最大返回次数。
* @returns {Promise<boolean>} - 是否成功到达目标页面。
* @description
* 调用安卓返回键,直到某页
*/
const getBackAndFind = async (act, target, limit = 10) => {
//通常按返回的间隔大于5秒可以确保不会退出应用
let over = false, times = 0;
while (!over && times < limit) {
times++;
try {
await act.waitForElement(target, 4000);
over = true;
}
catch {
await act.client.pressKeyCode(4);
}
}
return over;
}
/**
* 捕获轻提示。
* @async
* @param {Object} act - 操作对象。
* @param {string} [checkString=null] - 要检查的字符串。
* @returns {Promise<string>} - 捕获到的提示信息。
* @description
* 捕获轻提示
*/
const getToast = async (act, checkString) => {
let result = ''
try {
for (let j = 0; j < 5; j++) {
try {
let toast = await act.client.$('//hierarchy/android.widget.Toast');
await toast.waitForExist({ timeout: 800 });
let toastStr = await toast.getText();
log.debug(`获取消息: ${toastStr}`)
result = toastStr
break
}
catch {
// log.debug('尝试获取toast', j)
}
}
}
catch (err) {
console.log('toast error', err);
}
if (checkString) {
log.debug(`消息比对: ${checkString === result}`)
if (checkString === result) return result
else return ''
}
else return result
}
module.exports = {
log: log,
MyAction: MyAction,
sleep: sleep,
getToast: getToast,
getError: getError,
screenShot: screenShot,
getBackAndFind: getBackAndFind,
}