從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.pswp.cn/web/39966.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/39966.shtml
英文地址,請注明出處:http://en.pswp.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,…

Linux Conda 入門案例教程

Conda 的基本概念 1.什么是 Conda&#xff1f;&#xff1a;Conda 是一個開源的包管理器和環境管理器&#xff0c;用于管理 Python 和其他語言的環境和依賴項。 2.Conda 的特點&#xff1a;Conda 的特點包括快速、可靠、靈活和跨平臺支持等。 安裝和配置 1.安裝 Conda&#x…

adb不插usb線通過wifi調試

說起做手機開發也有好多年了&#xff0c;說來慚愧&#xff0c;我最近才知道安卓手機是可以不插數據線進行開發調試的。起因是公司近期采購了一批安卓一卡通設備&#xff0c;需要對其進行定制開發APP,但是由于我插USB調試發現沒有反應。通過詢問廠家才知道可以通過WIFI進行調試。…

請注意,以下這幾種操作都會導致流量卡被停用!

最近一段時間&#xff0c;小編經常收到一些反饋&#xff0c;明明是剛辦理的手機號還沒有用幾天就被停用了&#xff0c;今天&#xff0c;這篇文章我們要了解就是手機號被停用的問題。 ? 對于新辦理的手機號會被停用這個問題&#xff0c;主要還是因為運營商為了防止電話詐騙&…

vue監聽數據時 newValue, oldValue操作處理

要只存入新更改的數據&#xff0c;可以在 watch 的回調函數中進行比較&#xff0c;篩選出有變化的屬性并將其存入新數組。以下是一個示例代碼&#xff0c;假設要監聽的對象為 obj&#xff1a; data() {return {differenceArray: [], obj: { /* 對象的初始屬性 */ }}; }, compu…

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; 結果談完…

GNU/Linux - 如何編譯kernel

使用 make 命令構建 Linux 內核涉及多個步驟。下面是整個過程的基本概述&#xff1a; 1. 獲取內核源代碼 * 從 kernel.org 或你的發行版軟件倉庫下載內核源代碼。 * 將源代碼解壓縮到一個目錄中。 2. 配置內核 * 切換到內核源代碼目錄。 * 可選擇清理源代碼樹&#xff1a;mak…

前端面試題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;即語音、短信和流量都不…