Reputation: 949
We're testing our Xamarin.Forms app using UITest, and need to select the value of a Picker on iOS.
It's possible to select by the row number in the collection of items like this:
app.Query(x => x.Class("UIPickerView").Invoke("selectRow", 1, "inComponent", 0, "animated", true))
where we're selecting row 1 in the above example.
However, we need to be able to select by value - so for example, with a Picker which has a list of titles, to select the "Mrs" title, we need to do something like:
app.Query(x => x.Class("UIPickerView").Invoke("selectValue", "Mrs", "inComponent", 0, "animated", true))
However, there isn't a selectValue (or similar) method available on the UIPickerView (reference here).
One thought is to get the number of rows in the items, and iterate through them - but when I try to call getNumberOfRows
, I get the following error:
app.Query(x => x.Class("UIPickerView").Invoke("numberOfRows", "inComponent", 0))
Error while performing Query([unknown]) Exception: System.Exception: Invoking an iOS selector requires either 0 or an uneven number of arguments (they have to match up pairwise including method name). at Xamarin.UITest.Queries.InvokeHelper.AppTypedSelector (Xamarin.UITest.Queries.AppQuery appQuery, Xamarin.UITest.Queries.ITokenContainer tokenContainer, System.Object[] queryParams, System.String methodName, System.Object[] arguments, System.Boolean explicitlyRequestedValue) [0x0011a] in <2a16c16730a54859bda72c6bc1c728f7>:0 at Xamarin.UITest.Queries.InvokeHelper.Invoke (Xamarin.UITest.Queries.AppQuery appQuery, System.String methodName, System.Object[] arguments) [0x00000] in <2a16c16730a54859bda72c6bc1c728f7>:0 at Xamarin.UITest.Queries.AppQuery.Invoke (System.String methodName, System.Object arg1, System.Object arg2) [0x00000] in <2a16c16730a54859bda72c6bc1c728f7>:0 at .m__0 (Xamarin.UITest.Queries.AppQuery x) [0x0000b] in <0de9804cff324d049415e25573e8da8a>:0 at Xamarin.UITest.SharedApp.Expand[T] (System.Func
2[T,TResult] typedQuery) [0x0000c] in <2a16c16730a54859bda72c6bc1c728f7>:0 at Xamarin.UITest.iOS.iOSApp+<>c__DisplayClass17_0
1[T].b__0 () [0x00000] in <2a16c16730a54859bda72c6bc1c728f7>:0 at Xamarin.UITest.Utils.ErrorReporting.With[T] (System.Func`1[TResult] func, System.Object[] args, System.String memberName) [0x0000e] in <2a16c16730a54859bda72c6bc1c728f7>:0 Exception: Error while performing Query([unknown])
The cause of the error is quite clear - the method is expecting a particular pairing of parameters, but I can't see how to formulate the query correctly.
So any thoughts/pointers on how I can select by value, not row number, and/or how to get the number of items?
Upvotes: 1
Views: 1286
Reputation: 849
You'll find the latest code for DatePickers and TimePickers in this official doc: https://learn.microsoft.com/en-us/appcenter/test-cloud/frameworks/uitest/features/date-time-pickers
Upvotes: 0
Reputation: 1
As of Xamarin.UITest v3.2.2 iOS 15, I noticed that you can insert a string into the selectRow Invoke method:
app.Query(x => x.Class("UIPickerView").Invoke("selectRow", "[ITEM_NAME]", "inComponent", 0, "animated", false));
Replace [ITEM_NAME]
with the exact string of a row. You still need to do the workaround with nudging the Picker (DragCoordinates) to trigger the change delegates as shown by James Lavery's answer:
// This is a hack to move the item so that Xamarin Forms will generate the event to update the UI
var rect = app.Query(x => x.Class("UIPickerTableView").Index(0).Child()).First().Rect;
app.DragCoordinates(rect.CenterX, rect.CenterY, rect.CenterX, rect.CenterY + 22);
app.DragCoordinates(rect.CenterX, rect.CenterY, rect.CenterX, rect.CenterY - 22);
Upvotes: 0
Reputation: 949
A better solution is to write a UITest backdoor, and then in the backdoor find the Renderer for the Picker and control it directly.
The core to the solution was provided to me by @LandLu on the Xamarin forums:
You should obtain the current view controller first. Then iterate the subview to get your picker renderer. Here is my dependency service for you referring to:
[assembly: Dependency(typeof(FindViewClass))]
namespace Demo.iOS
{
public class FindViewClass : IFindView
{
public void FindViewOfClass()
{
UIViewController currentViewController = topViewController();
getView(currentViewController.View);
}
List<PickerRenderer> pickerList = new List<PickerRenderer>();
void getView(UIView view)
{
if (view is PickerRenderer)
{
pickerList.Add((PickerRenderer)view);
}
else
{
foreach (UIView subView in view.Subviews)
{
getView(subView);
}
}
}
UIViewController topViewController()
{
return topViewControllerWithRootViewController(UIApplication.SharedApplication.KeyWindow.RootViewController);
}
UIViewController topViewControllerWithRootViewController(UIViewController rootViewController)
{
if (rootViewController is UITabBarController)
{
UITabBarController tabbarController = (UITabBarController)rootViewController;
return topViewControllerWithRootViewController(tabbarController.SelectedViewController);
}
else if (rootViewController is UINavigationController)
{
UINavigationController navigationController = (UINavigationController)rootViewController;
return topViewControllerWithRootViewController(navigationController.VisibleViewController);
}
else if (rootViewController.PresentedViewController != null)
{
UIViewController presentedViewController = rootViewController.PresentedViewController;
return topViewControllerWithRootViewController(presentedViewController);
}
return rootViewController;
}
}
}
Once I've done that, I can select the index/item I want via the Element property of the Renderer.
Edit I've worked this up into a GitHub repository which implements a set of Backdoors to make iOS Picker selection easier
Upvotes: 1
Reputation: 949
Ok... I've got something which works.
The premise is that we select rows in the underlying list of items, and then check whether the Picker is displaying the value we want.
Notes 1. I've got a UIElement helper class which allows me to get at either the underlying Control or AutomationId.
The two methods below are used so that I can call SelectPickerValue
directly from my TestPages or use TapAndSelectPickerValue
- I should probably tidy this up.
I haven't catered for the ability to scroll the other way if the current value is in the middle of available values.
If the item is in view, it's possible to just Tap it - but if it's not we have to do scrolling/row selection.
internal static void TapAndSelectPickerValue(UIElement element, string v, int maxRows = 10)
{
app.Tap(element.UIControl);
SelectPickerValue(v, maxRows, element.AutomationId);
}
internal static void SelectPickerValue(string v, int maxItems = 10,string pickerId = "")
{
if (_platform == Platform.iOS)
{
bool managedToSelect = false;
// Check that we haven't already got the value we want selected.
Xamarin.UITest.Queries.AppResult[] valueCheckResult = app.Query(x => x.Text(v).Id(pickerId));
if (valueCheckResult.Length == 0)
{
int rowNumber = 0;
// Select rows until we either reach the max to try, or we get the one we want selected
for (rowNumber = 0; rowNumber < maxItems; rowNumber++)
{
app.Query(x => x.Class("UIPickerView").Invoke("selectRow", rowNumber, "inComponent", 0, "animated", true));
// This is a hack to move the item so that Xamarin Forms will generate the event to update the UI
var rect = app.Query(x => x.Class("UIPickerTableView").Index(0).Child()).First().Rect;
app.DragCoordinates(rect.CenterX, rect.CenterY, rect.CenterX, rect.CenterY + 22);
app.DragCoordinates(rect.CenterX, rect.CenterY, rect.CenterX, rect.CenterY - 22);
// check to see if the Picker is now displaying the value we want
valueCheckResult = app.Query(x => x.Text(v).Id(pickerId));
if (valueCheckResult.Length == 1)
{
managedToSelect = true;
break;
}
}
}
// If we couldn't select the value we wanted, raise an exception
if (!managedToSelect)
{
throw new Exception($"Could not select value '{v}' for picker '{pickerId}'");
}
// Close the Picker
app.Tap(c => c.Text("Done"));
}
else
{
app.ScrollDownTo(m => m.Text(v), x => x.Id("select_dialog_listview"));
app.Tap(x => x.Text(v));
}
}
Upvotes: 0