51Testing软件测试论坛

标题: appium 中 sendkeys 方法会输入原有字符的原因及解决方案 [打印本页]

作者: 悠悠小仙仙    时间: 2017-6-20 15:25
标题: appium 中 sendkeys 方法会输入原有字符的原因及解决方案
前言之前看到一个帖子(appium 1.3.4.1 版 sendkey 错误),里面提到了在appium 1.3.4.1中用sendKeys输入文本会变成追加文本,而appium 1.2.4则没有这个问题。同时如果sendKeys前有先clear也不会出现这个问题。周末有时间在源码环境下探究了一下具体原因,结果不仅发现了具体原因,还发现了一个小bug。
调试环境appium版本:1.3.6(REV faf0873919a70c930b32df48e7653e8fe830a142)
所用IDE:      WebStorm(node.js部分),IDEA(Android上使用的bootstrap部分)
源码修改:    为了能对bootstrap进行远程调试,我在appium/libs/devices/android/uiautomator.js的启动参数中添加了-e debug true这个参数:




被测app:     自行制作的app,里面只有一个界面,界面中有一个editText控件,控件的默认值为“中文”(非hint text)。控件具有content-description,因此脚本中使用id来查找控件。
注意:添加了这个参数后bootstrap启动时会一直等待直到有debugger联系到它。所以非debug用途请勿做此修改。
至于如何设置远程调试bootstrap,调试方法和调试Android UIAutomator一样。由于不是本文的重点,因此不再仔细介绍,有兴趣的同学请自行搜索。
SendKeys工作过程log分析SendKeys从 appium server->bootstraps->最终输入 的完整log如下:
  1. info: --> POST /wd/hub/session/be7c34f0-e12d-4368-9f80-c116e0cc3990/element/1/value {"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990","id":"1","value":["f","i","r","s","t"]}
  2. info: [debug] Pushing command to appium work queue: ["element:setText",{"elementId":"1","text":"first","replace":false}]
  3. info: [debug] [BOOTSTRAP] [debug] Got data from client: {"cmd":"action","action":"element:setText","params":{"elementId":"1","text":"first","replace":false}}
  4. info: [debug] [BOOTSTRAP] [debug] Got command of type ACTION
  5. info: [debug] [BOOTSTRAP] [debug] Got command action: setText
  6. info: [debug] [BOOTSTRAP] [debug] Attempting to clear using UiObject.clearText().
  7. info: [debug] [BOOTSTRAP] [debug] Sending plain text to element: 中文first
  8. info: [debug] [BOOTSTRAP] [debug] Returning result: {"value":true,"status":0}
  9. info: [debug] Responding to client with success: {"status":0,"value":true,"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990"}
  10. info: <-- POST /wd/hub/session/be7c34f0-e12d-4368-9f80-c116e0cc3990/element/1/value 200 9119.841 ms - 76 {"status":0,"value":true,"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990"}
复制代码
简单说一下各个log是什么意思:
  1. info: --> POST /wd/hub/session/be7c34f0-e12d-4368-9f80-c116e0cc3990/element/1/value {"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990","id":"1","value":["f","i","r","s","t"]}

  2. 表示server接收到一个post类型的http请求,post的url是/wd/hub/session/be7c34f0-e12d-4368-9f80-c116e0cc3990/element/1/value,post请求的内容(即http协议中的的body部分)为{"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990","id":"1","value":["f","i","r","s","t"]}
  3. 此部分的通讯遵循webdriver协议,无论哪个client发出来都是采用这个格式

  4. info: [debug] Pushing command to appium work queue: ["element:setText",{"elementId":"1","text":"first","replace":false}]

  5. 表示server把["element:setText",{"elementId":"1","text":"first","replace":false}]这个命令放到待处理命令序列中。

  6. info: [debug] [BOOTSTRAP] [debug] Got command of type ACTION
  7. info: [debug] [BOOTSTRAP] [debug] Got command action: setText

  8. 表示bootstrap收到类型为action,名称为setText的命令

  9. info: [debug] [BOOTSTRAP] [debug] Attempting to clear using UiObject.clearText().

  10. 表示执行UiObject.clearText()来清除元素的现有文字

  11. info: [debug] [BOOTSTRAP] [debug] Sending plain text to element: 中文first

  12. 表示在元素中输入中文first

  13. info: [debug] [BOOTSTRAP] [debug] Returning result: {"value":true,"status":0}

  14. 表示bootstrap返回执行结果{"value":true,"status":0}

  15. info: [debug] Responding to client with success: {"status":0,"value":true,"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990"}

  16. 表示appium server准备返回{"status":0,"value":true,"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990"}给发起请求的client

  17. info: <-- POST /wd/hub/session/be7c34f0-e12d-4368-9f80-c116e0cc3990/element/1/value 200 9119.841 ms - 76 {"status":0,"value":true,"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990"}

  18. 表示appium server返回url为/wd/hub/session/be7c34f0-e12d-4368-9f80-c116e0cc3990/element/1/value,http状态码为200,内容为{"status":0,"value":true,"sessionId":"be7c34f0-e12d-4368-9f80-c116e0cc3990"}的数据包给client
复制代码
提出疑问通过分析,里面有几个让人在意的地方:
带着这几个问题,我们开始分析及调试源码。
解决疑问所有命令都是先通过routing分析来源来调用对应的方法。因此先查routing:

  1. appium/lib/server/routing.js
  2. ...
  3.   rest.post('/wd/hub/session/:sessionId?/element/:elementId?/value', controller.setValue);
  4. ...
  5.   rest.post('/wd/hub/session/:sessionId?/appium/element/:elementId?/value', controller.setValueImmediate);
  6.   rest.post('/wd/hub/session/:sessionId?/appium/element/:elementId?/replace_value', controller.replaceValue);
复制代码
找到三个和serValue相关的命令。通过url可以看出只有setValue方法是遵循webdriver api的,其它两个方法都是appium自己另外添加的。这里的controller方法的源码分别如下:
  1. appium/lib/server/controller.js
  2. ...
  3. exports.setValue = function (req, res) {
  4.   var elementId = req.params.elementId
  5.       // spec says value attribute is an array of strings;
  6.       // let's turn it into one string
  7.     , value = req.body.value.join('');

  8.   req.device.setValue(elementId, value, getResponseHandler(req, res));
  9. };

  10. exports.replaceValue = function (req, res) {
  11.   var elementId = req.params.elementId
  12.       // spec says value attribute is an array of strings;
  13.       // let's turn it into one string
  14.     , value = req.body.value.join('');

  15.   req.device.replaceValue(elementId, value, getResponseHandler(req, res));
  16. };
  17. ...
  18. exports.setValueImmediate = function (req, res) {
  19.   var element = req.params.elementId
  20.     , value = req.body.value;
  21.   if (checkMissingParams(req, res, {element: element, value: value})) {
  22.     req.device.setValueImmediate(element, value, getResponseHandler(req, res));
  23.   }
  24. };
  25. ...
复制代码



作者: 悠悠小仙仙    时间: 2017-6-20 15:26
此处req.device会根据平台不同使用不同的实现来执行。此处我们仅关注android平台,它的实现方法如下:
appium/lib/devices/android/android-controller.js
...
androidController.setValue = function (elementId, value, cb) {
var params = {
elementId: elementId,
text: value,
replace: false
};
if (this.args.unicodeKeyboard) {
params.unicodeKeyboard = true;
}
this.proxy(["element:setText", params], cb);
};

androidController.replaceValue = function (elementId, value, cb) {
var params = {
elementId: elementId,
text: value,
replace: true
};
if (this.args.unicodeKeyboard) {
params.unicodeKeyboard = true;
}
this.proxy(["element:setText", params], cb);
};
...
androidController.setValueImmediate = function (elementId, value, cb) {
cb(new NotYetImplementedError(), null);
};
...
可以看到replace和unicodeKeyboard参数都是在这里加入的。其中setValue方法和replaceValue方法唯一区别是replace参数的值,unicodeKeyboard是根据server的unicodeKeyboard参数值(就是启动session时的传入的unicodeKeyboard参数)决定的。而setValueImmediate方法还没实现,因此不再探究。
至此,第一个疑问解决。repalce和unicodeKeyboard是在appium/lib/devices/android/android-controller.js中根据调用的方法来设定的。其中setValue的replace参数值固定为false
bootstrap部分
关于bootstrap源码的分析我主要参考了http://www.it165.net/pro/html/201407/17696.html,里面已经大致说明了setText方法的实现是放在bootstrap/src/io/appium/android/bootstrap/handler/SetText.java中:
appium/libs/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/SetText.java
...
boolean replace = Boolean.parseBoolean(params.get("replace").toString());
String text = params.get("text").toString();
...
boolean unicodeKeyboard = false;
if (params.get("unicodeKeyboard") != null) {
unicodeKeyboard = Boolean.parseBoolean(params.get("unicodeKeyboard").toString());
}
String currText = el.getText();
new Clear().execute(command);
if (!el.getText().isEmpty()) {
// clear could have failed, or we could have a hint in the field
// we'll assume it is the latter
Logger.debug("Text not cleared. Assuming remainder is hint text.");
currText = "";
}
if (!replace) {
text = currText + text;
}
final boolean result = el.setText(text, unicodeKeyboard);
if (!result) {
return getErrorResult("el.setText() failed!");
}
...
return getSuccessResult(result);
...
从这里看到,从获得命令到完成输入一共有以下步骤:
判断并存储replace, text, unicodeKeyboard参数的值
通过getText获取当前元素的文字,存到currText中
使用new Clear().execute(command);清除当前元素的所有文字
再次获取当前元素文字。如果文字仍不为空,认定它是hint text并把currText置空(由于此处也有可能是clear方法出错导致没有clear成功,因此留了一个log说明假设还存在的text是hint text)
如果replace不是true,在text前面加入currText。
调用setText方法执行实际输入。
在这里解答了第二个疑问:sendKeys的文字会在前面加了“中文”这两个字符是因为它在第5步加入了元素原来的text内容。即采用追加方法来输入文本。
为何早期appium版本(如1.2.4)没有采用追加,而现在采用追加 导致破坏了向下兼容性呢?答案是 早期版本的实现实际上是错的。
webdriver api中对于sendKeys方法的描述明确说明了SendKeys的实现应该是 在当前文本框的文字最后采用追加形式来输入文本。 要实现清除文本框内容后输入文本应该在脚本中采用先Clear后SendKeys的方式。
到这里为止,已经基本解决了帖子中的问题。但对于第三个疑问,目前还没看到哪里出问题。而且UIAutomator API的setText方法并没有uincodeKeyboard这个参数,所以推测这里的setText并不是UiObject的setText方法。再来看看这里的el.setText(text, unicodeKeyboard)的实现:
appium/libs/devices/android/bootstrap/src/io/appium/android/bootstrap/AndroidElement.java
...
AndroidElement(final String id, final UiObject el) {
this.el = el;
this.id = id;
}
...
public boolean setText(final String text, boolean unicodeKeyboard)
throws UiObjectNotFoundException {
if (unicodeKeyboard && UnicodeEncoder.needsEncoding(text)) {
Logger.debug("Sending Unicode text to element: " + text);
String encodedText = UnicodeEncoder.encode(text);
Logger.debug("Encoded text: " + encodedText);
return el.setText(encodedText);
} else {
Logger.debug("Sending plain text to element: " + text);
return el.setText(text);
}
}
...
可以看到,这里的el是UiAutomator的UiObject对象了。然后根据setText函数看到输入文本的具体步骤:
判断unicodeKeyboard是否为true,如果是还要检查需要输入的文本是否需要进行encode,两者均为true,用UnicodeEncoder.encode(text)把文本编码,然后再把编码后的文本发给UiObject的setText方法
如果不符合上面的条件,直接调用UiObject的setText方法。
咋看之下没啥问题。但我在调试过程中使用Evaluate Expression功能单独运行setText("中文"),结果 文本框没有输入任何值,但返回值为true,而setText("first")则能正常输入!
由此解决了第三个疑问,同时发现一个bug:
当没有设定unicodeKeyboard为true的情况下,直接使用sendKeys方法,当editText的默认值(非hint text)含有非ASCII字符时,会遇到脚本没有报错,但实际上没有输入任何内容的情况
从用户角度,在unicodeKeyboard为false情况下接收到含有无法输入的字符时,应该直接报错并说明无法输入。否则这个bug的性质就如同 你添加了一个记录,系统显示添加成功,但实际上没有添加进去这么坑爹。
从哪个版本开始sendKeys变成了追加:
经过在appium的repo中查询,查到把sendKeys改为追加的commit是:https://github.com/appium/appium ... 9c4074bac318fdc7195
这个commit存在于v1.2.2及以后的所有tag中,所以应该1.2.2以后的appium都变成了追加模式。至于帖子中为何说1.2.4还是替换模式,由于手上没有1.2.4的appium,所以暂时还不清楚。
总结
sendKeys在webdriver API中本来就是在文本后追加。Appium早期版本(1.2.2以前)错误地把它实现成了替换当前文本,在1.2.2后修复。
如果想实现替换当前文本,可以先使用clear方法清空文本,再使用sendKeys方法输入文本。
在不确定文本框默认值是否含有非ASCII字符前,如无特殊原因,测试android应用请尽量设置unicodeKeyboard为true(关于unicode的具体说明详见https://github.com/testerhome/ap ... ppium/unicode.cn.md)。
作者: 巴黎的灯光下    时间: 2017-6-20 15:37
层层剖析,写的真是太好了。
作者: 悠悠小仙仙    时间: 2017-6-20 15:38
巴黎的灯光下 发表于 2017-6-20 15:37
层层剖析,写的真是太好了。

谢谢支持!




欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2