Peter Z.
Peter Z.

Reputation: 188

Android Espresso DrawerActions.open() doesnt work

I want to open my drawer layout with Espresso and test whether it is open. But as simple code as:

@Test
    fun isDrawerOpening(){
        onView(withId(R.id.drawerLayoutMain))
            .check(matches(isDisplayed()))
            .check(matches(DrawerMatchers.isClosed()))
            .perform(DrawerActions.open())
            .check(matches(DrawerMatchers.isOpen()))
    }

doesn't want to work. swipeRight() the same. I get 'Test Failed':

[Robolectric] com.zywczas.bestonscreen.views.MainActivityTest.isDrawerOpening: sdk=28; resources=BINARY
Called loadFromPath(/system/framework/framework-res.apk, true); mode=binary sdk=28

androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'is drawer open' doesn't match the selected view.
Expected: is drawer open
     Got: "DrawerLayout{id=2131230868, res-name=drawerLayoutMain, visibility=VISIBLE, width=320, height=470, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=androidx.coordinatorlayout.widget.CoordinatorLayout$LayoutParams@22a54b8d, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=56.0, child-count=2}"

<Click to see difference>


    at java.lang.Thread.getStackTrace(Thread.java:1559)
    at androidx.test.espresso.base.DefaultFailureHandler.getUserFriendlyError(DefaultFailureHandler.java:16)
    at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:36)
    at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:103)
    at androidx.test.espresso.ViewInteraction.check(ViewInteraction.java:31)
    at com.zywczas.bestonscreen.views.MainActivityTest.isDrawerOpening(MainActivityTest.kt:56)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:54)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:546)
    at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:252)
    at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
Caused by: junit.framework.AssertionFailedError: 'is drawer open' doesn't match the selected view.
Expected: is drawer open
     Got: "DrawerLayout{id=2131230868, res-name=drawerLayoutMain, visibility=VISIBLE, width=320, height=470, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=androidx.coordinatorlayout.widget.CoordinatorLayout$LayoutParams@22a54b8d, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=56.0, child-count=2}"

    at androidx.test.espresso.matcher.ViewMatchers.assertThat(ViewMatchers.java:17)
    at androidx.test.espresso.assertion.ViewAssertions$MatchesViewAssertion.check(ViewAssertions.java:15)
    at androidx.test.espresso.ViewInteraction$SingleExecutionViewAssertion.check(ViewInteraction.java:10)
    at androidx.test.espresso.ViewInteraction$2.call(ViewInteraction.java:11)
    at androidx.test.espresso.ViewInteraction$2.call(ViewInteraction.java:2)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at android.os.Handler.$$robo$$android_os_Handler$handleCallback(Handler.java:873)
    at android.os.Handler.handleCallback(Handler.java)
    at android.os.Handler.$$robo$$android_os_Handler$dispatchMessage(Handler.java:99)
    at android.os.Handler.dispatchMessage(Handler.java)
    at org.robolectric.shadows.ShadowPausedLooper$IdlingRunnable.run(ShadowPausedLooper.java:308)
    at org.robolectric.shadows.ShadowPausedLooper.executeOnLooper(ShadowPausedLooper.java:273)
    at org.robolectric.shadows.ShadowPausedLooper.idle(ShadowPausedLooper.java:85)
    at org.robolectric.android.internal.LocalControlledLooper.drainMainThreadUntilIdle(LocalControlledLooper.java:15)
    at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:99)
    ... 19 more

How to make it open by Espresso? I use Robolectric and Navigation. I want to test my main activity and clicking nav buttons in the drawer. It works when I use the app. But cannot achieve the same with Espresso.

My dependencies:

testImplementation 'androidx.test.espresso:espresso-core:3.3.0'
testImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
testImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
testImplementation 'org.robolectric:robolectric:4.3'

Layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:scrollbarSize="20dp">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbarMovies"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="false">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbarMovies"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:minHeight="?attr/actionBarSize"
            android:theme="@style/ToolbarTheme"
            app:buttonGravity="center_vertical"
            app:layout_scrollFlags="scroll|enterAlways" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/drawerLayoutMain"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="false"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".views.MainActivity"
        tools:openDrawer="start">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/navHostFragmentView"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:navGraph="@navigation/main_nav_graph" />

        <com.google.android.material.navigation.NavigationView
            android:id="@+id/navDrawer"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:fitsSystemWindows="true"
            app:itemIconPadding="20dp"
            app:itemIconTint="@android:color/holo_red_dark"
            app:itemTextAppearance="@style/TextAppearance.AppCompat.Body1"
            app:itemTextColor="@android:color/black"
            app:menu="@menu/menu_navigation">

        </com.google.android.material.navigation.NavigationView>

    </androidx.drawerlayout.widget.DrawerLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>

Activity:

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var moviesFragmentsFactory: MoviesFragmentsFactory
    private val navHostFragment by lazy {
        supportFragmentManager.findFragmentById(R.id.navHostFragmentView) as NavHostFragment }
    private val navController by lazy { navHostFragment.navController }
    private val appBarConfiguration by lazy {
        AppBarConfiguration(setOf(R.id.destinationDb, R.id.destinationApi), drawerLayoutMain) }

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        supportFragmentManager.fragmentFactory = moviesFragmentsFactory
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        navDrawer.setupWithNavController(navController)
        toolbarMovies.setupWithNavController(navController, appBarConfiguration)
        toolbarMovies.setNavigationOnClickListener(openCloseDrawerNavClick)
    }

    private val openCloseDrawerNavClick = View.OnClickListener {
        if (drawerLayoutMain.isDrawerOpen(GravityCompat.START)){
            drawerLayoutMain.closeDrawer(GravityCompat.START)
        } else {
            navController.navigateUp(appBarConfiguration)
        }
    }

}

Upvotes: 1

Views: 799

Answers (1)

Aaron
Aaron

Reputation: 3894

Your test looks good. You should only use DrawerActions.open for best practices, unless you have other goals. I think you're probably facing a race condition between DrawerActions.open() and DrawerMatchers.isOpen(). If that's the case you'll have to wait until the drawers are completely open, you can either use Thread.sleep() for quick solution, or IdlingResource for elegancy:

fun waitUntilOpen(drawerGravity: Int = GravityCompat.START): ViewAction = object : ViewAction {

    private val callback = DrawerOpenCallback()

    override fun getConstraints(): Matcher<View> = isAssignableFrom(DrawerLayout::class.java)

    override fun getDescription(): String = "wait for drawer to open"

    override fun perform(uiController: UiController, view: View) {
        if (!(view as DrawerLayout).isDrawerOpen(drawerGravity)) {
            IdlingRegistry.getInstance().register(callback)
            view.addDrawerListener(callback)
            uiController.loopMainThreadUntilIdle()
            IdlingRegistry.getInstance().unregister(callback)
            view.removeDrawerListener(callback)
        }
    }
}

private class DrawerOpenCallback : IdlingResource, DrawerLayout.DrawerListener {

    private lateinit var callback: IdlingResource.ResourceCallback
    private var opened = false

    override fun getName() = "Drawer open callback"

    override fun isIdleNow() = opened

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
        this.callback = callback
    }

    override fun onDrawerStateChanged(newState: Int) {
    }

    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
    }

    override fun onDrawerClosed(drawerView: View) {
    }

    override fun onDrawerOpened(drawerView: View) {
        opened = true
        callback.onTransitionToIdle()
    }
}

Then perform waitUntilOpen after DrawerActions.open():

@Test fun isDrawerOpening(){
    onView(withId(R.id.drawerLayoutMain))
        .check(matches(isDisplayed()))
        .check(matches(DrawerMatchers.isClosed()))
        .perform(DrawerActions.open(), waitUntilOpen())
        .check(matches(DrawerMatchers.isOpen()))
}

Upvotes: 3

Related Questions