从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.mzph.cn/web/39966.shtml

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

相关文章

vue 中 使用腾讯地图 (动态引用腾讯地图及使用签名验证)

在设置定位的时候使用 腾讯地图 选择地址 在 mounted中引入腾讯地图&#xff1a; this.website.mapKey 为地图的 key // 异步加载腾讯地图APIconst script document.createElement(script);script.type text/javascript;script.src https://map.qq.com/api/js?v2.exp&…

SS8812T替代DRV8812的国产双通道H桥电机驱动芯片

由工采网代理的SS8812T是一款国产双通道H桥电机驱动芯片&#xff1b;该芯片为打印机和其它电机一体化应用提供一种双通道集成电机驱动方案&#xff1b;可Pin-to-Pin兼容替代DRV8812&#xff0c;可广泛应用于POS、打印机、安防相机、办公自动化设备、游戏机、机器人等。 产品描述…

Vue.js 案例——商品管理

一.需要做出的效果图&#xff1a; 二.实现的步骤 首先&#xff0c;先建一个项目&#xff0c;命名Table&#xff0c;在Table项目中的components里新建一个MyTable.vue文件。 第二步&#xff0c;在原有的 HelloWorld.vue中写入代码。 HelloWorld.vue代码如下&#xff1a; <…

KumiaoQQ机器人框架源码

源码介绍 酷喵机器人框架基于PC协议与MGCH的结合&#xff0c;MGCH即 MiraiGO-CQhttp&#xff08;代码类型&#xff1a;易语言&#xff09;基本的API功能已经实现&#xff0c;具体可自测&#xff08;教程/日志/说明文本已附带&#xff09;开放源码仅供参考学习交流&#xff0c;…

远超美国!中国AI专利数量全球第一!商汤推出面向C端用户大模型“Vimi”,可生成分钟级视频!|AI日报

文章推荐 苹果获得OpenAI董事会观察员职位&#xff01;Runway正筹集新一轮融资&#xff0c;估值40亿美元&#xff01;&#xff5c;AI日报 AI基准测评&#xff08;下&#xff09;&#xff1a;视频生成、代码能力、逻辑推理&#xff0c;AI是否已经超越人类&#xff1f; 联合国…

【linux高级IO(一)】理解五种IO模型

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux高级IO 1. 前言2. 重谈对…

kubernetes dashboard安装

1.查看符合自己版本的kubernetes Dashboard 比如我使用的是1.23.0版本 https://github.com/kubernetes/dashboard/releases?page5 对应版本 kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml修改对应的yaml,…

adb不插usb线通过wifi调试

说起做手机开发也有好多年了&#xff0c;说来惭愧&#xff0c;我最近才知道安卓手机是可以不插数据线进行开发调试的。起因是公司近期采购了一批安卓一卡通设备&#xff0c;需要对其进行定制开发APP,但是由于我插USB调试发现没有反应。通过询问厂家才知道可以通过WIFI进行调试。…

请注意,以下这几种操作都会导致流量卡被停用!

最近一段时间&#xff0c;小编经常收到一些反馈&#xff0c;明明是刚办理的手机号还没有用几天就被停用了&#xff0c;今天&#xff0c;这篇文章我们要了解就是手机号被停用的问题。 ​ 对于新办理的手机号会被停用这个问题&#xff0c;主要还是因为运营商为了防止电话诈骗&…

java数据结构集合复习之包装类和泛型

前言: 这是我最一年学习java的一部分的回顾总结 1.包装类 在Java中&#xff0c;由于基本类型不是继承自Object&#xff0c;为了在泛型代码中可以支持基本类型&#xff0c;Java给每个基本类型都对应了一个包装类型。 1.1基本数据类型和对应的包装类 ----—基本数据类型包装类…

ubuntu软件源的两种格式和环境变量

1. ubuntu的/etc是什么目录&#xff1f; 在Ubuntu操作系统中&#xff0c;/etc/是一个特殊的目录&#xff0c;它包含系统的配置文件。这些配置文件用于设置各种系统和应用程序的参数和选项。 一般来说&#xff0c;用户可以在这个目录下找到各种重要的配置文件&#xff0c;如网络…

Web3 ETF的主要功能

Web3 ETF的主要功能可以概括为以下几点&#xff0c;Web3 ETF仍是一项新兴投资产品&#xff0c;其长期表现仍存在不确定性。投资者在投资Web3 ETF之前应仔细研究相关风险&#xff0c;并做好充分的风险评估。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xf…

商务办公优选!AOC Q27E3S2商用显示器,打造卓越新体验!

摘要&#xff1a;助办公室一族纵横职场&#xff0c;实现高效舒适办公&#xff01; 在日常商务办公中&#xff0c;对于办公室一族来说总有太多“难难难难难点”&#xff1a;工作任务繁琐&#xff0c;熬夜加班心力交瘁、长时间伏案工作导致颈椎、眼睛等出现问题&#xff0c;职业…

BBA车主,千万别去试驾问界M9

文 | AUTO芯球 作者 | 雷慢&响铃 我劝你啊&#xff0c;千万别去试驾问界M9&#xff0c; 不然啊&#xff0c;可能1个小时50万就没了&#xff0c; 不信你看这个“大冤种”&#xff0c; 他曾经发誓打死不买电车&#xff0c; 考虑了三、四年换宝马X5&#xff0c; 结果谈完…

前端面试题5(前端常见的加密方式)

前端常见的加密方式 在前端进行数据加密主要是为了保护用户的隐私和提升数据传输的安全性。前端数据加密可以采用多种方法&#xff0c;以下是一些常见的加密技术和方法&#xff1a; 1. HTTPS 虽然不是直接的前端加密技术&#xff0c;但HTTPS是保障前端与后端数据传输安全的基…

关于MCU-Cortex M7的存储结构(flash与SRAM)

MCU并没有DDR&#xff0c;所以他把代码存储在flash上&#xff0c;临时变量和栈运行在SRAM上。之所以这么做是因为MCU的cpu频率很低&#xff0c;都是几十MHZ到一二百MHZ&#xff0c;flash的读取速度能够满足cpu 的取指需求&#xff0c;但flash 的写入速度很慢&#xff0c;所以引…

刚办理的手机号被停用,你可能遇到这些问题了!

很多朋友都会遇到手机号被停用的情况&#xff0c;那么你知道你的手机号为什么会被停用吗&#xff1f;接下来&#xff0c;关于手机号被停用的问题&#xff0c;跟着小编一块来了解一下吧。 ​停机的两种形态&#xff1a; 1、第一个是局方停机&#xff0c;即语音、短信和流量都不…

xmind2testcase工具将测试用例从Xmind转为CSV导入禅道

使用xmind编写测试用例&#xff0c;使用xmind2testcase工具将测试用例从Xmind转为CSV导入禅道&#xff0c;便于管理。 1.工具准备 第一步&#xff1a;安装python 第二步&#xff1a;安装xmind2testcase工具 运行-cmd-打开命令提示符弹窗&#xff0c;输入安装命令 安装命令&…

意图数据集HWU、Banking预处理

当谈到意图数据集时&#xff0c;HWU、Banking和Clinc是三个常见的数据集。以下是关于这三个数据集的介绍&#xff1a; 目录 一、数据集介绍 HWU数据集 Banking数据集 Clinc数据集 二、数据集预处理 数据处理 数据存储 数据类别分析 句子长度统计 一、数据集介绍 HW…

AI产品经理能力模型的重点素质:人文素养和灵魂境界

在AI产品经理的能力模型中&#xff0c;我最想提的差异化关键点&#xff0c;就是“人文素养和灵魂境界”。 1 为什么“人文素养和灵魂境界”非常重要&#xff1f; 一、“人文素养和灵魂境界”如何影响AI产品设计&#xff1f; 例1&#xff1a;面对一个具体的AI场景&#xff0…