从0构建一款appium-inspector工具

  上一篇博客从源码层面解释了appium-inspector工具实现原理,这篇博客将介绍如何从0构建一款简单的类似appium-inspector的工具。如果要实现一款类似appium-inspector的demo工具,大致需要完成如下六个模块内容

  • 启动 Appium 服务器
  • 连接到移动设备或模拟器
  • 启动应用并获取页面源代码
  • 解析页面源代码
  • 展示 UI 元素
  • 生成 Locator

启动appium服务

  安装appium,因为要启动android的模拟器,后续需要连接到appium server上,所以这里还需要安装driver,这里需要安装uiautomater2的driver。

npm install -g appium
appium -v
appium//安装driver
appium driver install uiautomator2
appium driver list//启动appium服务
appium

   成功启动appium服务后,该服务默认监听在4723端口上,启动结果如下图所示

连接到移动设备或模拟器

  在编写代码连接到移动设备前,需要安装android以及一些SDK,然后通过Android studio启动一个android的手机模拟器,这部分内容这里不再详细展开,启动模拟器后,再编写代码让client端连接下appium服务端。

   下面代码通过调用webdriverio这个lib中提供remote对象来连接到appium服务器上。另外,下面的代码中还封装了ensureClient()方法,连接appium服务后,会有一个session,这个sessionId超时后会过期,所以,这里增加ensureClient()方法来判断是否需要client端重新连接appium,获取新的sessionId信息。

import { remote } from 'webdriverio';
import fs from 'fs';
import xml2js from 'xml2js';
import express from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';// 获取当前文件的目录名
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 加载配置文件
const config = JSON.parse(fs.readFileSync('./src/config.json', 'utf-8'));
// 配置连接参数
const opts = {path: '/',port: 4723,capabilities: {'appium:platformName': config.platformName,'appium:platformVersion': config.platformVersion,'appium:deviceName': config.deviceName,'appium:app': config.app,'appium:automationName': config.automationName,'appium:appWaitActivity':config.appActivity},
};const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
let client;const initializeAppiumClient = async () => {try {client = await remote(opts);console.log('Connected to Appium server');} catch (err) {console.error('Failed to connect to Appium server:', err);}
};
//解决session过期的问题
const ensureClient = async () => {if (!client) {await initializeAppiumClient();} else {try {await client.status();} catch (err) {if (err.message.includes('invalid session id')) {console.log('Session expired, reinitializing Appium client');await initializeAppiumClient();} else {throw err;}}}
};

启动应用并获取页面信息

  当client端连接到appium server后,获取当前模拟器上应用页面信息是非常简单的,这里需要提前在模拟器上安装一个app,并开启app。代码的代码中将获取page source信息,获取screenshot信息,点击tap信息都封装成了api接口,并通过express,在9096端口上启动了一个后端服务。

app.get('/page-source', async (req, res) => {try {await ensureClient();// 获取页面源代码const pageSource = await client.getPageSource();const parser = new xml2js.Parser();const result = await parser.parseStringPromise(pageSource);res.json(result);} catch (err) {console.error('Error occurred:', err);res.status(500).send('Error occurred');}
});app.get('/screenshot', async (req, res) => {try {await ensureClient();// 获取截图const screenshot = await client.takeScreenshot();res.send(screenshot);} catch (err) {console.error('Error occurred:', err);res.status(500).send('Error occurred');}
});app.post('/tap', async (req, res) => {try {await ensureClient();const { x, y } = req.body;await client.touchAction({action: 'tap',x,y});res.send({ status: 'success', x, y });} catch (err) {console.error('Error occurred while tapping element:', err);res.status(500).send('Error occurred');}
});app.listen(9096, async() => {await initializeAppiumClient();console.log('Appium Inspector server running at http://localhost:9096');
});process.on('exit', async () => {if (client) {await client.deleteSession();console.log('Appium client session closed');}
});

  下图就是上述服务启动后,调用接口,获取到的页面page source信息,这里把xml格式的page source转换成了json格式存储。结果如下图所示:

显示appUI以及解析获取element信息

  下面的代码是使用react编写,所以,可以通过react提供的命令,先初始化一个react项目,再编写下面的代码。对于在react编写的应用上显示mobile app的ui非常简单,调用上面后端服务封装的api获取page source,使用<imag src=screenshot>就可以在web UI上显示mobile app的UI。

  另外,除了显示UI外,当点击某个页面元素时,期望能获取到该元素的相关信息,这样才能结合元素信息生成locator,这里封装了findElementAtCoordinates方法来从pageSource中查找match的元素,查找的逻辑是根据坐标信息,也就是pagesource中bounds字段信息进行匹配match的。

import React, {useState, useEffect, useRef} from 'react';
import axios from 'axios';const App = () => {const [pageSource, setPageSource] = useState('');const [screenshot, setScreenshot] = useState('');const [elementInfo, setElementInfo] = useState(null);const [highlightBounds, setHighlightBounds] = useState(null);const imageRef = useRef(null);const ERROR_MARGIN = 5; // 可以调整误差范围const getPageSource = async () => {try {const response = await axios.get('http://localhost:9096/page-source');setPageSource(response.data);} catch (err) {console.error('Error fetching page source:', err);}};const getScreenshot = async () => {try {const response = await axios.get('http://localhost:9096/screenshot');setScreenshot(`data:image/png;base64,${response.data}`);} catch (err) {console.error('Error fetching screenshot:', err);}};useEffect( () => {getPageSource();getScreenshot()}, []);const handleImageClick = (event) => {if (imageRef.current && pageSource) {const rect = imageRef.current.getBoundingClientRect();const x = event.clientX - rect.left;const y = event.clientY - rect.top;// 检索页面源数据中的元素pageSource.hierarchy.$.bounds="[0,0][1080,2208]";const element = findElementAtCoordinates(pageSource.hierarchy, x, y);if (element) {setElementInfo(element.$);const bounds = parseBounds(element.$.bounds);setHighlightBounds(bounds);} else {setElementInfo(null);setHighlightBounds(null);}}};const parseBounds = (boundsStr) => {const bounds = boundsStr.match(/\d+/g).map(Number);return {left: bounds[0],top: bounds[1],right: bounds[2],bottom: bounds[3],centerX: (bounds[0] + bounds[2]) / 2,centerY: (bounds[1] + bounds[3]) / 2,};};const findElementAtCoordinates = (node, x, y) => {if (!node || !node.$ || !node.$.bounds) {return null;}const bounds = parseBounds(node.$.bounds);const withinBounds = (x, y, bounds) => {return (x >= bounds.left &&x <= bounds.right &&y >= bounds.top &&y <= bounds.bottom);};if (withinBounds(x, y, bounds)) {for (const child of Object.values(node)) {if (Array.isArray(child)) {for (const grandChild of child) {const foundElement = findElementAtCoordinates(grandChild, x, y);if (foundElement) {return foundElement;}}}}return node;}return null;};return (<div>{screenshot && (<div style={{ position: 'relative' }}><imgref={imageRef}src={screenshot}alt="Mobile App Screenshot"onClick={handleImageClick}style={{ cursor: 'pointer', width: '1080px', height: '2208px' }} // 根据 page source 调整大小/>{highlightBounds && (<divstyle={{position: 'absolute',left: highlightBounds.left,top: highlightBounds.top,width: highlightBounds.right - highlightBounds.left,height: highlightBounds.bottom - highlightBounds.top,border: '2px solid red',pointerEvents: 'none',}}/>)}</div>)}{elementInfo && (<div><h3>Element Info</h3><pre>{JSON.stringify(elementInfo, null, 2)}</pre></div>)}</div>);
};export default App;

  下图图一是android模拟器上启动了一个mobile app页面。

   下图是启动react编写的前端应用,可以看到,在该应用上显示了模拟器上的mobile app ui,当点击某个元素时,会显示被点击元素的相关信息,说明整个逻辑已经打通。当点击password这个输入框元素时,下面显示了element info,可以看到成功查找到了对应的element。当然,这个工具只是一个显示核心过程的demo code。例如higlight的红框,不是以目标元素为中心画的。

   关于生成locator部分,这里并没有提供code,当获取到element信息后,还需要获取该element的parent element,根据locator的一些规则,编写方法实现,更多的细节可以参考appium-server 源代码。

    整个工具的demo code 详见这里,关于如果启动应用部分,可以看readme信息。   

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/371034.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode每日一题-3101 交替子数组计数

暴力遍历&#xff1a;看起来像是回溯,实际上就是递归 class Solution { private:long long _res 0; public:long long countAlternatingSubarrays(vector<int>& nums) {backtrack(nums, 0);return _res;}void backtrack(vector<int>& nums, long long st…

零基础STM32单片机编程入门(八)定时器PWM输入实战含源码视频

文章目录 一.概要二.PWM输入框架图三.CubeMX配置一个PWM输入例程1.硬件准备2.创建工程3.调试 四.CubeMX工程源代码下载五.讲解视频链接地址六.小结 一.概要 脉冲宽度调制(PWM)&#xff0c;是英文“Pulse Width Modulation”的缩写&#xff0c;简称脉宽调制&#xff0c;是利用单…

最短路算法——差分约束

差分约束 (1) 求不等式组的可行解 源点&#xff1a;从源点出发&#xff0c;一定可以走到所有的边求可行解步骤&#xff1a; 先将每个不等式 x i ≤ x j c x_i \le x_j c xi​≤xj​c,转化成一条从 s j s_j sj​走到 s i s_i si​&#xff0c;长度为 c k c_k ck​ 的一条边找…

【面试八股文】java基础知识

引言 本文是java面试时的一些常见知识点总结归纳和一些拓展&#xff0c;笔者在学习这些内容时&#xff0c;特地整理记录下来&#xff0c;以供大家学习共勉。 一、数据类型 1.1 为什么要设计封装类&#xff0c;Integer和int区别是什么&#xff1f; 使用封装类的目的 对象化:…

C++ 引用——常量引用

作用&#xff1a;常量引用主要用来修饰形参&#xff0c;防止误操作 在函数形参列表中&#xff0c;可以加const修饰形参&#xff0c;防止形参改变实参 示例&#xff1a; 运行结果&#xff1a;

微信小程序消息通知(一次订阅)

在微信公众平台配置通知模版 通过wx.login获取code发送给后端 let that this // 登陆codewx.login({success: function (res) {if (res.code) {// 发送code到后端换取openid和session_keythat.setData({openCode: res.code})console.log(that.data.openCode, openCode);// 调…

ARMv8寄存器详解

文章目录 一、ARMv8寄存器介绍二、通用寄存器三、 PSTAE寄存器四、特殊寄存器五、系统寄存器 一、ARMv8寄存器介绍 本文我来给大家介绍一下ARMv8的寄存器部分&#xff0c;ARMv8中有34个寄存器&#xff0c;包括31个通用寄存器、一个栈指针寄存器SP(X31),一个程序计数器寄存器PC…

Git中两个开发分支merge的原理

一 分支合并 1.1 原理 分支合并&#xff1a;就是将A分支修改后且commit的内容&#xff0c;合并到B分支&#xff0c;这些修改且提交的内容和B分支对应的内容和位置进行比较&#xff1a; 1.不一样的话&#xff0c;提示冲突&#xff0c;需要人工干预。 2.一样的话&#xff0c;…

LLM - 卷积神经网络(CNN)

1. 卷积神经网络结构&#xff1a;分为输入层&#xff0c;卷积层&#xff0c;池化层&#xff0c;全连接层&#xff1b; &#xff08;1&#xff09;首先进入输入层&#xff0c;对数据数据进行处理&#xff0c;将输入数据向量化处理&#xff0c;最终形成输入矩阵。 &#xff08;…

vue3使用方式汇总

1、引入iconfont阿里图库图标&#xff1a; 1.1 进入阿里图标网站&#xff1a; iconfont阿里&#xff1a;https://www.iconfont.cn/ 1.2 添加图标&#xff1a; 1.3 下载代码&#xff1a; 1.4 在vue3中配置代码&#xff1a; 将其代码复制到src/assets/fonts/目录下&#xff1…

Overleaf :LaTeX协作神器!【送源码】

Overleaf 是一个广受欢迎的在线 LaTeX 编辑器&#xff0c;专为学术写作和文档排版设计。它以其协作功能和用户友好的界面而闻名&#xff0c;使得 LaTeX 编辑变得更加容易和直观。 软件介绍 Overleaf 提供了一个基于云的 LaTeX 编辑环境&#xff0c;支持实时协作&#xff0c;使得…

Nordic 52832作为HID 键盘连接配对电视/投影后控制没反应问题的分析和解决

问题现象&#xff1a;我们的一款HID键盘硬件一直都工作的很好&#xff0c;连接配对后使用起来和原装键盘效果差不多&#xff0c;但是后面陆续有用户反馈家里的电视等蓝牙设备配对连接我们的键盘后&#xff0c;虽然显示已连接&#xff0c;但实际上控制不了。设备涉及到了好些品牌…

Blazor SPA 的本质是什么以及服务器端渲染如何与 Blazor 的新 Web 应用程序配合使用

Blazor 通常被称为单页应用程序 (SPA) 框架。当我第一次开始使用 Blazor 时&#xff0c;我对 SPA 的含义、组件如何为 SPA 架构做出贡献以及所有这些如何与交互性联系在一起感到困惑。 今天&#xff0c;我将解答大家可能关心的三个问题&#xff1a; 什么是 SPA&#xff1f;了…

STM32 Cannot access memory

问题描述 最近自己做了一块STM32F103ZET6的板子&#xff0c;在焊接完成后可以在下载器界面看到idcode&#xff0c;但烧录时报错 Cannot access memory 。 解决办法 测量STM32各个供电项&#xff0c;发现时33脚处VDDA电压只有1.8V&#xff0c;是因为R3电阻过大&#xff0c;…

基于YOLOv9的脑肿瘤区域检测

数据集 脑肿瘤区域检测&#xff0c;我们直接采用kaggle公开数据集&#xff0c;Br35H 数据中已对医学图像中脑肿瘤位置进行标注 数据集我已经按照YOLO格式配置好&#xff0c;数据内容如下 数据集中共包含700张图像&#xff0c;其中训练集500张&#xff0c;验证集200张 模型训…

Xilinx FPGA:vivado关于真双端口的串口传输数据的实验

一、实验内容 用一个真双端RAM&#xff0c;端口A和端口B同时向RAM里写入数据0-99&#xff0c;A端口读出单数并存入单端口RAM1中&#xff0c;B端口读出双数并存入但端口RAM2中&#xff0c;当检测到按键1到来时将RAM1中的单数读出显示到PC端&#xff0c;当检测到按键2到来时&…

YOLO V7网络实现细节(2)—网络整体架构总结

YOLO V7网络整体架构总结 YOLO v7网络架构的整体介绍 不同GPU和对应模型&#xff1a; ​​​​​​​边缘GPU&#xff1a;YOLOv7-tiny普通GPU&#xff1a;YOLOv7​​​​​​​云GPU的基本模型&#xff1a; YOLOv7-W6 激活函数&#xff1a; YOLOv7 tiny&#xff1a; leaky R…

openmetadata1.3.1 自定义连接器 开发教程

openmetadata自定义连接器开发教程 一、开发通用自定义连接器教程 官网教程链接&#xff1a; 1.https://docs.open-metadata.org/v1.3.x/connectors/custom-connectors 2.https://github.com/open-metadata/openmetadata-demo/tree/main/custom-connector &#xff08;一&…

24西安电子科技大学经济与管理学院—考研录取情况

24西安电子科技大学—经理与管理学院—考研录取统计 01、经理与管理学院各个方向 02、24经济与管理近三年复试分数线对比 1、经管院24年院线相对于23年院线普遍下降2-15分&#xff0c;个别专业上涨4-10分。 2、经管院应用经济学2024年院线350分&#xff1b;管理科学与工程院线…

Apache Seata tcc 模块源码分析

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 一 .导读 spring 模块分析中讲到&#xff0c;Seata 的 spring 模块会对涉及到分布式业务的 b…