news 2026/9/13 3:12:41

Appium universal-xml-plugin:把 iOS 与 Android 页面源码统一为一套通用 XML 语法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Appium universal-xml-plugin:把 iOS 与 Android 页面源码统一为一套通用 XML 语法

Appium universal-xml-plugin:把 iOS 与 Android 页面源码统一为一套通用 XML 语法

【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium

本文以 Appium 仓库中的@appium/universal-xml-plugin插件文档(README)为主体,结合 lib/plugin.ts、lib/source.ts、lib/xpath.ts 等源码实现,完整讲清这个插件的工作机制:如何安装启用、如何把两个平台的 Page Source 统一为通用节点/属性命名、XPath 查询如何被自动翻译回平台原生 XML,以及映射表、过滤属性的完整清单。读完你将掌握跨平台 UI 测试中“一份源码、一套 XPath”的完整用法与实现原理。

1. 解决什么问题

Appium 中 iOS 与 Android 驱动返回的页面源码(Page Source)命名体系完全不同:iOS 使用XCUIElementTypeButton这类 XCUITest 元素类型和name/label/visible属性,Android 使用android.widget.Button这类控件全名和content-desc/text/displayed属性。这意味着同一个“登录按钮”在两个平台需要写两套 XPath,跨平台测试无法复用。

@appium/universal-xml-plugin的目标(README 中的 Motivation)正是:让 iOS 与 Android 的 XML 源码具备互操作性,从而简化跨平台测试的编写。插件把 Get Page Source 返回的 XML 中的节点名、属性名统一改写为一套对两个平台都适用的通用术语(如Buttontextvisible),未收录在映射表中的名称原样保留。

从 package.json 可以看到该插件的元信息:npm 包名@appium/universal-xml-pluginappium.pluginNameuniversal-xml,主类为UniversalXMLPluginpeerDependencies要求appium ^3.0.0-beta.0engines要求 Node^20.19.0 || ^22.12.0 || >=24.0.0

2. 安装与启用

README 给出的两步操作即为完整用法:

# 安装插件 appium plugin install universal-xml
# 启动 Appium 服务器时显式激活插件 appium --use-plugins=universal-xml

与所有 Appium 插件一样,插件不会自动生效,必须在启动服务器时通过--use-plugins显式激活。插件激活后,会拦截两类命令并做转换:

  1. Get Page Source命令返回的应用源码;
  2. Find Element / Find Elements系列命令中携带的节点名与属性名(即 XPath 选择器)。

3. 拦截机制:插件到底 hook 了哪些命令

UniversalXMLPlugin 继承自 Appium 的BasePlugin,只实现了三个方法:getPageSourcefindElementfindElements

3.1 拦截 Get Page Source

getPageSource(plugin.ts)的流程是:

  1. 先调用next()拿到驱动返回的原始 XML(或兜底直接调driver.getPageSource());
  2. 从会话能力caps.platformName中取出平台名(getPlatformName);
  3. 若平台为 Android,额外从驱动选项driver.opts.appPackage取包名放入转换元数据——这个包名稍后会用于剥离resource-id的前缀;
  4. 调用 transformSourceXml 完成真正的 XML 转换,返回统一命名后的字符串。

此外还有一个值得注意的“质量反馈”机制:转换过程中所有未被映射表收录的节点名与属性名会被收集起来,插件会在日志中打 warn,提示“这些未知名称应该被报告,以改进插件质量”(plugin.ts)。也就是说源码里保留了完整的 unknown 统计通道,只是默认行为是“原样保留 + 告警”。

3.2 拦截 Find Element 系列命令

findElement/findElements共用内部方法_find(plugin.ts),其前置条件非常明确,源码中只有同时满足以下三点才会走翻译逻辑,否则直接放行给驱动(next()):

  • 定位策略strategy必须是xpath(不区分大小写);
  • 驱动实现了getCurrentContext
  • 当前上下文必须是NATIVE_APP

满足条件后的执行链是:

  1. 先以addIndexPath: true模式重新生成一份带索引路径的转换后 XML(见第 5 节);
  2. 用 transformQuery 把用户写的“通用 XPath”翻译成针对原始平台 XML的等价路径表达式;
  3. 若翻译结果为null(意味着没有匹配节点或翻译失败),插件记录 warn,然后findElementNoSuchElementErrorfindElements返回空数组[]
  4. 翻译成功则记录Selector was translated to: ...的 info 日志,并直接调用driver.findElement/findElements(strategy, newSelector)——注意这里是直接调驱动而不是next(),因为翻译后的选择器必须作用于未经插件改写的原生 XML。

4. 源码转换核心:解析、递归改写与命名映射

transformSourceXml 基于fast-xml-parser的单例XMLParser/XMLBuilder实现,属性前缀常量ATTR_PREFIX@_(source.ts),输出前会补上<?xml version="1.0" encoding="UTF-8"?>声明。解析器配置中isArray恒返回 true(对非属性节点),保证重复节点名合并时能正确形成数组(source.ts)。

4.1 节点名映射(多对一)

映射数据结构定义在 types.ts:键是通用节点名,值按平台给出字符串或字符串数组,即支持多个平台名映射到同一个通用名(many-to-one)。查找函数 getUniversalName 遍历整张表做包含判断,未命中返回null

改名时 transformChildNodes 先递归处理子树,再替换节点名;当两个不同的原名映射到同一通用名时,它会把原有值包成数组并合并(source.ts),避免覆盖丢失。

README 中给出的三行示例(Button/Alert/SwitchInput)在 node-map.ts 中有完整映射表,这里给出全文(iOS/Android 均为数组时以顿号分隔,表示该平台无对应来源名):

通用节点名iOS 来源Android 来源
AlertXCUIElementTypeAlertandroid.widget.Toast
AppXCUIElementTypeApplication
ButtonXCUIElementTypeButton、DecrementArrow、IncrementArrow、DisclosureTriangle、Handle、Key、Link、MenuButton、PageIndicator、PopUpButton、ToolbarButton、RadioButton、Tabandroid.widget.Button、ImageButton、RadioButton、QuickContactBadge
CellXCUIElementTypeCell
CheckBoxXCUIElementTypeCheckBoxandroid.widget.CheckBox
ColumnXCUIElementTypeTableColumn
DateInputXCUIElementTypeDatePickerandroid.widget.DatePicker
ElementXCUIElementTypeOther、Any、Matte、MenuBarItem、MenuItem、Ruler、RulerMarker、Splitter、StatusItem、Timelineandroid.widget.Space、TwoLineListItem
GridXCUIElementTypeGridandroid.widget.GridLayout、GridView
IconXCUIElementTypeIcon、DockItem
ImageXCUIElementTypeImageandroid.widget.ImageView
IndicatorXCUIElementTypeLevelIndicator、ProgressIndicator、RatingIndicator、RelevanceIndicator、ValueIndicatorandroid.widget.RatingBar、ProgressBar
InputXCUIElementTypeColorWell
ListXCUIElementTypeCollectionViewandroid.widget.ListView、ExpandableListView、Gallery
MapXCUIElementTypeMap
MenuXCUIElementTypeMenu、MenuBarandroid.widget.ActionMenuView、PopupMenu
ModalXCUIElementTypeDrawer、Dialog、Popoverandroid.widget.ListPopupWindow、PopupWindow、SlidingDrawer、Magnifier
NavXCUIElementTypeNavigationBar
PickerInputXCUIElementTypePickerWheelandroid.widget.NumberPicker、TimePicker、CalendarView
RadioInputXCUIElementTypeRadioGroupandroid.widget.RadioGroup
RowXCUIElementTypeTableRow、OutlineRow、SegmentedControl、TouchBarandroid.widget.TableRow
ScrollableXCUIElementTypeScrollViewandroid.widget.ScrollView、HorizontalScrollView
SearchInputXCUIElementTypeSearchFieldandroid.widget.SearchView
SliderInputXCUIElementTypeSlider、Stepper、ScrollBarandroid.widget.SeekBar
SpinnerXCUIElementTypeActivityIndicatorandroid.widget.Spinner
SwitchInputXCUIElementTypeSwitchandroid.widget.Switch
TableXCUIElementTypeTableandroid.widget.TableLayout
TextXCUIElementTypeStaticText、TextView、HelpTagandroid.widget.TextView、Chronometer、TextClock
TextInputXCUIElementTypeTextField、SecureTextField、ComboBoxandroid.widget.EditText、AutoCompleteTextView、MultiAutoCompleteTextView
ToggleInputXCUIElementTypeToggleandroid.widget.CheckedTextView、ToggleButton
ToolbarXCUIElementTypeToolbarandroid.widget.Toolbar
UIAppiumAUThierarchy
Videoandroid.widget.VideoView
ViewXCUIElementTypeBrowser、Group、Keyboard、LayoutArea、LayoutItem、Outline、Picker、Sheet、SplitGroup、StatusBar、TabBar、TabGroupandroid.widget.FrameLayout、LinearLayout、RelativeLayout、android.view.View、ViewGroup、MediaController、StackView
WebViewXCUIElementTypeWebView
WindowXCUIElementTypeWindow

(上表中 iOS 来源为完整XCUIElementTypeXxx名的缩写展示,以 node-map.ts 中的全量定义为准。)

未收录的名称不做任何改写,仅计入 unknown 统计并触发第 3.1 节的告警日志。

4.2 属性名映射与移除清单

属性映射表 ATTR_MAP 与 README 中三行示例对应的完整内容是:

通用属性iOS 来源Android 来源
axIdnamecontent-desc
textlabeltext
visiblevisibledisplayed
x/y/width/height同名(由 bounds 换算,见 4.3)同名(由 bounds 换算,见 4.3)
idresource-id
enabledenabled见下方说明
valuevalue

README 同时强调“插件还会从转换后的 XML 中删除若干属性”,即 REMOVE_ATTRS:

index, type, package, class, checkable, checked, clickable, enabled, focusable, focused, long-clickable, password, scrollable, selected, bounds, rotation

从源码看,transformAttrs 中对每个属性先判断是否命中 REMOVE_ATTRS、命中即删除,再做映射查找。因此上表中enabled一行存在一个平台差异:iOS 的enabled会映射为通用enabled保留;Android 的enabled因先命中移除清单而被直接丢弃。同理 Android 的bounds虽在移除清单中,但它的坐标信息会先被平台转换器换算为x/y/width/height(见下节),信息并未丢失。

4.3 平台预处理转换器:Android 的 bounds 与 resource-id

除了改名,transformNode在每个节点上还会调用平台转换器(source.ts):

  • iOS 转换器是空操作(transformers.ts),因为 XCUITest 源码本身已带有x/y/width/height与短name属性;
  • Android 转换器(transformers.ts)做两件事:
    1. resource-id形如com.example:id/title的值剥离${appPackage}:id/前缀(包名即第 3.1 节从driver.opts.appPackage取的元数据),最终呈现为短id属性;
    2. bounds="[x,y][x2,y2]"拆分解析,换算出xywidthheight四个通用属性。

以测试夹具 test/fixtures/android.xml 中的一行为例,其中android.widget.EditText节点带有content-desc="username"text="alice"bounds="[150,504][930,616]"displayed="true"。按上述规则,它会被改写为TextInput节点,并携带axId="username"text="alice"visible="true"x="150"y="504"width="780"height="112";其余index/class/clickable等命中移除清单的属性则被丢弃。转换前后对照可参考夹具中的 android-transformed.xml 与 ios-transformed.xml。

4.4 indexPath:为 XPath 反查埋下的索引

transformNode 在addIndexPath开启时要求每个节点必须带index属性(缺失则直接抛错),并把父路径拼接成parent/index形式写入@indexPath属性。源码注释里特别说明了一个不对称处理(source.ts):iOS 的<AppiumAUT>根节点被 XCUITest 驱动排除在查询层级之外,因此故意不给它写 indexPath;Android 的<hierarchy>根则参与查询层级,保留其索引。

5. XPath 选择器的翻译原理

transformQuery 是整个插件最巧妙的部分,思路是“在转换后的 XML 上先跑一遍用户选择器,再把命中的节点按 indexPath 反写成原生 XML 中的位置表达式”:

  1. @xmldom/xmldom+xpath库在转换后、带 indexPath 的 XML上执行用户的通用 XPath(runQuery);
  2. 过滤掉没有indexPath的节点(即第 4.4 节中的 iOSAppiumAUT根);
  3. 把每个命中节点的indexPath(形如/0/0/1/1/0/1/0/2)逐段 +1(XPath 索引从 1 开始),映射为*[n]位置轴表达式并重新拼接,例如/0/0/1变为/*[1]/*[1]/*[2]——由于插件改名是保序的,位置轴路径在未改名的原生 XML 上同样指向原节点;
  4. 单个查询取第一条结果;多元素查询(findElements)用|合并所有命中路径(xpath.ts)。

这一机制的代价与边界也随之明确:选择器必须在原生 XML 上唯一可定位,翻译走的是“位置轴”而非属性匹配,因此页面在获取源码与实际查询之间发生重排时可能定位漂移;另外只有xpath策略 +NATIVE_APP上下文会被翻译,accessibility idclass name等其他策略直接透传给驱动处理。

6. 验证与测试入口

仓库为该插件提供了可直接运行的验证材料:

  • 夹具:test/fixtures/ 下成对存放原始与转换结果,如 android.xml ↔ android-transformed.xml、ios.xml ↔ ios-transformed.xml,另有边界场景夹具 ios-edge.xml 与 web-view.xml;
  • 单测:test/unit/plugin.spec.ts 验证插件命令拦截,test/unit/source.spec.ts 验证 XML 转换,test/unit/xpath.spec.ts 验证选择器翻译;
  • CLI 入口:index.ts 暴露了一个命令行转换工具,用法为node <构建产物>/index.js <xmlDataPath> <platform> [optsJson],把转换结果打印到 stdout、unknown 统计打印到 stderr,也支持--smoke-test冒烟检查(对应test:smoke脚本)。

7. 小结与使用边界

  • 该插件是 Appium 官方 monorepo 内的独立包,插件名universal-xml,通过appium plugin install universal-xml安装、--use-plugins=universal-xml激活;
  • 它统一的是节点名 + 属性名 + XPath 查询三层:Page Source 输出通用 XML(node-map.ts 与 attr-map.ts 定义全部映射),XPath 则通过 indexPath 反查被翻译成原生位置表达式(xpath.ts);
  • 未收录的名称/属性原样保留并打 warn 日志,映射表以源码文件为准持续扩展;
  • 适用边界:仅拦截xpath策略且上下文为NATIVE_APP的元素查找;findElements在无匹配时返回空数组而非报错;bounds换算、resource-id剥离等预处理目前只针对 Android 源(iOS 转换器为空操作),这些行为均可从 transformers.ts 直接印证。

【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 3:10:16

微博公开数据爬取实战:登录态维护、文本清洗与中文词云生成

简介&#xff1a;基于Python的微博数据采集与词云可视化项目源码包&#xff0c;面向计算机相关专业的毕业设计、课程设计以及爬虫与文本分析入门学习者。项目采用Scrapy框架搭建完整爬虫工程&#xff0c;包含爬虫核心逻辑、中间件、管道处理、设置配置与自定义工具模块&#xf…

作者头像 李华
网站建设 2026/9/13 3:08:51

DeepSeek Harness本地部署实战:从环境准备到跑通第一个任务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华