Android - Espresso - scrolling to a non-list View item - android

Is there a general approach for scrolling to non-list View items that are not yet visible on the screen?
Without any precautions, Espresso will indicate that "No Views in hierarchy found matching with id .....
I found this answer ... is this the best approach?
onView( withId( R.id.button)).perform( scrollTo(), click());

According to the scrollTo JavaDoc, to use the code you specified ( onView( withId( R.id.button)).perform( scrollTo(), click()); ), the preconditions are: "must be a descendant of ScrollView" and "must have visibility set to View.VISIBLE". If that is the case, then that will work just fine.
If it is in an AdapterView, then you should use onData instead. In some cases, you may have to implement the AdapterViewProtocol, if your AdapterView is not well behaved.
If it is neither in an AdapterView nor a child of a ScrollView, then you would have to implement a custom ViewAction.

If you have a view inside android.support.v4.widget.NestedScrollView instead of scrollView scrollTo() does not work.
In order to work you need to create a class that implements ViewAction just like ScrollToAction but allows NestedScrollViews:
public Matcher<View> getConstraints() {
return allOf(
withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
isDescendantOfA(anyOf(
isAssignableFrom(ScrollView.class),
isAssignableFrom(HorizontalScrollView.class),
isAssignableFrom(NestedScrollView.class))
)
);
}
extra tip and access the action like:
public static ViewAction betterScrollTo() {
return actionWithAssertions(new AllScrollViewsScrollToAction());
}
But with this scroll it does not trigger events from the layout managers.

Code that worked for me is:
ViewInteraction tabView = onView(allOf(
childAtPosition(childAtPosition(withId(R.id.bottomControlTabView), 0), 1),
isDisplayed()));
tabView.perform(click());
tabView.perform(click());
public static Matcher<View> childAtPosition(final Matcher<View> parentMatcher,
final int position) {
return new TypeSafeMatcher<View>() {
#Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
#Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}

The code onView( withId( R.id.button)).perform( scrollTo(), click()); will work if the view is descendant of ScrollView, HorizontalScrollView or ListView.
If we have NestedScrollView instead of ScrollView and for those who don't want to look into ScrollToAction class code I wrote the sample.
As Bruno Oliveira said we can do something like this:
class ScrollToActionImproved : ViewAction {
override fun getConstraints(): Matcher<View> {
return allOf(
withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
isDescendantOfA(
anyOf(
isAssignableFrom(ScrollView::class.java),
isAssignableFrom(HorizontalScrollView::class.java),
isAssignableFrom(NestedScrollView::class.java)
)
)
)
}
override fun getDescription(): String = "scroll to view"
override fun perform(uiController: UiController?, view: View?) {
if (isDisplayingAtLeast(90).matches(view)) {
//View is already displayed
return
}
val rect = Rect()
view!!.getDrawingRect(rect)
if (!view.requestRectangleOnScreen(rect, true)) {
//Scrolling to view was requested, but none of the parents scrolled.
}
uiController!!.loopMainThreadUntilIdle()
if (!isDisplayingAtLeast(90).matches(view)) {
throw PerformException.Builder()
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(view))
.withCause(
RuntimeException(
"Scrolling to view was attempted, but the view is not displayed"
)
)
.build()
}
}
}
And use it like this:
fun scrollToImproved(): ViewAction =
actionWithAssertions(ScrollToActionImproved())
/* some logic */
onView(withId(R.id.button)).perform(scrollToImproved())
It should work.

Related

Force parameter to be specific types that can be passed into a method

Android 3.5
Kotlin 1.3
I have the following method that passes in a parameter that could be VISIBLE, INVISIBLE, or GONE
fun setPromotionVisibility(Int: toVisiblity) {
tvPromoation.visibility = toVisibility
}
However, when I call this method I could pass in any Int that might not be a visibility i.e.
setPromotionVisibility(234)
instead of doing this:
setPromotionVisibility(View.VISIBLE)
Just wondering if there anything I could do to force the user of the method to only enter VISIBLE, INVISIBLE, or GONE
Many thanks in advance
You can create a type-safe approach with an enum:
enum class Visibility(
val asInt: Int
) {
VISIBLE(View.VISIBLE),
INVISIBLE(View.INVISIBLE),
GONE(View.GONE),
}
which you then use as a parameter type:
fun setPromotionVisibility(toVisiblity: Visibility) {
tvPromoation.visibility = toVisibility.asInt
}
Use Annotation for this
#IntDef({View.VISIBLE, View.INVISIBLE, View.GONE})
#Retention(RetentionPolicy.SOURCE)
public #interface Visibility {}
fun setPromotionVisibility(#Visibility toVisiblity: Int) {
tvPromoation.visibility = toVisibility
}
I don't know if it is useful for your case, but in my projects, I almost never use INVISIBLE.
So, I made an extension function
fun View.visible(value: Boolean) {
visibility = if (value) View.VISIBLE else View.GONE
}
It also can be better:
fun View.visible(value: Boolean, animated: Boolean = false) {
if (animated) {
if (value) animate().alpha(1F).withStartAction { visibility = View.VISIBILE }
else animate().alpha(0F).withEndAction { visibility = View.GONE }
} else visibility = if (value) View.VISIBLE else View.GONE
}

Android Espresso testing SwipeRefreshLayout OnRefresh not been triggered on swipeDown

I'm trying to write simple test for pull to refresh as a part of integration testing. I'm using the newest androidX testing components and Robolectric. I'm testing isolated fragment in which one I'm injecting mocked presenter.
XML layout part
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerTasks"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Fragment part
binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
presenter.onRefresh();
}
});
Test:
onView(withId(R.id.refreshLayout)).perform(swipeDown());
verify(presenter).onRefresh();
but test doesn't pass, message:
Wanted but not invoked: presenter.onRefresh();
The app works perfectly fine and pull to refresh calls presenter.onRefresh(). I did also debugging of the test and setOnRefreshListener been called and it's not a null. If I do testing with custom matcher to check the status of SwipeRefreshLayout test passes.
onView(withId(R.id.refreshLayout)).check(matches(isRefreshing()));
I did some minor investigation over last weekend since I was facing the same issue and it was bothering me. I also did some comparing with what happens on a device to spot the differences.
Internally androidx.swiperefreshlayout.widget.SwipeRefreshLayout has an mRefreshListener that will run when onAnimationEnd is called. The AnimationEnd will trigger then OnRefreshListener.onRefresh method.
That animation listener (mRefreshListener) is passed to the mCircleView (CircleImageView) and the circle animation start is called.
On a device when the view draw method is called it will call the applyLegacyAnimation method that will, in turn, call the AnimationStart method. At the AnimationEnd, the onRefresh method will be called.
On Robolectric the draw method of the View is never called since the items are not actually drawn. This means that the animation will never run and thus neither will the onRefresh method.
My conclusion is that with the current version of Robolectric is not possible to verify that the onRefresh called due to implementation limitations. It seems though that it is planned to have a realistic rendering in the future.
I'm finally able to solve this using a hacky way :
fun swipeToRefresh(): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return object : BaseMatcher<View>() {
override fun matches(item: Any): Boolean {
return isA(SwipeRefreshLayout::class.java).matches(item)
}
override fun describeMismatch(item: Any, mismatchDescription: Description) {
mismatchDescription.appendText(
"Expected SwipeRefreshLayout or its Descendant, but got other View"
)
}
override fun describeTo(description: Description) {
description.appendText(
"Action SwipeToRefresh to view SwipeRefreshLayout or its descendant"
)
}
}
}
override fun getDescription(): String {
return "Perform swipeToRefresh on the SwipeRefreshLayout"
}
override fun perform(uiController: UiController, view: View) {
val swipeRefreshLayout = view as SwipeRefreshLayout
swipeRefreshLayout.run {
isRefreshing = true
// set mNotify to true
val notify = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mNotify"
}
notify?.isAccessible = true
if (notify is KMutableProperty<*>) {
notify.setter.call(this, true)
}
// mockk mRefreshListener onAnimationEnd
val refreshListener = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mRefreshListener"
}
refreshListener?.isAccessible = true
val animatorListener = refreshListener?.get(this) as Animation.AnimationListener
animatorListener.onAnimationEnd(mockk())
}
}
}
}

How to type text on a SearchView using espresso

TypeText doesn't seem to work with SearchView.
onView(withId(R.id.yt_search_box))
.perform(typeText("how is the weather?"));
gives the error:
Error performing 'type text(how is the weather?)' on view 'with id:../yt_search_box'
For anyone that bump into this problem too, the solution is to write a ViewAction for the type SearchView, since the typeText only supports TextEditView
Here my solution:
public static ViewAction typeSearchViewText(final String text){
return new ViewAction(){
#Override
public Matcher<View> getConstraints() {
//Ensure that only apply if it is a SearchView and if it is visible.
return allOf(isDisplayed(), isAssignableFrom(SearchView.class));
}
#Override
public String getDescription() {
return "Change view text";
}
#Override
public void perform(UiController uiController, View view) {
((SearchView) view).setQuery(text,false);
}
};
}
#MiguelSlv answer above, converted to kotlin
fun typeSearchViewText(text: String): ViewAction {
return object : ViewAction {
override fun getDescription(): String {
return "Change view text"
}
override fun getConstraints(): Matcher<View> {
return allOf(isDisplayed(), isAssignableFrom(SearchView::class.java))
}
override fun perform(uiController: UiController?, view: View?) {
(view as SearchView).setQuery(text, false)
}
}
}
This works for me:
onView(withId(R.id.search_src_text)).perform(typeText("how is the weather?"))
Based on César Noreña respose I manage to insert text in the search field.
First I clicked on my view and then typeText on the android search view id.
onView(withId(R.id.my_own_search_menu_id)).perform(click());
onView(withId(R.id.search_src_text)).perform(typeText("how is the weather?"))
Also the X button of SearchView has ID search_close_btn
onView(withId(R.id.search_close_btn)).perform(click()); // first time erase the content
onView(withId(R.id.search_close_btn)).perform(click()); // second time close the SearchView
you can use Resource#getSystem to get the View
Resources.getSystem().getIdentifier("search_src_text",
"id", "android")
onView(withId(Resources.getSystem().getIdentifier("search_src_text",
"id", "android"))).perform(clearText(),typeText("enter the text"))
.perform(pressKey(KeyEvent.KEYCODE_ENTER))

Automating number picker in android using espresso

How to automate number picker using espresso. I want to set specific time in the timePicker using espresso.
To match a View by its class name you can simply use:
onView(withClassName(Matchers.equalTo(TimePicker.class.getName())));
Once you have the ViewInteraction object you can set a value on it defining and using a ViewAction as following:
public static ViewAction setTime(final int hour, final int minute) {
return new ViewAction() {
#Override
public void perform(UiController uiController, View view) {
TimePicker tp = (TimePicker) view;
tp.setCurrentHour(hour);
tp.setCurrentMinute(minute)
}
#Override
public String getDescription() {
return "Set the passed time into the TimePicker";
}
#Override
public Matcher<View> getConstraints() {
return ViewMatchers.isAssignableFrom(TimePicker.class);
}
};
}
Match the view and then perform the action:
ViewInteraction numPicker = onView(withClassName(Matchers.equalTo(NumberPicker.class.getName())));
numPicker.perform(setNumber(1));
Create a ViewAction to set the number:
public static ViewAction setNumber(final int num) {
return new ViewAction() {
#Override
public void perform(UiController uiController, View view) {
NumberPicker np = (NumberPicker) view;
np.setValue(num);
}
#Override
public String getDescription() {
return "Set the passed number into the NumberPicker";
}
#Override
public Matcher<View> getConstraints() {
return ViewMatchers.isAssignableFrom(NumberPicker.class);
}
};
}
There is a problem with accepted answer : it does not fire on change event. So (if you need it) you can't test if your view react to this on change event.
The following code (kotlin) is not cool anyway but i think it's the only way.
fun setValue(value: Int): ViewAction {
return object : ViewAction {
override fun getDescription(): String {
return "set the value of a " + NumberPicker::class.java.name
}
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(NumberPicker::class.java)
}
// the only way to fire onChange event is to call this private method
override fun perform(uiController: UiController?, view: View?) {
val numberPicker = view as NumberPicker
val setValueMethod = NumberPicker::class.java.getDeclaredMethod(
"setValueInternal",
Int::class.java,
Boolean::class.java
)
setValueMethod.isAccessible = true
setValueMethod.invoke(numberPicker, value, true)
}
}
}
For those looking at this question later (like me) this might be helpful: DateTimePickerTest makes use of PickerActions. PickerActions allows code for date pickers like this (Java):
onView(withClassName(Matchers.equalTo(DatePicker.class.getName()))).perform(PickerActions.setDate(year, month + 1, day));
Or for time pickers (Kotlin):
onView(withClassName(Matchers.equalTo(TimePicker::class.java.name))).perform(PickerActions.setTime(0, 10))
I used the built-in TimePickerDialog.
(Kotlin)
fun setAlarmTime(){
val calendar = Calendar.getInstance()
onView(isAssignableFrom(TimePicker::class.java)).perform(
PickerActions.setTime(
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE) + 2
)
)
onView(withText("OK")).perform(click())
}
Firstly, you should open the TimePickerDialog, then use this code. The time will be the current time + 2 minute. After the setup, it clicks on the OK button.
I have found a solution to the problem, that Luigi Massa Gallerano's solution does not trigger the change listener.
Additionally to his method you need to add a wrapping method, that swipes up and down one time each. This triggers the changeListener, though the former value of course, is lost.
fun setNumberPickerValue(viewInteraction: ViewInteraction, value: Int) {
viewInteraction.perform(setValue(value))
viewInteraction.perform(GeneralSwipeAction(Swipe.SLOW, GeneralLocation.TOP_CENTER, GeneralLocation.BOTTOM_CENTER, Press.FINGER))
SystemClock.sleep(50)
viewInteraction.perform(GeneralSwipeAction(Swipe.SLOW, GeneralLocation.BOTTOM_CENTER, GeneralLocation.TOP_CENTER, Press.FINGER))
SystemClock.sleep(50)
}
The example given is using NumberPicker and Kotlin, but in principle, it's the same.

How to disable all click events of a layout?

I have a layout that contains many views. Is there an easy way to disable all its views click events?
You can pass View for disable all child click event.
public static void enableDisableView(View view, boolean enabled) {
view.setEnabled(enabled);
if ( view instanceof ViewGroup ) {
ViewGroup group = (ViewGroup)view;
for ( int idx = 0 ; idx < group.getChildCount() ; idx++ ) {
enableDisableView(group.getChildAt(idx), enabled);
}
}
}
Rather than iterating through all the children view, you can add this function to the parent Layout view
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
This will be called before the onTouchEvent for any child views, and if it returns true, the onTouchEvent for child views wont be called at all. You can create a boolean field member to toggle this state on and off if you want.
I would create a ViewGroup with all the views that you want to enable/disable at the same time and call setClickable(true/false) to enable/disable clicking.
Here is a Kotlin extension function implementation of Parag Chauhan's answer
fun View.setAllEnabled(enabled: Boolean) {
isEnabled = enabled
if (this is ViewGroup) children.forEach { child -> child.setAllEnabled(enabled) }
}
You need to call setEnabled(boolean value) method on the view.
view.setClickable(false);
view.setEnabled(false);
If you dont want to set all child views to disable state (because they may look different to enable state) you can use this approach:
private fun toogleTouchable(canTouch: Boolean) {
container.descendantFocusability =
if (value) {
container.requestFocus()
ViewGroup.FOCUS_BLOCK_DESCENDANTS
} else {
ViewGroup.FOCUS_AFTER_DESCENDANTS
}
}
my layout has two modes. One preview, when children should not be clickable and the other when children should be clickable.
Of all the solutions mentioned here, I found overriding onInterceptTouchEvent most appropriate. Very little code, with no iteration on children.
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(isPreviewMode())
{
//No child is clickable in preview mode.
return true;
}
//All children are clickable otherwise
return false;
}
Though disabling children is most popular solution, I am not confortable with that.
You may need to reEnable them and the logic should match exactly. The more is the code, the higher the chances of bugs in the future.
Extension function in Kotlin, with the support of Recycler view.
fun ViewGroup.setEnabledStateForAllChildren(enable: Boolean) {
children.forEach {
when (it) {
is RecyclerView -> {
it.addOnItemTouchListener(object : SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
return enable.not()
}
})
}
is ViewGroup -> {
it.setEnabledStateForAllChildren(enable)
}
else -> {
it.isEnabled = enable
}
}
}
}
I would implement onClickListener interface in your activity class and return false in onClick method. I feel it's the easiest way to solve your problem.

Categories

Resources