从0构建一款appium-inspector工具

news2024/11/25 2:43:53

  上一篇博客从源码层面解释了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' }}>
                    <img
                        ref={imageRef}
                        src={screenshot}
                        alt="Mobile App Screenshot"
                        onClick={handleImageClick}
                        style={{ cursor: 'pointer', width: '1080px', height: '2208px' }} // 根据 page source 调整大小
                    />
                    {highlightBounds && (
                        <div
                            style={{
                                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.coloradmin.cn/o/1893159.html

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

相关文章

构建机部署之Azure DevOps添加代理机(Linux)

目录 一、权限检查二、添加代理机三、更换代理四、删除并重新配置代理 一、权限检查 确认用户具有权限 默认代理池的所有者有添加代理的权限 1&#xff09;代理池所有者可以生成一个PAT&#xff0c;共享使用。代理不会在日常操作中使用此人凭据&#xff0c;但需要使用有权限的…

【机器学习】机器学习与图像识别的融合应用与性能优化新探索

文章目录 引言第一章&#xff1a;机器学习在图像识别中的应用1.1 数据预处理1.1.1 数据清洗1.1.2 数据归一化1.1.3 数据增强 1.2 模型选择1.2.1 卷积神经网络1.2.2 迁移学习1.2.3 混合模型 1.3 模型训练1.3.1 梯度下降1.3.2 随机梯度下降1.3.3 Adam优化器 1.4 模型评估与性能优…

小学vr虚拟课堂教学课件开发打造信息化教学典范

在信息技术的浪潮中&#xff0c;VR技术正以其独特的魅力与课堂教学深度融合&#xff0c;引领着教育方式的创新与教学方法的变革。这一变革不仅推动了“以教促学”的传统模式向“自主探索”的新型学习方式转变&#xff0c;更为学生带来了全新的学习体验。 运用信息技术融合VR教学…

前端学习(五)CSS浮动与补白

目录&#xff1a; 内容&#xff1a; //设置左右浮动 .left{float:left; } .right{float:right; } /*必须设置不同浮动*/ //创建div <div> <dic class"left">左边</div> <div class"right">右边</div> </div> //设置浮…

[C/C++] -- gdb调试与coredump

1.gdb调试 GDB&#xff08;GNU 调试器&#xff09;是一个强大的工具&#xff0c;用于调试程序。 安装 1. wget http://ftp.gnu.org/gnu/gdb/gdb-8.1.tar.gz 2. tar -zxvf gdb-8.1.1.tar.gz 3. cd gdb-8.1.1 4. ./configure 5. make 6. make install 基础用法 …

springboot的非物质文化遗产管理系统-计算机毕业设计源码16087

目录 摘要 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1系统开发流程 2.2.2 用户登录流程 2.2.3 系统操作流程 2.2.4 添加信息流程 2.2.5 修改信息流程 2.2.6 删除信息流程 2.3 系统功能…

图书管理系统(持久化存储数据以及增添新功能)

目录 一、数据库表设计 二、引入MyBatis 和MySQL 驱动依赖 三、配置数据库 & 日志 四、Model创建 五、枚举类 常量类用户登录 六、用户登录 七、添加图书 八、图书列表 九、修改图书 十、删除图书 十一、批量删除 十二、强制登录 十三、前端代码 &#xff0…

【C语言】bool 关键字

在C语言中&#xff0c;bool类型用于表示布尔值&#xff0c;即真或假。C语言本身在标准库中并未提供布尔类型&#xff0c;直到C99标准引入了stdbool.h头文件。该头文件定义了bool类型&#xff0c;以及两个常量&#xff1a;true和false。在此之前&#xff0c;通常使用整数来表示布…

6.8应用进程跨网络通信

《计算机网络》第7版&#xff0c;谢希仁 理解socket通信

初入Node.js必备知识

Node.js因什么而生&#xff0c;作用是干什么&#xff1f; Node.js是一个用c和c打造的一个引擎&#xff0c;他能够读懂JavaScript&#xff0c;并且让JavaScript能够和操作系统打交道的能力 JavaScript 原本只能在浏览器中运行,但随着Web应用程序越来越复杂,仅靠客户端JavaScri…

35 智能指针

目录 为什么需要智能指针&#xff1f;内存泄露智能指针的使用及原理c11和boost中智能指针的关系RAII扩展学习 1. 为什么需要智能指针&#xff1f; 下面我们先分析一下下面这段程序有没有什么内存方面的问题&#xff1f; int div() {int a, b;cin >> a >> b;if (…

AutoPSA的应力加强系数

GD2000里的直连三通的应力加强系数是错误的&#xff0c;建议用户删除再使用。 当应力加强系数为空的时候&#xff0c;psa是会自已计算应力加强系数&#xff1b;当用户填了加强系数&#xff0c;软件就优先用填了的加强系数&#xff1b; 直连三通和假三通的作用一样&#xff0c…

JAVA医院绩效考核系统源码:绩效考核的重要性、绩效管理分配实践具体实操,基于B/S架构开发的一套(公立医院绩效考核系统源码)

JAVA医院绩效考核系统源码&#xff1a;绩效考核的重要性、绩效管理分配实践具体实操&#xff0c;基于B/S架构开发的一套&#xff08;公立医院绩效考核系统源码&#xff09; 系统开发环境 开发语言&#xff1a;java 技术架构&#xff1a;B/S架构 开发工具&#xff1a;maven、…

C++基础(五):类和对象(上)

从今天开始&#xff0c;我们正式进入面向对象编程&#xff0c;这是C与C语言的重要区别&#xff0c;编程思想发生变化&#xff0c;那到底什么是面向对象编程呢&#xff1f;接下来&#xff0c;我们慢慢的深入学习。 目录 一、面向过程和面向对象初步认识 1.1 面向过程 1.2 面…

[激光原理与应用-97]:激光焊接焊中检测系统系列介绍 - 1 - 什么是焊接以及传统的焊接方法

目录 一、什么是焊接 1.1 概述 1.2 基本原理 二、传统的焊接技术与方法 2.1 手工电弧焊&#xff1a; 1、定义与原理 2、特点 3、焊条类型 4、应用领域 5、安全注意事项 2.2 气体保护焊&#xff1a; 1、原理与特点 2、应用领域 3、气体选择 4、注意事项 2.3 电阻…

Zabbix 配置PING监控

Zabbix PING监控介绍 如果需要判断机房的网络或者主机是否正常&#xff0c;这就需要使用zabbix ping&#xff0c;Zabbix使用外部命令fping处理ICMP ping的请求&#xff0c;在基于ubuntu APT方式安装zabbix后默认已存在fping程序。另外zabinx_server配置文件参数FpingLocation默…

layui中添加上下文提示弹窗

<p context-tip"自定义上下文提示信息">段落内容...</p> <div context-tip"自定义上下文提示信息">div内容...</div>// 悬浮提示 $("body").on("mouseenter", "*[context-tip]", function () {v…

清华 PowerPaint:多功能局部重绘模型

PowerPaint 是清华和上海人工智能实验室推出的一个开源高质量多功能的图像修补模型&#xff0c;同时支持插入物体、移除物体、图像扩展、形状可控的物体生成功能。 可以在 清华 PowerPaint&#xff1a;多功能局部重绘模型

【大语言模型系列之Transformer】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

关键词搜索商品API的工作原理是什么?

关键词搜索商品API的工作原理基于复杂的数据处理和检索机制&#xff0c;通过爬虫抓取、数据预处理、数据索引等流程。 在网上购物成为日常生活的一部分&#xff0c;关键词搜索商品API成为了电子商务平台不可或缺的功能。通过这种API&#xff0c;消费者可以轻松地通过输入关键字…