在 Appium Inspector 中选中一个控件时,只能查看到 name、label、value、xpath 这些字段。
但是在 Appium Ruby Console 中,查看同一个控件,却可以看到控件的 ID 字段。
请问这是什么原因呢?如果我要在 Appium Inspector 中查看到控件的 ID 属性,要怎么操作呢?
ios 把所有可见元素的 name,value,label 都认为是 accessiblity id
#1 楼 @lihuazhang 谢谢啦,刚采用 id 命令去试了下 name、label 和 value,的确也能定位到控件。不过我还不懂的是,为什么在 Console 中可以看到 id,但是在 Inspector 中看不到
#2 楼 @debugtalk 我理解为实现方式不一样
#3 楼 @lihuazhang 以你的经验来看,定位元素的最佳实践是采用什么方式呢?当前我是打算采用 name,但是 app 存在多国语言,这样就需要针对每一种语言进行处理;官方文档推荐采用 ID,但是 APP 里面大多控件都没有 ID 这个属性(从 Console 来看);而采用 xpath 的话,考虑到需求变动频繁,很有可能 UI 变动较大,所以也是个问题。
#4 楼 @debugtalk ID. iOS 基本元素都有 id。而且 id 是指 元素的 name,value,label
#5 楼 @lihuazhang 我还是不大明白。如果把 name,value,label 作为控件的 ID 的话,那和直接采用 find_element(:name, 'XXX') 这样的方式有什么差异呢?
而且,如果把 name,value,label 这些作为 ID,那么 APP 在不同语言下的值很有可能不一样,而且在版本迭代中,也很有可能改变,例如按钮名称变化;这也没有享受到 ID 相对较为固定的好处啊。
#6 楼 @debugtalk 一般来说会指定一个 ID 的,通常是 label 这种。不会一直变。find_element(:name, 'XXX') 本来就没什么差异。
没用过 ruby console 。。。据我所知本身 iOS 控件应该没有 id 这样的属性的,一般用的是 AccessibilityLabel 或者 AccessibilityIdentify。你能把在 ruby console 获取 id 时的 appium server log 发上来看看实际上请求的是什么命令吗?
#8 楼 @chenhengjie123
我也是在看官方文档时看到的,说通过 ID 定位元素是更好的方式。
按照你的要求,我在 Ruby Console 里面通过 id 查询某元素。
➜ arc
[1] pry(main)> page
UIAButton
name, label: My Account
id: My Account => My Account
nil
[2] pry(main)> id('My Account').name
"My Account"
Appium Server Log 里面对应的日志:
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:33 Leos-Mac-mini CoreSimulatorBridge[52416]: Switching to keyboard: zh-Hans
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:33 Leos-Mac-mini CoreSimulatorBridge[52416]: KEYMAP: Failed to determine iOS keyboard layout for language zh-Hans.
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:33 Leos-Mac-mini CoreSimulatorBridge[52416]: Switching to keyboard: zh-Hans
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:33 Leos-Mac-mini CoreSimulatorBridge[52416]: KEYMAP: Failed to determine iOS keyboard layout for language zh-Hans.
[HTTP] --> POST /wd/hub/session/9884af6e-bb65-44d5-9489-e4e73e135f64/element {"using":"id","value":"My Account"}
[MJSONWP] Calling AppiumDriver.findElement() with args: ["id","My Account","9884af6e-bb65-44d5-9489-e4e73e135f64"]
[debug] [iOS] Executing iOS command 'findElement'
[debug] [BaseDriver] Waiting up to 0 ms for condition
[debug] [UIAuto] Sending command to instruments: au.getElementByAccessibilityId('My Account')
[debug] [Instruments] [INST] 2016-05-27 06:12:35 +0000 Debug: Got new command 17 from instruments: au.getElementByAccessibilityId('My Account')
[debug] [Instruments] [INST] 2016-05-27 06:12:35 +0000 Debug: evaluating au.getElementByAccessibilityId('My Account')
[debug] [Instruments] [INST] 2016-05-27 06:12:35 +0000 Debug: evaluation finished
[debug] [Instruments] [INST] 2016-05-27 06:12:35 +0000 Debug: Lookup returned [object UIAButton] with the name "My Account" (id: 2).
[debug] [Instruments] [INST] 2016-05-27 06:12:35 +0000 Debug: responding with:
[debug] [Instruments] [INST] 2016-05-27 06:12:35 +0000 Debug: Running system command #18: /Applications/Appium.app/Contents/Resources/node/bin/node /Applications/Appium.app/Contents/Resources/node_modules/appium/node_modules/appium-ios-driver/node_modules/appium-uiauto/build/lib/bin/command-proxy-client.js /var/folders/wy/fh5d0cn53b5714pkw7282n540000gn/T/instruments_sock 2,{"status":0,"v...
[debug] [UIAuto] Socket data received (38 bytes)
[debug] [UIAuto] Got result from instruments: {"status":0,"value":{"ELEMENT":"2"}}
[MJSONWP] Responding to client with driver.findElement() result: {"ELEMENT":"2"}
[HTTP] <-- POST /wd/hub/session/9884af6e-bb65-44d5-9489-e4e73e135f64/element 200 1226 ms - 87
[HTTP] --> GET /wd/hub/session/9884af6e-bb65-44d5-9489-e4e73e135f64/element/2/attribute/name {}
[MJSONWP] Calling AppiumDriver.getAttribute() with args: ["name","2","9884af6e-bb65-44d5-9489-e4e73e135f64"]
[debug] [iOS] Executing iOS command 'getAttribute'
[debug] [UIAuto] Sending command to instruments: au.getElement('2').name()
[debug] [Instruments] [INST] 2016-05-27 06:12:36 +0000 Debug: Got new command 18 from instruments: au.getElement('2').name()
[debug] [Instruments] [INST] 2016-05-27 06:12:36 +0000 Debug: evaluating au.getElement('2').name()
[debug] [Instruments] [INST] 2016-05-27 06:12:36 +0000 Debug: evaluation finished
[debug] [Instruments] [INST] 2016-05-27 06:12:36 +0000 Debug: responding with:
[debug] [Instruments] [INST] 2016-05-27 06:12:36 +0000 Debug: Running system command #19: /Applications/Appium.app/Contents/Resources/node/bin/node /Applications/Appium.app/Contents/Resources/node_modules/appium/node_modules/appium-ios-driver/node_modules/appium-uiauto/build/lib/bin/command-proxy-client.js /var/folders/wy/fh5d0cn53b5714pkw7282n540000gn/T/instruments_sock 2,{"status":0,"v...
[debug] [UIAuto] Socket data received (35 bytes)
[debug] [UIAuto] Got result from instruments: {"status":0,"value":"My Account"}
[MJSONWP] Responding to client with driver.getAttribute() result: "My Account"
[HTTP] <-- GET /wd/hub/session/9884af6e-bb65-44d5-9489-e4e73e135f64/element/2/attribute/name 200 1064 ms - 84
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:37 Leos-Mac-mini CoreSimulatorBridge[52416]: Switching to keyboard: zh-Hans
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:37 Leos-Mac-mini CoreSimulatorBridge[52416]: KEYMAP: Failed to determine iOS keyboard layout for language zh-Hans.
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:37 Leos-Mac-mini CoreSimulatorBridge[52416]: Switching to keyboard: zh-Hans
[iOSLog] [IOS_SYSLOG_ROW] May 27 14:12:37 Leos-Mac-mini CoreSimulatorBridge[52416]: KEYMAP: Failed to determine iOS keyboard layout for language zh-Hans.
[HTTP] --> GET /wd/hub/status {}
[MJSONWP] Calling AppiumDriver.getStatus() with args: []
[MJSONWP] Responding to client with driver.getStatus() result: {"build":{"version":"1.5.2","revision":null}}
[HTTP] <-- GET /wd/hub/status 200 17 ms - 83
[HTTP] --> GET /wd/hub/status {}
[MJSONWP] Calling AppiumDriver.getStatus() with args: []
[MJSONWP] Responding to client with driver.getStatus() result: {"build":{"version":"1.5.2","revision":null}}
[HTTP] <-- GET /wd/hub/status 200 16 ms - 83
#10 楼 @debugtalk 我想看的是 page
命令对应的 log ,但你给的貌似是 id('My Account').name
的(log 里面一开始就是 find element)。能补充一下吗?
#11 楼 @chenhengjie123
page 命令对应的 log 如下:
[HTTP] --> GET /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/context {}
[MJSONWP] Calling AppiumDriver.getCurrentContext() with args: ["4d6f5865-be02-4462-a33f-59304d4f2b20"]
[debug] [iOS] Executing iOS command 'getCurrentContext'
[MJSONWP] Responding to client with driver.getCurrentContext() result: "NATIVE_APP"
[HTTP] <-- GET /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/context 200 8 ms - 84
[HTTP] --> GET /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/context {}
[MJSONWP] Calling AppiumDriver.getCurrentContext() with args: ["4d6f5865-be02-4462-a33f-59304d4f2b20"]
[debug] [iOS] Executing iOS command 'getCurrentContext'
[MJSONWP] Responding to client with driver.getCurrentContext() result: "NATIVE_APP"
[HTTP] <-- GET /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/context 200 3 ms - 84
[HTTP] --> POST /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/execute {"script":"UIATarget.localTarget().frontMostApp().windows()[0].getTree()","args":[]}
[MJSONWP] Calling AppiumDriver.execute() with args: ["UIATarget.localTarget().frontMostApp().windows()[0].getTree()",[],"4d6f5865-be02-4462-a33f-59304d4f2b20"]
[debug] [iOS] Executing iOS command 'execute'
[debug] [UIAuto] Sending command to instruments: UIATarget.localTarget().frontMostApp().windows()[0].getTree()
[debug] [Instruments] [INST] 2016-05-27 12:30:39 +0000 Debug: Got new command 3 from instruments: UIATarget.localTarget().frontMostApp().windows()[0].getTree()
[debug] [Instruments] [INST] 2016-05-27 12:30:39 +0000 Debug: evaluating UIATarget.localTarget().frontMostApp().windows()[0].getTree()
[debug] [Instruments] [INST] 2016-05-27 12:30:39 +0000 Debug: evaluation finished
[debug] [Instruments] [INST] 2016-05-27 12:30:39 +0000 Debug: responding with:":{"x":0,"y":0},"size":{"width":375,"height":618}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[{"name":"no_login_icon","type":"UIAImage","label":null,"value":null,"rect":{"origin":{"x":157.5,"y":60},"size":{"width":60,"height":60}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"XXX","type":"UIAStaticText","label":"XXX","value":"XXX","rect":{"origin":{"x":20,"y":135},"size":{"width":335,"height":20.5}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"Label","type":"UIAStaticText","label":"Label","value":"Label","rect":{"origin":{"x":0,"y":196},"size":{"width":375,"height":20.5}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"Fans","type":"UIAStaticText","label":"Fans","value":"Fans","rect":{"origin":{"x":217.5,"y":220},"size":{"width":30.5,"height":20}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"Follow","type":"UIAStaticText","label":"Follow","value":"Follow","rect":{"origin":{"x":116,"y":220},"size":{"width":41.5,"height":20}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"100","type":"UIAStaticText","label":"100","value":"100","rect":{"origin":{"x":122.5,"y":198},"size":{"width":28.5,"height":22}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"200","type":"UIAStaticText","label":"200","value":"200","rect":{"origin":{"x":217.5,"y":198},"size":{"width":31,"height":22}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"me edite","type":"UIAButton","label":"me edite","value":null,"rect":{"origin":{"x":335,"y":25},"size":{"width":30,"height":30}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[{"name":"me_edite","type":"UIAImage","label":null,"value":null,"rect":{"origin":{"x":337.5,"y":27.5},"size":{"width":25,"height":25}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null}],"hint":null},{"name":"Login","type":"UIAButton","label":"Login","value":null,"rect":{"origin":{"x":44.5,"y":175},"size":{"width":123,"height":30}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"Register","type":"UIAButton","label":"Register","value":null,"rect":{"origin":{"x":207.5,"y":175},"size":{"width":123,"height":30}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"Hi!Guest","type":"UIAStaticText","label":"Hi!Guest","value":"Hi!Guest","rect":{"origin":{"x":0,"y":134.5},"size":{"width":375,"height":20.5}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"Label","type":"UIAStaticText","label":"Label","value":"Label","rect":{"origin":{"x":0,"y":165.5},"size":{"width":375,"height":20.5}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"User Feedback ","type":"UIATableCell","label":null,"value":"","rect":{"origin":{"x":0,"y":260},"size":{"width":375,"height":50}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[{"name":"User Feedback ","type":"UIAStaticText","label":"User Feedback ","value":"User Feedback ","rect":{"origin":{"x":50,"y":276},"size":{"width":107.5,"height":18}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null}],"hint":null},{"name":null,"type":"UIATableGroup","label":null,"value":null,"rect":{"origin":{"x":0,"y":310},"size":{"width":375,"height":20}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":"System Settings","type":"UIATableCell","label":null,"value":"","rect":{"origin":{"x":0,"y":330},"size":{"width":375,"height":50}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[{"name":"System Settings","type":"UIAStaticText","label":"System Settings","value":"System Settings","rect":{"origin":{"x":50
,"y":346},"size":{"width":111.5,"height":18}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null}],"hint":null}],"hint":null},{"name":null,"type":"UIATabBar","label":null,"value":null,"rect":{"origin":{"x":0,"y":618},"size":{"width":375,"height":49}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[{"name":null,"type":"UIAImage","label":null,"value":null,"rect":{"origin":{"x":0,"y":617.5},"size":{"width":375,"height":0.5}},"dom":null,"enabled":true,"valid":true,"visible":false,"children":[],"hint":null},{"name":null,"type":"UIAImage","label":null,"value":null,"rect":{"origin":{"x":0,"y":618},"size":{"width":375,"height":49}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"Store","type":"UIAButton","label":"Store","value":null,"rect":{"origin":{"x":2,"y":619},"size":{"width":71,"height":48}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"Experience","type":"UIAButton","label":"Experience","value":null,"rect":{"origin":{"x":77,"y":619},"size":{"width":71,"height":48}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"Nearby","type":"UIAButton","label":"Nearby","value":null,"rect":{"origin":{"x":152,"y":619},"size":{"width":71,"height":48}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"Forum","type":"UIAButton","label":"Forum","value":null,"rect":{"origin":{"x":227,"y":619},"size":{"width":71,"height":48}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null},{"name":"My Account","type":"UIAButton","label":"My Account","value":1,"rect":{"origin":{"x":302,"y":619},"size":{"width":71,"height":48}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[],"hint":null}],"hint":null}],"hint":null}}
[debug] [Instruments] [INST] 2016-05-27 12:30:39 +0000 Debug: Running system command #4: /Applications/Appium.app/Contents/Resources/node/bin/node /Applications/Appium.app/Contents/Resources/node_modules/appium/node_modules/appium-ios-driver/node_modules/appium-uiauto/build/lib/bin/command-proxy-client.js /var/folders/wy/fh5d0cn53b5714pkw7282n540000gn/T/instruments_sock 2,{"status":0,"v...
[debug] [UIAuto] Socket data received (6165 bytes)
[debug] [UIAuto] Got result from instruments: {"status":0,"value":{"name":null,"type":"UIAWindow","label":null,"value":null,"rect":{"origin":{"x":0,"y":0},"size":{"width":375,"height":667}},"dom":null,"enabled":true,"valid":true,"visible":true,"children":[{"name":null,"type":"UIATableView","label":null,"value":"rows 1 to 2 of 2","rect":{"origin
[MJSONWP] Responding to client with driver.execute() result: {"name":null,"type":"UIAWindow","label":null,"value":null,"rect":{"origin":{"x":0,"y":0},"size":{"width":375,"height":667}},"dom":null,"enabled":tr...
[HTTP] <-- POST /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/execute 200 1100 ms - 6238
[HTTP] --> POST /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/appium/app/strings {}
[MJSONWP] Calling AppiumDriver.getStrings() with args: [null,null,"4d6f5865-be02-4462-a33f-59304d4f2b20"]
[debug] [iOS] Executing iOS command 'getStrings'
[debug] [iOS] Gettings strings for language 'undefined' and string file 'null'
[debug] [iOS] No language specified. Using default strings
[debug] [iOS] Strings file not found. Looking in 'en.lproj' directory
[debug] [iOS] Parsed app 'Localizable.strings'
[MJSONWP] Responding to client with driver.getStrings() result: {"Pull down to refresh":"Pull down to refresh","Please input the new password.":"Please input your new password.","再看看":"Cancel","Total":"Total","我...
[HTTP] <-- POST /wd/hub/session/4d6f5865-be02-4462-a33f-59304d4f2b20/appium/app/strings 200 34 ms - 51687
#12 楼 @debugtalk 从 log 来看,server 返回的这个控件的属性如下:
{
"name": "My Account",
"type": "UIAButton",
"label": "My Account",
"value": 1,
"rect": {
"origin": {
"x": 302,
"y": 619
},
"size": {
"width": 71,
"height": 48
}
},
"dom": null,
"enabled": true,
"valid": true,
"visible": true,
"children": [],
"hint": null
}
里面并没有返回名为 id 的属性。估计这个属性是 ruby client 或者 ruby console 自己添加的。然后看了下 ruby client 的源码,发现的确是它自己加的。相关代码:
...
# there may be many ids with the same value.
# output all exact matches.
attributes = [name, label, value, hint].select { |attr| !attr.nil? }
partial = {}
id_matches = @strings_xml.select do |key, val|
next if val.nil? || val.empty?
partial[key] = val if attributes.detect { |attr| attr.include?(val) }
attributes.detect { |attr| val == attr }
end
# If there are no exact matches, display partial matches.
id_matches = partial if id_matches.empty?
unless id_matches.empty?
match_str = ''
max_len = id_matches.keys.max_by(&:length).length
# [0] = key, [1] = val
id_matches.each do |key, val|
arrow_space = ' ' * (max_len - key.length).to_i
match_str += ' ' * 7 + "#{key} #{arrow_space}=> #{val}\n"
end
puts " id: #{match_str.strip}\n"
end
...
完整的源码:https://github.com/appium/ruby_lib/blob/master/lib/appium_lib/ios/helper.rb。
当前我是打算采用 name,但是 app 存在多国语言,这样就需要针对每一种语言进行处理;官方文档推荐采用 ID,但是 APP 里面大多控件都没有 ID 这个属性(从 Console 来看);
关于这个补充回答一下,做 UI 自动化时一般需要手动对控件添加 AccessibilityLabel 来保证其唯一性的。这个属性仅用于做盲人辅助/UI 自动化,和界面显示、多语言之类的都无关。大部分情况下开发不会主动添加这个属性,个别控件会有这个属性值的原因是它们的 AccessibilityLabel 默认值就是控件的某个属性,但这种一般无法满足自动化的需要。
#14 楼 @chenhengjie123 太感谢啦,总算解决这些天的困惑了。