開源項目文檔 - 擴展iQuery

在前文開源類庫iQuery Android版使用說明類jQuery selector的控件查詢iQuery開源類庫介紹中,介紹了iQuery的基本用法。css

iQuery是一個開源的自動化測試框架項目,有興趣的朋友能夠在這裏下載:html

https://github.com/vowei/iQuery/downloads java

源碼位置:
https://github.com/vowei/iQuery node

iQuery的一個主要目標就是提供一個跨平臺的控件查詢機制,那就須要考慮以下幾個平臺差別性:android

  • 編程語言的差別,例如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」

    這樣的查詢語句找到界面上全部的ExpandableListView,而在iOS上可使用
    「>> UIAScrollView」

    而針對同一控件在各平臺名字不一樣的狀況,iQuery的作法是提供一個僞類的概念,這個概念也是借自jQuery,例以下面的僞類就統一表示了各平臺下能夠看成單選框按鈕的控件:
    「:radio」

    然而,在實現iQuery的時候,咱們並不能假設:radio在Android上就是RadioButton控件,在iOS上就應該是UIASwitch,在Windows Phone上就是RadioBox控件,所以咱們決定將定義僞類的控制權交給開發者,這是iQuery的第二個擴展點。

  • 控件屬性的差別性,例如同是按鈕控件,在iOS上就是UIAButton.name(),而在Android上卻又是mText屬性,甚至有些屬性在不一樣的平臺上,有的存在,有的不存在,好比說Android上有mBottom屬性,在iOS上就不存在。

    跟解決控件的差別性方法相似,iQuery除了提供按屬性名和方法名讀取屬性值之外,例如
    iOS上的
    「:button [name = ‘肯定’]」

    Android上的
    「:button [mText = ‘肯定’]」

    爲了統一表達控件在各平臺通用的屬性,iQuery還提供了僞屬性的概念,好比上面的例子可使用下面的查詢完成:
    「:button [:text = ‘肯定’]」

    跟僞類同樣,僞屬性也是能夠由開發者自定義,這是iQuery的第三個擴展點。
  • UI自動化框架的差別,例如 iOS UI自動化測試的框架必然和Andorid UI自動化測試框架不一樣,但不管如何不一樣,當今大部分操做系統的圖形界面都有下面的性質:
    • 界面由控件樹組成。
    • 各控件有屬性的概念。

爲了儘量的支持更多的框架,iQuery將上面的性質封裝成兩個接口,所以對新平臺的支持,只要實現這兩個接口就能夠了git

本文介紹擴展僞類、僞屬性和添加對新平臺支持的方法,後續文章會解釋支持其餘編程語言的作法。github

擴展僞類

在Java版本中,在iQA.Runtime.jar包裏,能夠經過iQueryParser. registerPseudoClass這個函數註冊一個新的僞類,步驟以下:
編程

  • 使用
iQueryParser. createParser(String iquery, boolean registerPseudo)
  • 建立一個iQueryParser實例。
    再使用iQueryParser. registerPseudoClass(String name, IPseudoClass func)註冊一個新的僞類,例以下面的代碼,註冊一個名爲text的僞類,過濾方式爲全部類型名以EditText結尾的控件:
parser.registerPseudoClass("text", new IPseudoClass() {
        public boolean resolve(ITreeNode node) {
            return filterByNameEndsWith(node, "EditText");
        }
        });

在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版本中的作法是:

  1. 引入如下幾個JavaScript文件:
    #import "common.js";
    #import "antlr3-all-min.js";
    #import "iQueryLexer.js";
    #import "iQueryParser.js";
    #import "error.js";
  2. 建立一個iQuery實例:
    var iq = new iQuery(selector);
  3. 註冊僞屬性:
    iq.parser.registerPseudoAttrs("bottom", function(uiaobj) {
    if ( uiaobj != undefined && uiaobj.rect != undefined ) {
        var rect = uiaobj.rect();
        return rect.origin.y + rect.size.height;
    }
    });

添加對新平臺的支持

當前支持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;
    }
}

本文由 知平軟件 施懿民編寫,請關注咱們的 微博
相關文章
相關標籤/搜索