从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' }}>
                    <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.mfbz.cn/a/773779.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML+CSS笔记

标签 HTML标签 网页的大包围 整体网页内容的外衣 所有的网页文档内容都要写在 html标签内 lang属性&#xff0c;是指内容语言的&#xff0c;目的是让浏览器知晓这个页面的主要展示语言 是什么 只跟浏览器的翻译有关 主要展示的语言如果是英语 en&#xff0c;主要展示的语言如果…

移动硬盘传输中断后无法识别:深度解析与数据救援指南

在日常的数据存储与传输过程中&#xff0c;移动硬盘凭借其大容量、便携性成为众多用户的首选。然而&#xff0c;当我们在复制或移动大量数据时遭遇传输中断&#xff0c;随后发现移动硬盘无法被电脑识别&#xff0c;这无疑是一场数据安全的紧急警报。此情此景&#xff0c;不仅影…

Docker学习笔记(三)Dockerfile

一、什么是Dockerfile Dockerfile 是一个用于自动化构建 Docker 镜像的文本文件&#xff0c;其中包含了从一个基础镜像开始&#xff0c;到最终形成所需定制镜像的所有指令集。这个文件中的每一条指令都对应着构建镜像过程中的一个步骤或一层&#xff0c;指导 Docker 如何安装软…

红蓝对抗下的内网横向移动渗透技术详解

一、利用Windows计划任务横向移动 Windows计划任务是一个非常实用的功能&#xff0c;可以帮助我们自动完成一些重复性的任务。比如&#xff0c;我们可以设定一个计划任务来自动备份文件、更新软件、执行脚本等,本文主要介绍了如何利用Windows计划任务进行横向渗透。 &#xf…

线程池实践篇

文章目录 配置线程池参数定义参数实体bean配置线程池使用 配置线程池参数 定时任务线程池基础参数 # 定时任务线程池基础参数 task:pool:corePoolSize: 5 # 核心线程数maxPoolSize: 20 # 设置最大线程数keepAliveSeconds: 300 # 设置线程活跃时间&#xff0c;单位秒queueCapa…

[C++初阶]vector的初步理解

一、标准库中的vector类 1.vector的介绍 1. vector是表示可变大小数组的序列容器 &#xff0c; 和数组一样&#xff0c;vector可采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大…

海思SD3403/SS928V100开发(14)WIFI模块RTL8821驱动调试

1.前言 芯片平台: 海思SD3403/SS928V100 操作系统平台: Ubuntu20.04.05【自己移植】 WIFI模块: LB-LINK的RTL8821 2. 调试记录 参考供应商提供的操作手册 2.1 lsusb查看设备 2.2 编译供应商提供的驱动 2.2.1 修改Makefile 2.2.2 编译报错 解决办法: 将Makefile中arm…

FPGA基本资源介绍

文章目录 FPGA资源介绍1.可编程输入输出单元(IOB)2.可配置逻辑块(CLB)3.数字时钟管理模块(DCM)4.嵌入式块RAM(BLOCK RAM / BRAM)4.1其他ram 5.丰富的布线资源6.底层内嵌功能单元7.内嵌专用硬核软核、硬核、以及固核的概念 FPGA资源介绍 1.可编程输入输出单元(IOB) 可编程输入…

大语言模型融合知识图谱的问答系统研究

文章目录 题目摘要方法实验消融实验 题目 大语言模型融合知识图谱的问答系统研究 论文地址&#xff1a;http://fcst.ceaj.org/CN/10.3778/j.issn.1673-9418.2308070 项目地址&#xff1a;https://github.com/zhangheyi-1/llmkgqas-tcm/ 摘要 问答系统&#xff08;Question Ans…

数据库管理-第217期 Oracle的高可用-02(20240704)

数据库管理217期 2024-07-04 数据库管理-第217期 Oracle的高可用-02&#xff08;20240704&#xff09;1 GDS简介2 GDS架构2.1 全局数据服务池2.2 全局数据服务域2.3 全局服务管理2.4 全局数据服务目录2.5 Oracle通知服务 3 GDS简图3.1 负载均衡3.2 只读服务失败转移3.3 多主复制…

1014-33SF 同轴连接器

型号简介 1014-33SF是Southwest Microwave的2.92 mm 同轴连接器。这款连接器采用钢制外壳&#xff0c;铍铜触点&#xff0c;并经过金镀处理&#xff0c;以确保良好的导电性和耐腐蚀性。适用于高频微波应用&#xff0c;例如测试设备、通信系统等。 型号特点 频率范围&#xff1…

openEuler 社区 2024 年 5 月运作报告

概述 2024年5月&#xff0c;在 OpenAtom openEuler(简称&#xff1a;“openEuler”&#xff09;技术委员会例会上&#xff0c;经技术委员会委员审定&#xff0c;同意开发者在社区成立 SBOM SIG、Intelligence SIG。SBOM SIG 主要围绕 SBOM 构建openEuler社区软件供应链安全&…

【React】Ant Design -- Table分页功能实现

实现步骤 为Table组件指定pagination属性来展示分页效果在分页切换事件中获取到筛选表单中选中的数据使用当前页数据修改params参数依赖引起接口重新调用获取最新数据 const pageChange (page) > {// 拿到当前页参数 修改params 引起接口更新setParams({...params,page})…

Finding and exploting an unused API endpoint

Using 0$ account buy a piece of lether priced at $133 1、尝试访问api接口 大概率可能访问不到,但是可以尝试访问下 /api/swagger/v1 /openapi.json 2、页面功能点寻找 api send to Repeter 3、Find Supported HTTP请求 POST方法测试 通过测试得知支持GET方法和PATC…

[产品]理解产品

课程安排 认识互联网行业 1.行业对比 2.互联网公司 广义理解: 互联网行业的公司大都以计算机网络技术为基础, 利用网络平台帮助企业提供服务, 并以此获取收入 3.行业细分 典型产品 认识产品经理 1.职责差异 不同类型的公司, 产品经理岗位所负责的工作都是略有差异的 1,外包…

Docker Desktop 简易操作指南 (Windows, macOS, Linux)

1. 下载最新版本 Docker Desktop https://www.docker.com/products/docker-desktop/ 2.启动 Docker Desktop 3.常用命令&#xff08;在 cmd 或 Terminal 中执行&#xff09; #列出所有镜像&#xff08;Images&#xff09; docker images #列出所有容器&#xff08;Containers&…

后端之路——阿里云OSS云存储

一、何为阿里云OSS 全名叫“阿里云对象存储OSS”&#xff0c;就是云存储&#xff0c;前端发文件到服务器&#xff0c;服务器不用再存到本地磁盘&#xff0c;可以直接传给“阿里云OSS”&#xff0c;存在网上。 二、怎么用 大体逻辑&#xff1a; 细分的话就是&#xff1a; 1、准…

visual studio 2022配置和使用protobuf

上图证明&#xff0c;我真的测了好多遍&#xff0c;测了好多版本的protobuf&#xff0c;花了很多时间。不过好在最后在vs2022上测通了。 下载protobuf 这里是protobuf下载的地址。 Releases protocolbuffers/protobuf GitHub 个人使用的3.21.9这个版本才跑通的。 1、首先…

Lesson 48 Do you like ... ? Do you want ... ?

Lesson 48 Do you like … ? Do you want … ? 词汇 fresh a. 新鲜的【食物】 搭配&#xff1a;fresh water 淡水    fresh man 新生    fresh air 新鲜空气    fresh egg 新鲜鸡蛋 例句&#xff1a;我们喜欢新鲜的空气。    We like fresh egg. egg n. 蛋【通…

unity知识点 专项四 一文彻底说清楚(锚点(anchor)、中心点(pivot)、位置(position)之间的关系)

一 概述 想要使UI控件在屏幕中达到正确的显示效果&#xff0c;比如自适应屏幕尺寸、固定边距等等&#xff0c;首先要理清楚几个基本概念和设置&#xff1a;锚点(anchor)、中心点(pivot)、位置(position)、UI缩放模式、父物件的transform设置 二 Anchor、Pivot与Position 2…