Android Espresso等待文本出现

Ann*_*ova 0 android chatbot android-espresso

我正在尝试使用Espresso自动执行Android应用程序,即聊天机器人。我可以说我是Android应用程序自动化的全新手。现在,我正苦苦等待。如果我使用thread.sleep,它可以很好地工作。但是我想等待特定的文本出现在屏幕上-我该怎么做?

@Rule
public ActivityTestRule<LoginActivity> mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);

@Test
public void loginActivityTest() {
ViewInteraction loginName = onView(allOf(withId(R.id.text_edit_field),
childAtPosition(childAtPosition(withId(R.id.email_field),0), 1)));
loginName.perform(scrollTo(), replaceText("test@test.test"), closeSoftKeyboard());

ViewInteraction password= onView(allOf(withId(R.id.text_edit_field),
childAtPosition(childAtPosition(withId(R.id.password_field),0), 1)));
password.perform(scrollTo(), replaceText("12345678"), closeSoftKeyboard());

ViewInteraction singInButton = onView(allOf(withId(R.id.sign_in), withText("Sign In"),childAtPosition(childAtPosition(withId(R.id.scrollView), 0),2)));
singInButton .perform(scrollTo(), click());
Run Code Online (Sandbox Code Playgroud)

//这里我需要等待文本“ Hi ...” //一些解释:按下按钮签名后,聊天机器人说“ hi”,会提供更多信息,我想等待最后一条消息出现屏幕上。

man*_*alf 22

我喜欢上面@jeprubio 的回答,但是我遇到了评论中提到的@desgraci 相同的问题,他们的匹配器一直在寻找旧的、陈旧的 rootview 的视图。当尝试在测试中的活动之间进行转换时,这种情况经常发生。

我对传统“隐式等待”模式的实现存在于下面的两个 Kotlin 文件中。

EspressoExtensions.kt包含一个函数searchFor,一旦在提供的 rootview 中找到匹配项,它就会返回一个 ViewAction。

class EspressoExtensions {

    companion object {

        /**
         * Perform action of waiting for a certain view within a single root view
         * @param matcher Generic Matcher used to find our view
         */
        fun searchFor(matcher: Matcher<View>): ViewAction {

            return object : ViewAction {

                override fun getConstraints(): Matcher<View> {
                    return isRoot()
                }

                override fun getDescription(): String {
                    return "searching for view $matcher in the root view"
                }

                override fun perform(uiController: UiController, view: View) {

                    var tries = 0
                    val childViews: Iterable<View> = TreeIterables.breadthFirstViewTraversal(view)

                    // Look for the match in the tree of childviews
                    childViews.forEach {
                        tries++
                        if (matcher.matches(it)) {
                            // found the view
                            return
                        }
                    }

                    throw NoMatchingViewException.Builder()
                        .withRootView(view)
                        .withViewMatcher(matcher)
                        .build()
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

BaseRobot.kt调用该searchFor()方法,检查是否返回了匹配器。如果没有返回匹配项,它会休眠一点,然后获取一个新的根来匹配,直到它尝试了 X 次,然后它会抛出异常并且测试失败。对什么是“机器人”感到困惑?看看杰克沃顿关于机器人模式的精彩演讲。它与页面对象模型模式非常相似

open class BaseRobot {

    fun doOnView(matcher: Matcher<View>, vararg actions: ViewAction) {
        actions.forEach {
            waitForView(matcher).perform(it)
        }
    }

    fun assertOnView(matcher: Matcher<View>, vararg assertions: ViewAssertion) {
        assertions.forEach {
            waitForView(matcher).check(it)
        }
    }

    /**
     * Perform action of implicitly waiting for a certain view.
     * This differs from EspressoExtensions.searchFor in that,
     * upon failure to locate an element, it will fetch a new root view
     * in which to traverse searching for our @param match
     *
     * @param viewMatcher ViewMatcher used to find our view
     */
    fun waitForView(
        viewMatcher: Matcher<View>,
        waitMillis: Int = 5000,
        waitMillisPerTry: Long = 100
    ): ViewInteraction {

        // Derive the max tries
        val maxTries = waitMillis / waitMillisPerTry.toInt()

        var tries = 0

        for (i in 0..maxTries)
            try {
                // Track the amount of times we've tried
                tries++

                // Search the root for the view
                onView(isRoot()).perform(searchFor(viewMatcher))

                // If we're here, we found our view. Now return it
                return onView(viewMatcher)

            } catch (e: Exception) {

                if (tries == maxTries) {
                    throw e
                }
                sleep(waitMillisPerTry)
            }

        throw Exception("Error finding a view matching $viewMatcher")
    }
}
Run Code Online (Sandbox Code Playgroud)

使用它

// Click on element withId
BaseRobot().doOnView(withId(R.id.viewIWantToFind, click())

// Assert element withId is displayed
BaseRobot().assertOnView(withId(R.id.viewIWantToFind, matches(isDisplayed()))
Run Code Online (Sandbox Code Playgroud)

我知道IdlingResource是谷歌在 Espresso 测试中所宣扬的处理异步事件的方法,但它通常要求您在应用程序代码中嵌入测试特定代码(即钩子)以同步测试。这对我来说似乎很奇怪,并且在一个拥有成熟应用程序和多个开发人员每天提交代码的团队中工作,似乎为了测试而改造应用程序中无处不在的空闲资源似乎需要很多额外的工作。就个人而言,我更喜欢将应用程序和测试代码尽可能分开。/结束咆哮

  • 绝对令人难以置信的解决方案,以及我在这个网站上看到的一些最好的代码。谢谢! (2认同)
  • 我发现这个解决方案的一个问题是,即使可见性设置为不可见,“matcher.matches(it)”也会返回 true,但是单击将失败,因为它不可见。简单的解决方案就是使用 `matcher.matches(child).and(child.isVisible)` (2认同)

jep*_*bio 6

您可以创建一个空闲资源,也可以使用自定义ViewAction

/**
 * Perform action of waiting for a specific view id.
 * @param viewId The id of the view to wait for.
 * @param millis The timeout of until when to wait for.
 */
public static ViewAction waitId(final int viewId, final long millis) {
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isRoot();
        }

        @Override
        public String getDescription() {
            return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
        }

        @Override
        public void perform(final UiController uiController, final View view) {
            uiController.loopMainThreadUntilIdle();
            final long startTime = System.currentTimeMillis();
            final long endTime = startTime + millis;
            final Matcher<View> viewMatcher = withId(viewId);

            do {
                for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
                    // found view with required ID
                    if (viewMatcher.matches(child)) {
                        return;
                    }
                }

                uiController.loopMainThreadForAtLeast(50);
            }
            while (System.currentTimeMillis() < endTime);

            // timeout happens
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(new TimeoutException())
                    .build();
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式使用它:

onView(isRoot()).perform(waitId(R.id.theIdToWaitFor, 5000));
Run Code Online (Sandbox Code Playgroud)

更改theIdToWaitFor特定ID,并在必要时更新5秒(5000毫秒)的超时时间。