Commit 1f0de454 authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

Add view waiting function as test util

The added function |waitForView| repeatedly checks all views in a group
until either a view was found that matches the given matcher.
If this doesn't happen within a certain time or multiple views are
matched, it fails the test.

This simplifies UI tests by:
 - reducing flakiness, e.g. introduced due to animations
 - removing the need for sleeps or other timers.
 - reducing code duplication due to providing a central implementation

Upcoming CLs that would benefit greatly from this:
https://crrev.com/c/951588 and https://crrev.com/c/951764

Change-Id: Ibb6a0dcd2eb688467c4af1eaa82341f84e7e8bc6
Reviewed-on: https://chromium-review.googlesource.com/951688
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#542172}
parent bf17a41e
...@@ -36,6 +36,8 @@ import static org.hamcrest.Matchers.is; ...@@ -36,6 +36,8 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.chromium.chrome.test.util.ViewUtils.waitForView;
import android.app.Activity; import android.app.Activity;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.content.Intent; import android.content.Intent;
...@@ -44,14 +46,11 @@ import android.support.annotation.IntDef; ...@@ -44,14 +46,11 @@ import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Espresso; import android.support.test.espresso.Espresso;
import android.support.test.espresso.PerformException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.intent.Intents; import android.support.test.espresso.intent.Intents;
import android.support.test.espresso.util.TreeIterables;
import android.support.test.filters.SmallTest; import android.support.test.filters.SmallTest;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
...@@ -81,7 +80,6 @@ import java.lang.annotation.Retention; ...@@ -81,7 +80,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.TimeoutException;
/** /**
* Tests for the "Save Passwords" settings screen. * Tests for the "Save Passwords" settings screen.
...@@ -333,39 +331,6 @@ public class SavePasswordsPreferencesTest { ...@@ -333,39 +331,6 @@ public class SavePasswordsPreferencesTest {
Thread.sleep(100); Thread.sleep(100);
} }
/**
* Waits until a view matching the given matcher appears. Times out if no view was found until
* the UI_UPDATING_TIMEOUT_MS expired.
* @param viewMatcher The matcher matching the view that should be waited for.
*/
private void waitForView(Matcher<View> viewMatcher) {
Espresso.onView(isRoot()).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "wait for " + UI_UPDATING_TIMEOUT_MS + "ms to match "
+ viewMatcher.toString();
}
@Override
public void perform(UiController uiController, View root) {
final long start_time = System.currentTimeMillis();
do {
for (View view : TreeIterables.breadthFirstViewTraversal(root))
if (viewMatcher.matches(view)) return;
uiController.loopMainThreadForAtLeast(100);
} while (System.currentTimeMillis() - start_time < UI_UPDATING_TIMEOUT_MS);
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withCause(new TimeoutException())
.build();
}
});
}
/** /**
* Ensure that resetting of empty passwords list works. * Ensure that resetting of empty passwords list works.
...@@ -1590,12 +1555,15 @@ public class SavePasswordsPreferencesTest { ...@@ -1590,12 +1555,15 @@ public class SavePasswordsPreferencesTest {
ReauthenticationManager.setApiOverride(ReauthenticationManager.OVERRIDE_STATE_AVAILABLE); ReauthenticationManager.setApiOverride(ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
ReauthenticationManager.setScreenLockSetUpOverride( ReauthenticationManager.setScreenLockSetUpOverride(
ReauthenticationManager.OVERRIDE_STATE_AVAILABLE); ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
final Preferences prefs =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(), PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName()); SavePasswordsPreferences.class.getName());
// Open the search and filter all but "Zeus". // Open the search and filter all but "Zeus".
Espresso.onView(withSearchMenuIdOrText()).perform(click()); Espresso.onView(withSearchMenuIdOrText()).perform(click());
waitForView(withId(R.id.search_src_text));
Espresso.onView(isRoot()).check(
(root, e) -> waitForView((ViewGroup) root, withId(R.id.search_src_text)));
Espresso.onView(withId(R.id.search_src_text)) Espresso.onView(withId(R.id.search_src_text))
.perform(click(), typeText("Zeu"), closeSoftKeyboard()); .perform(click(), typeText("Zeu"), closeSoftKeyboard());
InstrumentationRegistry.getInstrumentation().waitForIdleSync(); InstrumentationRegistry.getInstrumentation().waitForIdleSync();
...@@ -1632,7 +1600,10 @@ public class SavePasswordsPreferencesTest { ...@@ -1632,7 +1600,10 @@ public class SavePasswordsPreferencesTest {
InstrumentationRegistry.getInstrumentation().waitForIdleSync(); InstrumentationRegistry.getInstrumentation().waitForIdleSync();
// The search bar should still be open and still display the search query. // The search bar should still be open and still display the search query.
waitForView(allOf(withId(R.id.search_src_text), withText("Zeu"))); Espresso.onView(isRoot()).check(
(root, e)
-> waitForView((ViewGroup) root,
allOf(withId(R.id.search_src_text), withText("Zeu"))));
Espresso.onView(withId(R.id.search_src_text)).check(matches(withText("Zeu"))); Espresso.onView(withId(R.id.search_src_text)).check(matches(withText("Zeu")));
} }
...@@ -1673,7 +1644,9 @@ public class SavePasswordsPreferencesTest { ...@@ -1673,7 +1644,9 @@ public class SavePasswordsPreferencesTest {
InstrumentationRegistry.getInstrumentation().removeMonitor(monitor); InstrumentationRegistry.getInstrumentation().removeMonitor(monitor);
Espresso.onView(withContentDescription(R.string.abc_action_bar_up_description)) Espresso.onView(withContentDescription(R.string.abc_action_bar_up_description))
.perform(click()); // Go back to the search list. .perform(click()); // Go back to the search list.
waitForView(withId(R.id.search_src_text)); Espresso.onView(isRoot()).check(
(root, e) -> waitForView((ViewGroup) root, withId(R.id.search_src_text)));
Assert.assertEquals(1, viewed_after_search_delta.getDelta()); Assert.assertEquals(1, viewed_after_search_delta.getDelta());
preferences.finish(); preferences.finish();
......
...@@ -54,6 +54,7 @@ android_library("chrome_java_test_support") { ...@@ -54,6 +54,7 @@ android_library("chrome_java_test_support") {
"javatests/src/org/chromium/chrome/test/util/ChromeSigninUtils.java", "javatests/src/org/chromium/chrome/test/util/ChromeSigninUtils.java",
"javatests/src/org/chromium/chrome/test/util/ChromeTabUtils.java", "javatests/src/org/chromium/chrome/test/util/ChromeTabUtils.java",
"javatests/src/org/chromium/chrome/test/util/DisableInTabbedMode.java", "javatests/src/org/chromium/chrome/test/util/DisableInTabbedMode.java",
"javatests/src/org/chromium/chrome/test/util/ViewUtils.java",
"javatests/src/org/chromium/chrome/test/util/FullscreenTestUtils.java", "javatests/src/org/chromium/chrome/test/util/FullscreenTestUtils.java",
"javatests/src/org/chromium/chrome/test/util/InfoBarTestAnimationListener.java", "javatests/src/org/chromium/chrome/test/util/InfoBarTestAnimationListener.java",
"javatests/src/org/chromium/chrome/test/util/InfoBarUtil.java", "javatests/src/org/chromium/chrome/test/util/InfoBarUtil.java",
...@@ -96,6 +97,7 @@ android_library("chrome_java_test_support") { ...@@ -96,6 +97,7 @@ android_library("chrome_java_test_support") {
"//third_party/android_tools:android_support_design_java", "//third_party/android_tools:android_support_design_java",
"//third_party/android_tools:android_support_v7_appcompat_java", "//third_party/android_tools:android_support_v7_appcompat_java",
"//third_party/android_tools:android_support_v7_recyclerview_java", "//third_party/android_tools:android_support_v7_recyclerview_java",
"//third_party/hamcrest:hamcrest_core_java",
"//third_party/jsr-305:jsr_305_javalib", "//third_party/jsr-305:jsr_305_javalib",
"//third_party/junit", "//third_party/junit",
"//third_party/ub-uiautomator:ub_uiautomator_java", "//third_party/ub-uiautomator:ub_uiautomator_java",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.test.util;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.support.annotation.IntDef;
import android.view.View;
import android.view.ViewGroup;
import org.hamcrest.Matcher;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.List;
/**
* Collection of utilities helping to clarify expectations on views in tests.
*/
public class ViewUtils {
@Retention(SOURCE)
@IntDef(flag = true, value = {VIEW_VISIBLE, VIEW_INVISIBLE, VIEW_GONE, VIEW_NULL})
public @interface ExpectedViewState {}
public static final int VIEW_VISIBLE = 1;
public static final int VIEW_INVISIBLE = 1 << 1;
public static final int VIEW_GONE = 1 << 2;
public static final int VIEW_NULL = 1 << 3;
private static class ExpectedViewCriteria extends Criteria {
private final Matcher<View> mViewMatcher;
private final @ExpectedViewState int mViewState;
private final ViewGroup mRootView;
ExpectedViewCriteria(
Matcher<View> viewMatcher, @ExpectedViewState int viewState, ViewGroup rootView) {
mViewMatcher = viewMatcher;
mViewState = viewState;
mRootView = rootView;
}
@Override
public boolean isSatisfied() {
List<View> matchedViews = new ArrayList<>();
findMatchingChildren(mRootView, matchedViews);
if (matchedViews.size() > 1) {
updateFailureReason("Multiple views matched: " + mViewMatcher.toString());
return false;
}
return matchedViews.size() == 0 ? hasViewExpectedState(null)
: hasViewExpectedState(matchedViews.get(0));
}
private void findMatchingChildren(ViewGroup root, List<View> matchedViews) {
for (int i = 0; i < root.getChildCount(); i++) {
View view = root.getChildAt(i);
if (mViewMatcher.matches(view)) matchedViews.add(view);
if (view instanceof ViewGroup) {
findMatchingChildren((ViewGroup) view, matchedViews);
}
};
}
private boolean hasViewExpectedState(View view) {
if (view == null) {
updateFailureReason("No matching view was found!");
return (mViewState & VIEW_NULL) != 0;
}
switch (view.getVisibility()) {
case View.VISIBLE:
updateFailureReason("Found view is unexpectedly visible!");
return (mViewState & VIEW_VISIBLE) != 0;
case View.INVISIBLE:
updateFailureReason("Found view is unexpectedly invisible!");
return (mViewState & VIEW_INVISIBLE) != 0;
case View.GONE:
updateFailureReason("Found view is unexpectedly gone!");
return (mViewState & VIEW_GONE) != 0;
}
assert false; // Not Reached.
return false;
}
}
private ViewUtils() {}
/**
* Waits until a view matching the given matches any of the given {@link ExpectedViewState}s.
* Fails if the matcher applies to multiple views. Times out if no view was found while waiting
* up to
* {@link CriteriaHelper#DEFAULT_MAX_TIME_TO_POLL} milliseconds.
* @param root The view group to search in.
* @param viewMatcher The matcher matching the view that should be waited for.
* @param viewState State that the matching view should be in. If multiple states are passed,
* the waiting will stop if at least one applies.
*/
public static void waitForView(
ViewGroup root, Matcher<View> viewMatcher, @ExpectedViewState int viewState) {
CriteriaHelper.pollUiThread(new ExpectedViewCriteria(viewMatcher, viewState, root));
}
/**
* Waits until a visible view matching the given matcher appears. Fails if the matcher applies
* to multiple views. Times out if no view was found while waiting up to
* {@link CriteriaHelper#DEFAULT_MAX_TIME_TO_POLL} milliseconds.
* @param root The view group to search in.
* @param viewMatcher The matcher matching the view that should be waited for.
*/
public static void waitForView(ViewGroup root, Matcher<View> viewMatcher) {
waitForView(root, viewMatcher, VIEW_VISIBLE);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment