0. はじめに
この記事は、Xamarin その1 Advent Calendar 2017 - Qiita の 23日目のエントリーです。12月23日は、私の誕生日でして、昨年に引き続き今年も23日目を担当させていただき、嬉しく思います。よろしくお願いいたします。
本記事は、Xamarin.UITest における Tips 集です。Xamarin で提供される個別のコントロールに対するテストコード方法について調査・検証した結果をまとめています。
1. Xamarin UITest の基本
AutomationId
属性
UI テストでは、ある特定のコントロールに対して操作を行う必要があります。Xamarin.Forms のコントロールには、UI テスト時にコントロールを識別するための仕組みとして、AutomationId
属性が提供されています。UI テストで操作する必要のあるコントロールには、AutomationId
属性を付与するようにしましょう。以下に Button
コントロールに AutomationId を付与した XAML の例を記載します。
<Button AutomationId="ButtonDemoPage.Button" Command="{Binding ClickCommand}" Text="Click"/>
この AutomadionId
は、各プラットフォームで異なるフィールドに割り当てられます。
Platform | フィールド名 |
---|---|
iOS | id |
Android | label |
Repl (read-eval-print-loop) を利用して、各プラットフォームの UI ツリーを確認すると上の表のように AutomationId
が割り当てられている様子がわかります。
- iOS の場合
- Android の場合
IApp
インターフェース
アプリケーションに対する操作(タップ、スワイプなどの操作)は、IApp
インターフェースが提供するメソッド経由で行います。以下に主なメソッドを記載します。
メソッド | 概要 |
---|---|
Query | 指定した条件に該当する UI 要素を検索する |
Tap | UI 要素をタップする |
Screenshot | スクリーンショットを撮影する |
EnterText | テキストを入力する |
ScrollUp | 上にスクロールする |
ScrollDown | 下にスクロールする |
Flash | 当該 UI 要素を点滅させる |
先ほどの Button
コントロールをタップしたい場合は、Xamarin.UITest では以下のように記述します。
app.Tap(x => x.Marked("ButtonDemoPage.Button"));
AppQuery
クラス
名前から想像できるように、アプリケーションの UI ツリーに対するクエリを組み立てるためのメソッドを提供するクラスです。
メソッド | 概要 |
---|---|
Class | 引数に指定した型にマッチした UI コントロールを取得する |
Marked | 引数で指定した識別子やテキスト値にマッチするコントロールを取得する |
Child | コントロールの子供を取得する |
Id | 指定した Id に該当するコントロールを取得する |
Index | UI コントロール配列から当該インデックスの要素を取得する |
Invoke | ネイティブコントロールのメソッド・プロパティを呼び出す |
先ほどの Button
コントロールをタップする例 app.Tap(x => x.Marked("ButtonDemoPage.Button"))
の意味は、"ButtonDemoPage.Button" という識別子で Mark されたコントロールを取得して、タップするという意味になります。
2. Xamarin.UITest Tips for Xamarin.Forms Controls
ここからは、Xamairn.Forms で提供される各コントロールの Xamarin.UITest の Tips になります。
Label
ラベルの表示が正しいかを Xamarin.UITest で確認をする方法を記載します。XAML 上で以下のように Label
が定義されているとします。
<Label AutomationId="LabelDemoPage.Label" Text="Welcome to Xamarin.Forms!"/>
Label
コントロールは、iOS では UILabel
クラス、Android では TextView
クラスのオブジェクトになります。このとき、Xamarin UI Test で Label を取得するには、以下のようにコードを記載します。
app.Query(x => x.Marked("Welcome to Xamarin.Forms!"));
Image
Image
コントロールを Xamairn.UITest で取得する方法について記載します。XAML 上に以下のように Image
コントロールが定義されているものとします。
<Image AutomationId="ImageDemoPage.Image"
Source="https://www.xamarin.com/content/images/pages/branding/assets/xamagon.png"/>
以下のコードに記載するように、AutomationId
を利用して、Image
コントロールを取得することができます。
app.Query(x => x.Marked("ImageDemoPage.Image"));
BoxView
ここでは、BoxView コントロールをタップする例を記載します。以下のように XAML 上で BoxView が定義されているとします。
<BoxView x:Name="MyBoxView" AutomationId="FormsGallery.BoxViewDemoPage.BoxView"
Color="Blue" WidthRequest="150" HeightRequest="150">
<BoxView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding TapCommand}"/>
</BoxView.GestureRecognizers>
</BoxView>
この BoxView を Xamarin.UITest でタップするには、以下のようにコードを記述します。
app.Tap(x => x.Marked("FormsGallery.BoxViewDemoPage.BoxView"));
Button
ボタンをタップするシナリオを Xamarin.UITest で実現する方法を記載します。なお、XAML 上では以下のようにボタンが実装されているとします。
<Button AutomationId="ButtonDemoPage.Button"
Command="{Binding ClickCommand}"
Text="Click"/>
Xamarin.UITest でボタンをタップするには IApp.Tap
メソッドを利用します。
app.Tap(x => x.Marked("ButtonDemoPage.Button"));
SearchBar
SearchBar で検索条件の入力・検索を Xamarin.UITest で実現する方法を記載します。なお、XAML 上では、以下のように SearchBar が実装されているとします。
<SearchBar AutomationId="SearchBar"
x:Name="SampleSearchBar"
Text="{Binding SearchWord}"
SearchCommand="{Binding SearchCommand}"
SearchCommandParameter="{Binding SearchWord}"/>
検索条件を入力し、検索処理を実行するには、IApp.EnterText
メソッドで条件を入力後、 IApp.PressEnter
メソッドを利用します。
app.EnterText(x => x.Marked("SearchBar"), "Title");
app.PressEnter();
検索条件をクリアするには、IApp.CelarText
メソッドを実行後、 IApp.DissmissKeyboard
メソッドでキーボードを消します。
app.ClearText(x => x.Marked(mark));
app.DismissKeyboard();
検索処理をキャンセルする場合は、Android と iOS で異なる処理を記述する必要があります。
// SearchBar のキャンセルボタンを押す
if (platform == Platform.Android)
{
app.Tap(x => x.Id("search_close_btn"));
}
else if (platform == Platform.iOS)
{
app.Tap(x => x.Marked("Cancel"));
}
app.DismissKeyboard();
Slider
Slider の値を設定・取得するシナリオを Xamarin.UITest で実現する方法について記載します。以下のように XAML 上に Slider が定義されているとします。
<Slider AutomationId="Slider"
Minimum="0"
Maximum="100"
Value="{Binding Value}"/>
- 値を設定するには、
IApp.SetSliderValue
メソッドを利用します - 値を取得するには、ネイティブコントロールのメソッドを
AppQuery.Invoke
で呼び出します- Android
-
getProgress
メソッド
-
- iOS
-
UISlider
クラスのvalue
プロパティ
-
- Android
string marked = "SliderDemoPage.Slider";
if(platform == Platform.Android)
{
var progress = 500.0;
app.SetSliderValue(marked, progress);
var actual = Convert.ToDouble(app.Query(x => x.Marked(marked).Invoke("getProgress"))[0]);
}
else if(platform == Platform.iOS)
{
var progress = 50;
app.SetSliderValue(marked, progress);
var actual = Convert.ToInt32(app.Query(x => x.Class("UISlider").Invoke("value"))[0]);
}
Stepper
Stepper の値の増減を Xamarin.UITest で実現する方法を記載します。XAML 上に以下のように Stepper
に定義されているものとします。
<Stepper Minimum="0" Maximum="100" Value="{Binding Value}"/>
AutomationId
を利用して Stepper
の値を増減させたいところですが、AutomationId
を利用して Stepper
の増減ボタンを押すことができません。理由は、AutomationId
が、Android では、LinearLayout
に付与され、iOS では UIStepper
に付与されており、値を増減させるボタンに付与されていないためです。
-
Stepper
を Repl の tree コマンド表示した図(Android)
-
Stepper
を Repl の tree コマンド表示した図(iOS)
従って、各プラットフォームでいかに説明するように Xamarin.UITest のコードを記述する必要があります。
Android の場合
Android の場合は、Stepper
のボタンは、ボタンのテキストが、"+" または "-" となるため、当該のボタンをタップするコードを記述します。
app.Tap(x => x.Class("android.widget.Button").Text("+")); // increment
app.Tap(x => x.Class("android.widget.Button").Text("-")); // decrement
iOS の場合
iOS の場合は、Stepper
のボタンは、ボタンのテキストが、"Increment" または "Decrement" なので、当該のボタンをタップするコードを記述します。
app.Tap(x => x.Marked("Increment")); // increment
app.Tap(x => x.Marked("Decrement")); // decrement
Switch
Switch
の ON / OFF を切り替える操作と Switch
の ON / OFF 状態を取得するシナリオを、Xamarin.UITest で実現する方法について記載します。以下のように XAML 上で Switch
が定義されているとします。
<Switch AutomationId="SwitchDemoPage.Switch" IsToggled="{Binding Value}"/>
Switch
の ON / OFF を切り替えるには、IApp.Tap
メソッドで AutomationId
を指定して、Switch
をタップします。Switch
の ON / OFF の状態を取得するには、ネイティブのメソッド・フィールドを AppQuery.Invoke
で呼び出して取得する必要があります。
以下に各プラットフォームで利用すべきネイティブのメソッド・フィールドについて記載します。
プラットフォーム | ネイティブコントロール | メソッド・フィールド | 補足 |
---|---|---|---|
Android | Android.Widget.Switch |
isChecked メソッドを利用する |
|
iOS | UISwitch |
isOn フィールドの値を取得する |
ON の時には 1、OFF の時には 0 が返される |
Xamarin.UITest のコードは、以下のようになります。
string marked = "SwitchDemoPage.Switch";
app.Tap(x => x.Marked(marked)); // Tap Switch control
if (platform == Platform.Android)
{
var result = (bool)app.Query(x => x.Marked(marked).Invoke("isChecked"))[0];
}
else if(platform == Platform.iOS)
{
// If switch is ON, get 1.
// If switch is OFF, get 0.
var result = Convert.ToInt32(app.Query(x => x.Marked(marked).Invoke("isOn")));
}
DatePicker
DatePicker
で日付を選択するシナリオを Xamarin.UITest で実現する方法について記載します。XAML 上で以下のように DatePicker
が定義されているものとします。
<DatePicker AutomationId="DatePickerDemoPage.DatePicker" Format="D"/>
DatePicker
での日付の選択を Xamarin.UITest で実現するには、以下のステップで処理を実装する必要があります。
-
DatePicker
をタップし、日付選択の Picker を起動する-
IApp.Tap
メソッドにAutomationId
を指定してタップ処理を実行します -
DatePicker
が表示されるまで待ちます
-
- 日付を変更する
- Android の場合
-
DatePicker
はAndroid.Widget.DatePicker
のオブジェクトに変換されるので、AppQuery.Invoke
メソッドを利用してAndroid.Widget.DatePicker
クラスのupdateDate
メソッドを呼び出す
-
- iOS の場合
-
UIPickerTableView
の年、月、日に該当するそれぞれの列に対して、以下の操作を行う必要があります-
IApp.ScrollDownTo
/IApp.ScrollUpTo
メソッドを利用してスクロールさせる - スクロール後、当該列のテキストをタップして、値を確定する
-
-
- Android の場合
- Picker を閉じる
- Android の場合
-
Android.Widget.DatePicker
の OK ボタンは、"button1" という Id が付与されているため、Id から OK ボタンのコントロールを取得して、IApp.Tap
メソッドを実行する
-
- iOS の場合
- "Done" とマークされたボタンをタップします
- Android の場合
Xamarin.UITest のコードは以下のようになります。テスト対象のデバイスの言語は en-US を想定しています。
var date = new DateTime(2018, 12, 23);
CultureInfo enus = new CultureInfo("en-US");
var month = enus.DateTimeFormat.GetMonthName(date.Month);
var expected = date.ToString("D", enus);
var actual = string.Empty;
var mark = "DatePickerDemoPage.DatePicker";
if (platform == Platform.Android)
{
app.Tap(x => x.Marked(mark));
app.WaitForElement(x => x.Class("DatePicker"));
app.Query(x => x.Class("DatePicker").Invoke("updateDate", date.Year, date.Month -1, date.Day));
app.Tap(x => x.Id("button1")); //Ok Button in DatePicker Dialogue
}
else if(platform == Platform.iOS)
{
app.Tap(x => x.Id(mark));
app.WaitForElement(x => x.Class("UIPickerView"));
// Scroll DatePicker items
var iOSTableViewClass = "UIPickerTableView";
app.ScrollDownTo(z => z.Marked(month), x => x.Class(iOSTableViewClass).Index(0), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Text(month));
app.ScrollDownTo(z => z.Marked(date.Day.ToString()), x => x.Class(iOSTableViewClass).Index(3), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Text(date.Day.ToString()));
app.ScrollDownTo(z => z.Marked(date.Year.ToString()), x => x.Class(iOSTableViewClass).Index(6), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Text(date.Year.ToString()));
app.Tap(x => x.Marked("Done")); // Tap button marked with "Done".
}
iOS の日付の変更ですが、UIDatePicker
の selectRow()
メソッドで値の設定はできるのですが、"Done" ボタンをタップした後に、DatePicker
の日付の表示が変更できませんでした。いろいろと試行錯誤をした結果、ここに記載している方法にたどり着きました。selectRow()
メソッドを利用した方法でも日付の変更ができるよという情報があれば、教えていただけると幸いです。
TimePicker
TimePicker
で時刻を選択するシナリオを、Xamarin.UITest で実現する方法について記載します。XAML 上で以下のように TimePicker
が定義されているものとします。
<TimePicker AutomationId="TimePickerDemoPage.TimePicker" Format="T"/>
TimePicker
での時刻の選択するシナリオの流れは、DatePicker
の流れとほぼ同じです。
-
TimePicker
をタップし、日付選択の Picker を起動する-
IApp.Tap
メソッドにAutomationId
を指定してタップ処理を実行する -
TimePicker
が表示されるまで待ちます
-
- 時刻を変更する
- Android の場合
-
TimePicker
は "timePicker" という Id が付与されたAndroid.Widget.TimePicker
のオブジェクトに変換されるので、AppQuery.Invoke
メソッドを利用してAndroid.Widget.TimePicker
クラスのsetHour
メソッド、setMinute
を呼び出す
-
- iOS の場合
-
UIPickerTableView
の時間、分、AM/PM に該当するそれぞれの列に対して、以下の操作を行う必要がある-
IApp.ScrollDownTo
/IApp.ScrollUpTo
メソッドを利用してスクロールさせる - スクロール後、当該列のテキストをタップして、値を確定する
-
-
- Android の場合
- Picker を閉じる
- Android の場合
-
Android.Widget.TimePicker
の OK ボタンは、"button1" という Id が付与されているので、`Id から OK ボタンのコントロールを取得して、IApp.Tap
メソッドを実行する
-
- iOS の場合
- "Done" とマークされたボタンをタップする
- Android の場合
var hour = 11;
var minutes = 59;
var meridian = "PM";
var mark = "TimePickerDemoPage.TimePicker";
if (platform == Platform.Android)
{
app.Tap(x => x.Marked(mark));
app.WaitForElement(x => x.Id("timePicker"));
app.Query(x => x.Id("timePicker").Invoke("setHour", hour + 12));
app.Query(x => x.Id("timePicker").Invoke("setMinute", minutes));
app.Tap(x => x.Id("button1")); //Ok Button in DatePicker Dialogue
}
else if(platform == Platform.iOS)
{
app.Tap(x => x.Id(mark));
app.WaitForElement(x => x.Class("UIPickerView"));
app.ScrollDownTo(z => z.Marked(hour.ToString()), x => x.Class(iOSTableViewClass).Index(0), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Text(hour.ToString()));
app.ScrollDownTo(z => z.Marked(minutes.ToString()), x => x.Class(iOSTableViewClass).Index(3), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Text(minutes.ToString()));
app.ScrollDownTo(z => z.Marked(meridian), x => x.Class(iOSTableViewClass).Index(6), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Text(meridian));
app.Tap(x => x.Marked("Done")); // Tap "Done" button.
}
Entry
Entry
のテキスト入力・クリアを行うシナリオを、Xamarin.UITest で実現する方法について記載します。以下のように Entry
が XAML 上に定義されているとします。
<Entry AutomationId="EntryDemoPage.MailEntry" Placeholder="Enter email address"/>
テキストの入力には IApp.EnterText
メソッドを利用し、テキストのクリアには IApp.CelarText
メソッドを利用します。
var mailId = "EntryDemoPage.MailEntry";
//EnterText
app.EnterText(x => x.Marked(mailId), "someone@somewhere.local");
app.DismissKeyboard();
//ClearText
app.ClearText(x => x.Marked(mailId));
app.DismissKeyboard();
Editor
Editor
の UI テストは、Entry
と同様の方法で実装できます。以下のような XAML が定義されているとします。
<Editor AutomationId="EditorDemoPage.Editor"/>
テキストの入力・クリアは以下のように実装することができます。
var marked = "EditorDemoPage.Editor";
//EnterText
app.EnterText(x => x.Marked(marked), "very long text");
app.DismissKeyboard();
//ClearText
app.ClearText(x => x.Marked(marked));
app.DismissKeyboard();
ActivityIndicator
以下の二つのシナリオを Xamarin.UITest で実現する方法について記載します。
-
ActivityIndicator
が表示・非表示になるまで待つ - インジケーターの表示・非表示を切り替える
なお、ActivityIndicator
は XAML で以下のように定義されているものとします。
<ActivityIndicator AutomationId="ActivityIndicator" IsRunning="True"/>
ActivityIndicator
が表示・非表示になるまで待つ
特定の要素の表示・非表示を待つので、WaitForElement
又は WaitForNoElement
メソッドを利用します。以下にサンプルを示します。
// ActivityIndicator が表示されるまで待つ
app.WaitForElement(x => x.Marked("ActivityIndicator"), timeout: TimeSpan.FromSeconds(30));
// ActivityIndicator が非表示になるまで待つ
app.WaitForNoElement(x => x.Marked("ActivityIndicator"), timeout: TimeSpan.FromSeconds(30));
インジケーターの表示・非表示を切り替える
インジケーターの表示・非表示を切り替えるには、ActivityIndicator
が、ネイティブではどのようにレンダリングされているかを知っておく必要があります。以下に Android, iOS それぞれのプラットフォームについて、ActivityIndicator
がレンダリングされるコントロール名、インジケーターの表示・非表示を切り替えるメソッドについて記載します。
プラットフォーム | ネイティブコントロール | 表示・非表示に関連するネイティブメソッド |
---|---|---|
Android | Android.Widget.ProgressBar |
setVisibility メソッド |
iOS | UIActivityIndicatorView |
stopAnimating メソッド |
従って、Xamarin UITest でインジケーターの表示・非表示を切り替えるには、上記のネイティブメソッドを AppQuery.Invoke
を利用して呼び出します。
if(platform == Platform.Android)
{
app.Query(x => x.Marked(mark).Invoke("setVisibility", 8));
app.WaitForNoElement(x => x.Marked(mark), timeout: TimeSpan.FromSeconds(30));
}
else if(platform == Platform.iOS)
{
app.Query(x => x.Marked(mark).Invoke("stopAnimating"));
app.WaitForNoElement(x => x.Marked(mark), timeout: TimeSpan.FromSeconds(30));
}
ProgressBar
ProgressBar の進捗状況を設定・取得するシナリオを Xamarin UITest で実現する方法について記載します。以下のように ProgressBar が XAML 上に定義されているとします。
<ProgressBar AutomationId="ProgressBarDemoPage.ProgressBar"
VerticalOptions="CenterAndExpand"/>
ProgressBar の進捗状況の取得・設定を行うためには、ProgressBar がネイティブコントロールに対して、AppQuery.Invoke
を実行する必要があります。以下に各プラットフォームにおけるコントロール、進捗状況を取得・設定するメソッド・プロパティの対応関係を示します。
プラットフォーム | ネイティブコントロール | メソッド・プロパティ | 補足 |
---|---|---|---|
Android | Android.Widget.ProgressBar |
setProgress / getProgress メソッド を利用する |
0 ~ 10,000の値をとり、10,000 が 100% に対応する |
iOS | UIProgressView |
progress プロパティを利用する |
0.0 ~ 1.0 の値をとり、1.0f が 100% に対応する |
具体的なコードを見ていきます。
if (platform == Platform.Android)
{
// 進捗を 25% に設定する
app.Query(x => x.Class("ProgressBar").Invoke("setProgress", 2500));
// 進捗を取得する
long progress = (long)app.Query(x => x.Class("ProgressBar").Invoke("getProgress"))[0];
}
else if (platform == Platform.iOS)
{
// 進捗を 50% に設定する
app.Query(x => x.Class("UIProgressView").Invoke("setProgress:animated", 0.5));
// 進捗を取得する
var progress = app.Query(x => x.Class("UIProgressView").Invoke("progress"))[0];
// Convert をかけているのは、long 型でデータが返される場合があるため(値が 0 の時など)
double value = System.Convert.ToDouble(progress);
}
iOS で進捗を取得するときに、long 型でデータが返されることがあるため、Convert
クラスを利用して値を変換するなどの処理を行っておく必要があります。
Picker
Picker の項目を選択するシナリオを Xamarin.UITest で実現する方法について記載します。以下のように Picker が XAML 上に定義されているとします。
<Picker AutomationId="PickerDemoPage.Picker"
Title="Color"
ItemsSource="{Binding PickerItems}"
SelectedItem="{Binding SelectedColor}"/>
UI Test では以下の 3つのステップを実現する必要があります。
- Picker をタップして選択項目のリストを表示する
- AutomationId を付与したコントロールを
IApp.Tap
メソッドでタップする
- AutomationId を付与したコントロールを
- 選択対象となる項目が表示されるまで Picker をスクロールさせる
-
IApp.ScrollDownTo
又はIApp.ScrollUpTo
メソッドを利用して、タップ対象の項目が表示されるまでスクロールさせる
-
- 選択対象の項目をタップする
- 対象の項目を
IApp.Tap
メソッドを利用してタップする
- 対象の項目を
- iOS の場合は、最後に "Done" ボタンをタップする
// Picker をタップする
var marked = "PickerDemoPage.Picker";
app.Tap(x => x.Marked(marked));
if (platform == Platform.Android)
{
app.ScrollDownTo(z => z.Marked("Yellow"),
x => x.Id("select_dialog_listview"),
timeout: TimeSpan.FromSeconds(30),
strategy: ScrollStrategy.Auto);
app.Tap(x => x.Marked("Yellow"));
}
else if(platform == Platform.iOS)
{
app.ScrollUpTo(z => z.Marked("Aqua"),
x => x.Class("UIPickerTableView").Index(0),
timeout: DefaultTimeout,
strategy: ScrollStrategy.Auto);
app.Tap(x => x.Marked("Aqua"));
app.Tap(x => x.Marked("Done")); // Done をタップする
}
ListView
ListView
の項目を選択するシナリオを Xamarin.UITest で実現する方法について記載します。XAML 上で以下のように ListView
が定義されているものとします。DataTemplate
内のコントロールで、UI Test で取得するコントロールには、AutomationId
を付与するようにしましょう。
<ListView AutomationId="ListViewDemoPage.ListView"
ItemsSource="{Binding People}"
IsPullToRefreshEnabled="True"
RefreshCommand="{Binding RefleshCommand}"
IsRefreshing="{Binding IsRefleshing, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell AutomationId="ListViewDemoPage.ViewCell">
<StackLayout Orientation="Horizontal" Margin="1,1">
<BoxView Color="{Binding FavoriteColor}"/>
<StackLayout Orientation="Vertical">
<Label AutomationId="ListViewDemoPage.NameLabel"
Text="{Binding Name}"/>
<Label AutomationId="ListViewDemoPage.BirthDayLabel"
Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}"/>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
- 画面上に表示されているアイテムは、
IApp.Query
メソッドを利用して、AutomationId
から取得することができます - 画面上に表示されていないアイテム(リストの最後尾のアイテムなど)を取得したい場合は、
IApp.ScrollDownTo
等を利用して画面をスクロール後、アイテムの取得を行う必要があります
以下に Xamarin.UITest のサンプルコードを示します。
var mark = "ListViewDemoPage.ListView";
var nameLabelMark = "ListViewDemoPage.NameLabel";
app.ScrollDownTo(z => z.Marked(nameLabelMark).Text("Yvonne"), x => x.Marked(mark), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
var nameLabels = app.Query(x => x.Marked(nameLabelMark));
var birthdayLabels = app.Query(x => x.Marked("ListViewDemoPage.BirthDayLabel"));
Pull To Refresh が実装された ListView の UI Test
ListView
を引っ張って更新するシナリオを Xamarin.UITest で実現したい場合の補足説明を記載します。具体的には、以下の二つの操作を実装する方法について説明します。
-
ListView
を引っ張って更新する -
ListView
が更新中かどうかを判定する
ListView
を引っ張って更新するには、ListView
をドラッグすればよいことになります。ここでは、ListView
の最初のセルの中心座標から、ListView
の中心座標までをドラッグすることで実現します。
以下に Xamarin.UITest のサンプルを示します。
AppResult firstCellInList = null;
if(platform == Platform.Android)
{
firstCellInList = app.Query(x => x.Class("ViewCellRenderer_ViewCellContainer").Index(0)).FirstOrDefault();
}
else if (platform == Platform.iOS)
{
firstCellInList = app.Query(x => x.Class("Xamarin_Forms_Platform_iOS_ViewCellRenderer_ViewTableCell")).FirstOrDefault();
}
var firstCenterX = firstCellInList.Rect.CenterX;
var firstCenterY = firstCellInList.Rect.CenterY;
var listview = app.Query(x => x.Marked(marked))[0];
var listviewCenterX = listview.Rect.CenterX;
var listviewCenterY = listview.Rect.CenterY;
app.DragCoordinates(firstCenterX, firstCenterY, listviewCenterX, listviewCenterY);
ListView
が更新中かどうかを判定する方法は、Android、iOS それぞれのネイティブの知識が必要になります。
Platform | ネイティブコントロール | 判定方法 |
---|---|---|
Android | android.support.v4.widget.SwipeRefreshLayout |
isRefreshing メソッドを利用する |
iOS | UIRefreshControl | コントロールが存在するかどうかで判定する |
判定を行う処理のサンプルを以下に示します。
var isRefreshing = false;
if (platform == Platform.Android)
{
isRefreshing = (bool)app.Query(x => x.Class("SwipeRefreshLayout").Invoke("isRefreshing")).First();
}
else(platform == Platform.iOS)
{
isRefreshing = (bool)app.Query(x => x.Class("UIRefreshControl")).Any();
}
この判定処理を定期的にチェックすることで、ListView
が更新中かどうかを判定することができます。
以下のように判定処理をプロパティ化し、判定処理を一定時間リトライするメソッドを実装し、WaitForIndicatorToDisappear(3, 10)
のように待ちの処理を実装するとよいでしょう。
public bool RefreshIndicatorIsDisplayed
{
get
{
if (platform == Platform.Android)
return (bool)app.Query(x => x.Class("SwipeRefreshLayout").Invoke("isRefreshing")).First();
if (platform == Platform.iOS)
return (bool)app.Query(x => x.Class("UIRefreshControl")).Any();
throw new Exception("NotSupportedPlatform");
}
}
public void WaitForIndicatorToDisappear(int retryCount = 3, int waitSeconds = 10)
{
int counter = 0;
while (RefreshIndicatorIsDisplayed)
{
Thread.Sleep(waitSeconds * 1000);
counter++;
if (counter >= retryCount)
throw new Exception($"待ち時間 {waitSeconds * retryCount} をオーバーしました");
}
}
TableView
TableView
内のセルを操作するシナリオを、Xamarin.UITest で実現する方法について記載します。以下のように XAML 上に TableView
が定義されているものとします。
<TableView AutomationId="TableViewFormDemoPage.TableView" Intent="Form">
<TableSection Title="Table Section">
<TextCell AutomationId="TextCell" Detail="With Detail Text" Text="Text Cell"/>
<ImageCell AutomationId="ImageCell" Text="Image Cell"
ImageSource="https://www.xamarin.com/content/images/pages/branding/assets/xamagon.png"/>
<SwitchCell AutomationId="SwitchCell" Text="Switch Cell" IsEnabled="False"/>
<EntryCell AutomationId="EntryCell" Label="Entry Cell" Placeholder="Type text here"/>
<ViewCell AutomationId="ViewCell">
<StackLayout>
<Label AutomationId="ViewCell.Label" Text="A ViewCell can be anything you want!"/>
</StackLayout>
</ViewCell>
</TableSection>
</TableView>
TableView
コントロールの取得は、AutomationId
を指定して取得することができます。しかしながら、TableView
内のセルnには、XAML で AutomationId
を付与してもネイティブのオブジェクトツリーには反映されません。従って、TableView
のコントロールを取得した後に、Child
メソッドを駆使しながら、テスト対象のコントロールを取得することになります。
以下、Xamairn.Forms で提供されている標準のセルコントロールについて、Xamarin.UITest ではどのように実装したらよいかについて記載します。
TextCell
TextCell
を Repl で評価すると、各プラットフォームで以下のように変換されていることがわかります。
- Android の場合
[ConditionalFocusLayout]
[TextCellRenderer_TextCellView > LinearLayout]
[TextView] text: "Text Cell"
[TextView] text: "With Detail Text"
[View]
- iOS の場合
[Xamarin_Forms_Platform_iOS_CellTableViewCell] text: "Text Cell"
[UITableViewCellContentView]
[UITableViewLabel] label: "Text Cell", text: "Text Cell"
[UITableViewLabel] label: "With Detail Text", text: "With Detail Text"
[_UITableViewCellSeparatorView]
[_UITableViewCellSeparatorView]
[UITableTextAccessibilityElement] label: "Text Cell, With Detail Text"
TextCell
コントロールを取得するには、表示されている文字列を指定してコントロールを取得します。
// TextCell
var textCellText = app.Query(x => x.Marked("Text Cell"));
var textCellDetail = app.Query(x => x.Marked("With Detail Text"));
EntryCell
EntryCell
のテキストの入力・クリアを行うシナリオを、Xamarin.UITest で実現する方法について記載します。
Android の場合、EntryCell
を Repl で評価すると以下のようなツリー構造になっていることがわかります。
[ConditionalFocusLayout]
[EntryCellView]
[TextView] text: "Entry Cell"
[EntryCellEditText]
[View]
Android の場合、入力用のテキストボックスは、EntryCellView
クラスの子オブジェクトの EntryCellEditText
オブジェクトであることがわかります。従って、EntryCell
でテキストを入力するには以下の方針でテキストを入力する必要があります。
-
AppQuery.Class
メソッドでEntryCellView
クラスのオブジェクトを取得し、その子オブジェクトを取得する- 子オブジェクトを取得する方法は、以下の二通りの方法がある
-
AppQuery.Child
メソッドでインデックスを指定する -
AppQuery.Child
メソッドでクラス名(EntryCellEditText)を指定する
-
- 子オブジェクトを取得する方法は、以下の二通りの方法がある
- テキストの入力は
IApp.EnterText
メソッド、テキストのクリアはIApp.ClearText
メソッドを利用する
iOS の場合、EntryCell
を Repl で評価すると以下のようなツリー構造になっていることがわかります。
[Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell] text: "Entry Cell"
[UITableViewCellContentView]
[UITextField]
[UITextFieldLabel] label: "Type text here", text: "Type text here"
[_UITextFieldContentView > UITextSelectionView]
[UIAccessibilityTextFieldElement] text: "Type text here"
[UITableViewLabel] label: "Entry Cell", text: "Entry Cell"
[_UITableViewCellSeparatorView]
[UITableTextAccessibilityElement] label: "Entry Cell"
[UIAccessibilityElementMockView] text: "Type text here"
[UIAccessibilityTextFieldElement] text: "Type text here"
iOS の場合、入力用のテキストボックスは、Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell
クラスの孫(UITableViewCellContentView
クラスの子供)となっているので、Xamarin.UITest では、以下の方針でテキストの入力・クリアの処理を実装する必要があります。
-
AppQuery.Class
メソッドでXamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell
を取得し、その最初の子供を取得する- 子オブジェクトは、
Child
メソッドで取得する- インデックス指定か、クラス名(
UITableViewCellContentView
)を指定する
- インデックス指定か、クラス名(
- その子供の最初のコントロールを
Child
メソッドで取得する- インデックス指定か、クラス名
- 子オブジェクトは、
- テキストの入力は
IApp.EnterText
メソッド、テキストのクリアはIApp.ClearText
メソッドを利用する
以下に、サンプルコードを示します。
// Enter text.
var inputText = "This is text for test.";
if(platform == Platform.Android)
{
app.EnterText(x => x.Class("EntryCellView").Child(1), inputText);
app.EnterText(x => x.Class("EntryCellView").Child("EntryCellEditText"), inputText);
app.DismissKeyboard();
}
else if (platform == Platform.iOS)
{
app.EnterText(x => x.Class("Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell").Child(0).Child(0), inputText);
app.EnterText(x => x.Class("Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell").Child("UITableViewCellContentView").Child("UITextField"), inputText);
app.DismissKeyboard();
}
// Clear text.
if (platform == Platform.Android)
{
app.ClearText(x => x.Class("EntryCellView").Child(1));
}
else if (platform == Platform.iOS)
{
app.ClearText(x => x.Class("Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell").Child(0).Child(0));
}
SwitchCell
SwitchCell
の Switch
の ON / OFF を切り替えるシナリオを、Xamarin.UITest で実現する方法について記載します。
Android の場合、SwitchCell
を Repl で評価すると、次のようなツリー構造になっています。従って、SwitchCellView
クラスの子供の Switch
クラスのオブジェクトに対して、Invoke
メソッドを呼び出してやればよいことになります。
[ConditionalFocusLayout]
[SwitchCellView]
[LinearLayout]
[TextView] text: "Switch Cell"
[Switch]
[View]
iOS の場合も Repl で評価すると以下のようなツリー構造になっているため、Xamarin_Forms_Platform_iOS_CellTableViewCell
クラスの子供の UISwitch
クラスのオブジェクトに対して、Invoke
メソッドを使用して、ON / OFF を切り替えればよいことになります。
[Xamarin_Forms_Platform_iOS_CellTableViewCell] text: "Switch Cell"
[UITableViewCellContentView]
[UITableViewLabel] label: "Switch Cell", text: "Switch Cell"
[UISwitch > UISwitchModernVisualElement] label: "Switch Cell", text: "0"
[UIView > UIView]
[UIView > UIView]
[UIView > UIImageView]
[UIImageView]
[_UITableViewCellSeparatorView]
[UITableTextAccessibilityElement] label: "Switch Cell", text: "0"
[UIAccessibilityElementMockView] label: "Switch Cell", text: "0"
以下に、サンプルコードを記載します。
// SwitchCell
if (platform == Platform.Android)
{
app.Query(x => x.Class("SwitchCellView").Class("Switch").Invoke("setChecked", true));
}
else if (platform == Platform.iOS)
{
app.Query(x => x.Class("Xamarin_Forms_Platform_iOS_CellTableViewCell").Class("UISwitch").Invoke("setOn:animated", true));
}
ImageCell
ImageCell
に配置された Image を Xamarin.UITest で取得する場合を考えます。
Android の場合のオブジェクトツリーは以下のようになるため、TextCellRenderer_TextCellView
クラスの子供の ```ImageView`` オブジェクトを取得することになります。
[ConditionalFocusLayout]
[TextCellRenderer_TextCellView]
[ImageView]
[LinearLayout]
[TextView] text: "Image Cell"
[View]
iOS の場合のオブジェクト構造は以下のようなツリー構造となるため、Xamarin_Forms_Platform_iOS_CellTableViewCell
クラスの孫となる UIImageView
オブジェクトを取得することになります。
[Xamarin_Forms_Platform_iOS_CellTableViewCell] text: "Image Cell"
[UITableViewCellContentView]
[UITableViewLabel] label: "Image Cell", text: "Image Cell"
[UIImageView]
[_UITableViewCellSeparatorView]
[UITableTextAccessibilityElement] label: "Image Cell"
以下にサンプルコードを記載します。
// ImageCell
if (platform == Platform.Android)
{
app.Query(x => x.Class("TextCellRenderer_TextCellView").Class("ImageView"));
}
else if(platform == Platform.iOS)
{
app.Query(x => x.Class("Xamarin_Forms_Platform_iOS_CellTableViewCell").Child("UITableViewCellContentView").Class("UIImageView"));
}
ViewCell
ViewCell
の Xamarin.UITest のコードは、セル内に配置したコントロールに依存します。ここで記載している各コントロールのテストコードの書き方を参考にしてください。
Frame
Frame
コントロールを Xamarin.UITest で取得する方法について記載します。XAML 上に以下のように Frame
が定義されているものとします。
<Frame AutomationId="FrameDemoPage.Frame"
OutlineColor = "Black"
VerticalOptions = "CenterAndExpand">
<Label Text = "I've been framed!"/>
</Frame>
AutomationId
を指定して以下のようなコードで、Frame
コントロールを取得することができます。
app.Query(x => x.Marked("FrameDemoPage.Frame"));
NavigationPage
NavigationPage
において、前の画面に戻る場合は、IApp.Back
メソッドを利用しましょう。
app.Back();
MasterDetailPage
以下のシナリオを Xamarin.UITest で実現方法について記載します。
- ハンバーガーメニューをタップする
- メニューのアイテムを選択する
ハンバーガーメニューをタップする
ハンバーガーメニューをタップするには、ネイティブでどのようなコントロールに変換されているかを知っておく必要があります。
<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:FormsGallery"
x:Class="FormsGallery.MasterDetailPageDemoPage"
Title="Master Detail"
IsPresented="{Binding IsPresented}">
<MasterDetailPage.Master>
<local:ColorListPage/>
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<NavigationPage>
<x:Arguments>
<local:NamedColorPage Title="Detail"/>
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Detail>
</MasterDetailPage>
- Android の場合
Repl で UI のツリー構造を調べると、ハンバーガーメニューは、ネイティブでは、AppCompatImageButton
の "OK" ボタンに変換されていることがわかります。
[Toolbar] id: "toolbar"
[AppCompatTextView] text: "Detail"
[AppCompatImageButton] label: "OK"
- iOS の場合
Repl で UI のツリー構造を調べると、ハンバーガーメニューは、ネイティブでは label にアイコン名が付与されたボタンに変換されていることがわかります。
[UINavigationBar] id: "Detail"
[_UIBarBackground > UIImageView]
[_UINavigationBarContentView]
[_UIButtonBarStackView]
[_UIButtonBarButton > ... > UIImageView] label: "hamburger"
[UILabel] label: "Detail", text: "Detail"
[_UIButtonBarButton > ... > UIImageView] label: "hamburger"
従って、ハンバーガーメニューをタップするには、各プラットフォームで以下のように Xamarin.UITest のコードを記述する必要があります。
プラットフォーム | タップする方法 | 補足 |
---|---|---|
Android |
AppCompatImageButton ボタンで、ボタンのテキストが "OK" のボタンを IApp.Tap メソッドでタップする |
|
iOS | メニューの label の値でコントロールを取得し、 IApp.Tap メソッドをタップする |
label は、Title の値。ただし、Icon を指定したときはアイコン名(拡張子を除く) |
以下にハンバーガーメニューをタップするコードのサンプルを示します。
if(platform == Platform.Android)
{
app.Tap(x => x.Class("AppCompatImageButton").Marked("OK"));
}
else if (platform == Platform.iOS)
{
app.Tap(x => x.Marked("hamburger")); // Tap control marked with icon name
}
メニューの項目を選択する
メニューの項目を選択は、ListView
のリストから項目を選択する方法と同じアプローチで実現することができます。
- 選択対象となる項目が表示されるまで、
IApp.ScrollDownTo
/IApp.ScrollUpTo
メソッドを利用してスクロールする - 選択対象となる項目を
IApp.Tap
メソッドを利用してタップする- メニューのページ
以下のような ListView
が XAML 上に定義されており、この ListView
に表示される項目をタップする場合を考えます。
<ListView AutomationId="ColorListPage.ListView" ItemsSource="{Binding ColorList}">
<ListView.Behaviors>
<behaviorsPack:SelectedItemBehavior Command="{Binding SelectedColorCommand}"/>
</ListView.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout VerticalOptions="Center">
<Label AutomationId="ColorListPage.ColorLabel" Text="{Binding Name}" Margin="5,0,0,0"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ListView
をスクロールし、項目を選択するサンプルコードは、以下のようになります。
app.ScrollDownTo(x => x.Marked("ColorListPage.ColorLabel").Text("Yellow"), x => x.Marked("ColorListPage.ListView"), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Marked("ColorListPage.ColorLabel").Text("Yellow"));
TabbedPage
TabbedPage
内のページを切り替える操作を、Xamarin.UITest で実現する方法について記載します。XAML 上で、以下のように TabbedPage
の子ページが定義されているとします。
<TabbedPage.Children>
<local:Page1 Title="Tab1"/>
<local:Page2 Title="Tab2"/>
<local:Page3 Title="Tab3"/>
</TabbedPage.Children>
UITest で Tab2 のコンテンツに切り替えるには、以下のように AppQuery.Marked
メソッドで Tab のTitle をコンテンツを取得し、IApp.Tap
メソッドを利用してタップ操作を実現することができます。
app.Tap(x => x.Marked("Tab2"));
CarouselPage
CarouselPage
内に定義されたコンテンツをスワイプ操作で切り替えるシナリオを、Xamarin.UITest で実現する方法について記載します。IApp
インタフェースには、スワイプ操作を実現するメソッドが提供されていますので、これを利用することができます。
メソッド | 説明 |
---|---|
SwipeRightToLeft |
右から左へスワイプする |
SwipeLeftToRight |
左から右へスワイプする |
右から左にスワイプ操作を行うコードの例を以下に記載します。
app.SwipeRightToLeft(); // Swipe
ToolbarItems
ToolbarItems
コントロールのアイテムをタップするシナリオを、Xamairn.UITest で実現する方法について記載します。以下のように XAML 上に ToolBarItems
が定義されているものとします。
<ContentPage.ToolbarItems>
<ToolbarItem AutomationId="ToolbarItemDemoPage.Item1"
Text="ToolBar1"
Command="{Binding ClickCommand}"
CommandParameter="ToolBar1"/>
<ToolbarItem AutomationId="ToolbarItemDemoPage.Item2"
Text="ToolBar2"
Command="{Binding ClickCommand}"
CommandParameter="ToolBar2"/>
</ContentPage.ToolbarItems>
ToolBarItem
はネイティブでは以下のコントロールに変換されます。
プラットフォーム | ネイティブコントロール |
---|---|
Android | Android.Widget.Button |
iOS | UIBarButtonItem |
したがって、各プラットフォームで IApp.Tap
に受け渡すパラメータが異なります。
- Android
- ボタンのテキストの文字列を
AppQuery.Marked
に渡してコントロールを取得し、IApp.Tap
メソッドでタップします
- ボタンのテキストの文字列を
- iOS
-
AutomationId
をAppQuery.Id
に渡してコントロールを取得し、IApp.Tap
メソッドでタップします
-
サンプルコードを以下に示します。
if (platform == Platform.Android)
{
app.Tap(x => x.Marked("ToolBar1"));
app.Tap(x => x.Marked("ToolBar2"));
}
else if(platform == Platform.iOS)
{
app.Tap(x => x.Id("ToolbarItemDemoPage.Item1"));
app.Tap(x => x.Id("ToolbarItemDemoPage.Item2"));
}
3. おわりに
最後まで、みていただきありがとうございました。以下のコントロールに関する Tips は、時間の都合でここに掲載できなかったのですが、いずれアップデートできればと考えています。
WebView
Map
4. ソースコード
記事の動作検証に利用したソースコードは、Github に公開しています。