在前文开源类库iQuery Android版使用说明和类jQuery selector的控件查询iQuery开源类库介绍中,介绍了iQuery的基本用法。
iQuery是一个开源的自动化测试框架项目,有兴趣的朋友可以在这里下载:
https://github.com/vowei/iQuery/downloads
源码位置:
https://github.com/vowei/iQuery
iQuery的一个主要目标就是提供一个跨平台的控件查询机制,那就需要考虑如下几个平台差异性:
- 编程语言的差异,例如iOS可以使用Object-C、JavaScript等语言编程,Android平台使用Java,而Windows 8平台使用C#、C++,网页自动化程序例如Selenium又支持很多编程语言。
iQuery在设计时就考虑到这些差异性,我们复用antlr这个工具,它已经提供了生成多种编程语言代码的功能,可以很快生成C#、C++、JavaScript、Java、Object-C、Python、Ruby等代码,这样只要维护一套语法就可以了。
虽然当前只实现了Java和JavaScript的版本,但对其他编程语言的支持也很容易实现,这是iQuery的第一个扩展点。
- 控件的差异性,不仅各平台有一些不同的控件,例如Android上的ExpandableListView在iOS上就找不到对应的控件,而且同一个控件在不同平台的名字也不一样,例如iOS上的UIASwitch基本上可以等价于Android和Windows 8上的Radio,这样都是在设计iQuery都需要去考虑的。
针对各平台控件的差异性,iQuery的做法是提供按类型名查询控件的语法,例如在Andorid上可以直接用
“>> ExpandableListView” .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
这样的查询语句找到界面上所有的ExpandableListView,而在iOS上可以使用
“>> UIAScrollView” .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
而针对同一控件在各平台名字不同的情况,iQuery的做法是提供一个伪类的概念,这个概念也是借自jQuery,例如下面的伪类就统一表示了各平台下可以当作单选框按钮的控件:
“:radio” .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
然而,在实现iQuery的时候,我们并不能假设:radio在Android上就是RadioButton控件,在iOS上就应该是UIASwitch,在Windows Phone上就是RadioBox控件,因此我们决定将定义伪类的控制权交给开发者,这是iQuery的第二个扩展点。
- 控件属性的差异性,例如同是按钮控件,在iOS上就是UIAButton.name(),而在Android上却又是mText属性,甚至有些属性在不同的平台上,有的存在,有的不存在,比如说Android上有mBottom属性,在iOS上就不存在。
跟解决控件的差异性方法类似,iQuery除了提供按属性名和方法名读取属性值以外,例如
iOS上的 “:button [name = ‘确定’]” .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
Android上的 “:button [mText = ‘确定’]” .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
为了统一表达控件在各平台通用的属性,iQuery还提供了伪属性的概念,比如上面的例子可以使用下面的查询完成:
“:button [:text = ‘确定’]” .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
跟伪类一样,伪属性也是可以由开发者自定义,这是iQuery的第三个扩展点。
- UI自动化框架的差异,例如 iOS UI自动化测试的框架必然和Andorid UI自动化测试框架不同,但无论如何不同,当今大部分操作系统的图形界面都有下面的性质:
为了尽可能的支持更多的框架,iQuery将上面的性质封装成两个接口,因此对新平台的支持,只要实现这两个接口就可以了
本文介绍扩展伪类、伪属性和添加对新平台支持的方法,后续文章会解释支持其他编程语言的做法。
扩展伪类
在Java版本中,在iQA.Runtime.jar包里,可以通过iQueryParser. registerPseudoClass这个函数注册一个新的伪类,步骤如下:
iQueryParser. createParser(String iquery, boolean registerPseudo) .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
- 创建一个iQueryParser实例。
再使用iQueryParser. registerPseudoClass(String name, IPseudoClass func)注册一个新的伪类,例如下面的代码,注册一个名为text的伪类,过滤方式为所有类型名以EditText结尾的控件:
parser.registerPseudoClass(
"text",
new IPseudoClass() {
public boolean resolve(ITreeNode node) {
return filterByNameEndsWith(node,
"EditText"); } }); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
在JavaScript版本中,暂时不支持扩展伪类的做法,后续版本会添加这个功能。
扩展伪属性
在Java版中,通过iQueryParser.registerPseudoAttribute函数注册一个新的伪属性,步骤如下:
- 使用iQueryParser. createParser(String iquery, boolean registerPseudo)创建一个iQueryParser实例。
- 再使用iQueryParser. registerPseudoAttribute (String name, IPseudoAttribute func)注册一个新的伪属性,例如下面的代码,注册一个名为bottom的伪类:
parser.registerPseudoAttribute("bottom", new IPseudoAttribute() { public String resolve(ITreeNode node) { return node.getProperty("mBottom").getValue(); } });
在iOS的JavaScript版本中的做法是:
- 引入以下几个JavaScript文件:
#import "common.js"; #import "antlr3-all-min.js"; #import "iQueryLexer.js"; #import "iQueryParser.js"; #import "error.js"; .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
- 创建一个iQuery实例:
var iq = new iQuery(selector); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
- 注册伪属性: iq.parser.registerPseudoAttrs("bottom", function(uiaobj) { if ( uiaobj != undefined && uiaobj.rect != undefined ) { var rect = uiaobj.rect(); return rect.origin.y + rect.size.height; } }); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
添加对新平台的支持
当前支持Java版本的扩展,扩展方式是:
- 在工程里添加iQA.Runtime.jar包依赖。
- 实现ITreeNode和IProperty接口。
- 例如iQuery for Android Instrument的版本就是通过这种方法实现的:
cc.iqa.iquery.android.SoloTreeNode.java:
package cc.iqa.iquery.android; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import android.view.View; import android.view.ViewGroup; import cc.iqa.iquery.*;
public
class SoloTreeNode implements ITreeNode {
private View _view =
null;
public SoloTreeNode(View view) { _view = view; }
public SoloTreeNode(View view, SoloTreeNode parent) {
this(view); _parent = parent; }
public View getView() {
return _view; } @Override
public String getName() {
return _view.getClass().toString(); }
public boolean containsProperty(String key) {
return getMethod(key) !=
null || getField(key) !=
null; }
public IProperty getProperty(String key) { Method method = getMethod(key); Object
value =
null;
if (method !=
null) {
try {
value = method.invoke(_view); }
catch (IllegalArgumentException e) {
return
null; }
catch (IllegalAccessException e) {
return
null; }
catch (InvocationTargetException e) {
return
null; } }
else { Field field = getField(key);
if ( field !=
null ) { field.setAccessible(
true);
try {
value = field.get(_view); }
catch (IllegalArgumentException e) {
return
null; }
catch (IllegalAccessException e) {
return
null; } } }
return
new SoloProperty(key,
value.toString()); }
private ITreeNode _parent; @Override
public ITreeNode getParent() {
return _parent; }
private List<ITreeNode> _children; @Override
public List<ITreeNode> getChildren() {
if (_children ==
null) { _children =
new ArrayList<ITreeNode>();
if (_view instanceof ViewGroup) { addChildren(_children, (ViewGroup) _view); } }
return _children; }
private Method getMethod(String key) { Class<?> cls = _view.getClass(); String getter = String.format(
"%1$s", key);
try {
return cls.getMethod(getter); }
catch (SecurityException e) {
return
null; }
catch (NoSuchMethodException e) {
return
null; } }
private Field getField(String key) { Class<?> cls = _view.getClass(); Field ret =
null;
while ( ret ==
null && cls !=
null ) {
try { ret = cls.getDeclaredField(key); }
catch (SecurityException e) { }
catch (NoSuchFieldException e) { }
if ( ret !=
null )
break;
try { ret = cls.getField(key); }
catch (SecurityException e) { }
catch (NoSuchFieldException e) { } cls = cls.getSuperclass(); }
return ret; }
private
void addChildren(List<ITreeNode> children, ViewGroup viewGroup) {
for (
int i = 0; i < viewGroup.getChildCount(); i++) { final View child = viewGroup.getChildAt(i); children.add(
new SoloTreeNode(child,
this)); } } @Override
public String getType() {
return _view.getClass().toString(); } @Override
public String getText() {
return getProperty(
"mText").getValue(); } }
cc.iqa.iquery.android.SoloProperty.java:
package cc.iqa.iquery.android; import cc.iqa.iquery.*;
public
class SoloProperty implements IProperty {
public SoloProperty(String name, String
value) {
this.name = name;
this.
value =
value; }
private String name;
public String getName() {
return name; }
private String
value;
public String getValue() {
return
value; } @Override
public boolean equals(Object obj) {
if (obj ==
null) {
return
false; }
if (getClass() != obj.getClass()) {
return
false; } final IProperty other = (IProperty) obj;
if (
this.name != other.getName() && (
this.name ==
null || !
this.name.equals(other.getName()))) {
return
false; }
return !(
this.
value != other.getValue() && (
this.
value ==
null || !
this.
value .equals(other.getValue()))); } @Override
public
int hashCode() {
int hash = 5; hash = 61 * hash + (
this.name !=
null ?
this.name.hashCode() : 0); hash = 61 * hash + (
this.
value !=
null ?
this.
value.hashCode() : 0);
return hash; } } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
本文由 知平软件 施懿民编写,请关注我们的 微博。