waveさんの技術日誌

wave1008の日記の新館です。

Shiratesを使ってみよう - セレクター式 -

Shiratesセレクター式を紹介します。

セレクター式

Shiratesのセレクター式は画面要素をフィルターするための式です。 select関数セレクター式を引数にとり、解析を行い、画面要素を取り出してフィルターし、結果として取得された要素を返します。 セレクター式は1つ以上のフィルター式から成ります。

フィルター式

フィルター フォーマルな書式 省略形 Androidの属性 iOSの属性
text text=text1 text1 text label
textStartsWith textStartsWith=text1 text1* text label
textContains textContains=text1 *text1* text label
textEndsWith textEndsWith=text1 *text1 text label
textMatches textMatches=^text$ n/a text label
literal literal=literal1 'literal1' text label
id id=id1 #id1 resource-id name
access access=access1 @access1 content-desc name
accessStartsWith accessStartsWith=access1 @access1* content-desc name
accessContains accessContains=access1 @*access1* content-desc name
accessEndsWith accessEndsWith=access1 @*access1 content-desc name
accessMatches accessMatches=^access1$ n/a content-desc name
value value=value1 n/a text value
valueStartsWith valueStartsWith=value1 n/a text value
valueContains valueContains=value1 n/a text value
valueEndsWith valueEndsWith=value1 n/a text value
valueMatches valueMatches=^value1$ n/a text value
class class=class1 .class1 class type
focusable focusable=true n/a focusable n/a
scrollable scrollable=true n/a scrollable n/a
visible visible=true n/a n/a visible
xpath xpath=//*[@text='text1'] n/a (arbitrary) (arbitrary)
pos pos=2 [2] n/a n/a
ignoreTypes ignoreTypes=Class1,Class2 n/a class type
image image=image1.png image1.png n/a n/a

フィルターにおける複数値の指定

フィルターは複数の値を (value1|value2)のように丸括弧と"|"(or)演算子で受け取ることができます。

it.select("text=(text1|text2)") // フォーマルな書式

it.select("(text1|text2)")  // 省略形の書式


以下のセレクター式はシンプルですが上記と等価です。

it.select("text1||text2")   // 等価なセレクター式


もっと複雑な状況では"|"が必要になるかもしれません。

it.select("(text1|text2)&&.class1||(@access1|@access2)&&.class2)")

セレクター式の規則

フィルター式は単独でセレクター式として使用できます。

text1


フィルター式は"&&"(AND)演算子で結合することができます。

text1&&.class1&&visible=true


フィルター式は"||"(OR)演算子で結合することができます。

text1||text2||@access1


"&&"は"||"よりも優先されます。

text1&&.class1||@access1

上記は(text=text1 and class=class1) or access=access1の意味となります。 ただし、以下のように丸括弧を使用することはできません(サポートしていません)。

悪い例

(text1&&.class1)||@access1

完全修飾ID

Androidresource-idはパッケージ名に対応したプレフィックスを持っています。例えば設定アプリは"com.android.settings がパッケージ名です。

完全修飾形 省略形
com.android.settings:id/search_bar search_bar

Shiratesでは完全修飾形への自動変換が行われ、かつ可読性が高いことから省略形の使用が推奨されます。 自動変換ではうまくいかない場合は完全修飾形を使用します。

セレクター式のためのプラットフォームアノテーション

プラットフォームアノテーション(@a, @i)を使用するとAndroid用とiOS用のセレクター式を1行で記述することができます。

@a<.android.widget.ImageButton>,@i<.XCUIElementTypeButton>
it.select("@a<.android.widget.ImageButton>,@i<.XCUIElementTypeButton>")

この記述を使用してニックネームを定義することができます。

"[Button1]": "@a<.android.widget.ImageButton>,@i<.XCUIElementTypeButton>"

否定フィルター

複雑な状況においては否定フィルターを使用することができます。

text=text1      // 通常のフィルター
text!=text1     // 否定フィルター

text1       // 通常のフィルター
!text1      // 否定フィルター

accessContains=text1    // 通常のフィルター
accessContains!=text1   // 否定フィルター

@*text1*    // 通常のフィルター
!@*text1*   // 否定フィルター

画像フィルター

画像ファイルを使用して画像マッチング(テンプレートマッチング)を行うことができます。 画像による検証

image=image1.png    // フォーマルな形式
image1.png          // 省略形
image1.png?scale=0.5&threshold=20   // オプションあり(フォーマルな形式)
image1.png?s=0.5&t=20   // オプションあり(省略形)

サンプルコード

サンプルコードの入手

本記事で説明するサンプルコードの完成版はこちらから入手してください。 https://github.com/wave1008/shirates-samples-selectors

SelectorTest2

import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import shirates.core.driver.branchextension.emulator
import shirates.core.driver.branchextension.realDevice
import shirates.core.driver.commandextension.*
import shirates.core.testcode.UITest

class SelectTest2 : UITest() {

    @Test
    @Order(10)
    fun selectByText() {

        scenario {
            case(1) {
                condition {
                    it.restartApp()
                }.action {
                    it.select("ネットワークとインターネット")
                }.expectation {
                    it.textIs("ネットワークとインターネット")
                }
            }
            case(2) {
                action {
                    it.select("ネットワークと*")
                }.expectation {
                    it.textIs("ネットワークとインターネット")
                }
            }
            case(3) {
                action {
                    it.select("*とインターネット")
                }.expectation {
                    it.textIs("ネットワークとインターネット")
                }
            }
            case(4) {
                action {
                    it.select("textMatches=^ネットワークとインターネット$")
                }.expectation {
                    it.textIs("ネットワークとインターネット")
                }
            }
            case(5) {
                action {
                    it.selectWithScrollDown("デバイス情報||エミュレートされたデバイスについて")
                }.expectation {
                    realDevice {
                        it.textIs("デバイス情報")
                    }.emulator {
                        it.textIs("エミュレートされたデバイスについて")
                    }
                }
            }
        }
    }

    @Test
    @Order(20)
    fun selectById() {

        scenario {
            case(1) {
                condition {
                    it.restartApp()
                }.action {
                    it.select("#search_action_bar_title")
                }.expectation {
                    it.textIs("設定を検索")
                }
            }
        }
    }

    @Test
    @Order(30)
    fun selectByAccessibility() {

        scenario {
            case(1) {
                condition {
                    it.restartApp()
                        .tap("ネットワークとインターネット")
                }.action {
                    it.select("@ネットワークとインターネット")
                }.expectation {
                    it.idIs("collapsing_toolbar")
                }
            }
        }
    }

    @Test
    @Order(40)
    fun selectByClass() {

        scenario {
            case(1) {
                condition {
                    it.restartApp()
                }.action {
                    it.select(".android.widget.ImageButton")
                }.expectation {
                    it.classIs("android.widget.ImageButton")
                }
            }
        }
    }

    @Test
    @Order(50)
    fun selectByXpath() {

        scenario {
            case(1) {
                condition {
                    it.restartApp()
                }.action {
                    it.select("xpath=//*[@text='設定を検索']")
                }.expectation {
                    it.textIs("設定を検索")
                }
            }
        }
    }

    @Test
    @Order(60)
    fun selectByPos() {

        scenario {
            case(1) {
                condition {
                    it.restartApp()
                        .tap("バッテリー")
                }.action {
                    it.select("*バッテリー*&&[1]")
                }.expectation {
                    it.textIs("バッテリー使用量")
                }
            }
            case(2) {
                action {
                    it.select("*バッテリー*&&[2]")
                }.expectation {
                    it.textIs("バッテリーセーバー")
                }
            }
            case(3) {
                action {
                    it.select("*バッテリー*&&[3]")
                }.expectation {
                    it.textIs("バッテリーを長持ちさせ、充電を最適化します")
                }
            }
        }
    }

}

サンプルコードの実行結果