DrawerActions.java
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.espresso.contrib;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
import static androidx.test.espresso.contrib.DrawerMatchers.isOpen;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import android.view.View;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import java.util.concurrent.atomic.AtomicInteger;
import org.hamcrest.Matcher;
/**
* Espresso actions for using a {@link DrawerLayout}.
*
* @see <a href="http://developer.android.com/design/patterns/navigation-drawer.html">Navigation
* drawer design guide</a>
*/
public final class DrawerActions {
private static final AtomicInteger nextId = new AtomicInteger();
private static final int TAG = getTag();
private static int getTag() {
try {
return R.id.androidx_test_espresso_contrib_drawer_layout_tag;
} catch (NoClassDefFoundError e) {
// If the caller of this class is compiled as the src of an android_test rule, it may not
// generate the R file, which leads to a NoClassDefFoundError at runtime. In this case, fall
// back to a randomly chosen value that hopefully won't conflict with any other tags.
return 0xFF3DE250;
}
}
private DrawerActions() {
// forbid instantiation
}
private abstract static class DrawerAction implements ViewAction {
@Override
public final Matcher<View> getConstraints() {
return isAssignableFrom(DrawerLayout.class);
}
@Override
public final void perform(UiController uiController, View view) {
DrawerLayout drawer = (DrawerLayout) view;
if (!checkAction().matches(drawer)) {
return;
}
Object tag = drawer.getTag(TAG);
IdlingDrawerListener idlingListener;
if (tag instanceof IdlingDrawerListener) {
idlingListener = (IdlingDrawerListener) tag;
} else {
idlingListener = new IdlingDrawerListener();
drawer.setTag(TAG, idlingListener);
drawer.addDrawerListener(idlingListener);
IdlingRegistry.getInstance().register(idlingListener);
}
performAction(uiController, drawer);
uiController.loopMainThreadUntilIdle();
IdlingRegistry.getInstance().unregister(idlingListener);
drawer.removeDrawerListener(idlingListener);
drawer.setTag(TAG, null);
}
protected abstract Matcher<View> checkAction();
protected abstract void performAction(UiController uiController, DrawerLayout view);
}
/**
* @deprecated Use {@link #open()} with {@code perform} after matching a view. This method will be
* removed in the next release.
*/
@Deprecated
public static void openDrawer(int drawerLayoutId) {
openDrawer(drawerLayoutId, GravityCompat.START);
}
/**
* @deprecated Use {@link #open(int)} with {@code perform} after matching a view. This method will
* be removed in the next release.
*/
@Deprecated
public static void openDrawer(int drawerLayoutId, int gravity) {
onView(withId(drawerLayoutId)).perform(open(gravity));
}
/**
* Creates an action which opens the {@link DrawerLayout} drawer with gravity START. This method
* blocks until the drawer is fully open. No operation if the drawer is already open.
*/
// TODO alias to openDrawer before 3.0 and deprecate this method.
public static ViewAction open() {
return open(GravityCompat.START);
}
/**
* Creates an action which opens the {@link DrawerLayout} drawer with the gravity. This method
* blocks until the drawer is fully open. No operation if the drawer is already open.
*/
// TODO alias to openDrawer before 3.0 and deprecate this method.
public static ViewAction open(final int gravity) {
return new DrawerAction() {
@Override
public String getDescription() {
return "open drawer with gravity " + gravity;
}
@Override
protected Matcher<View> checkAction() {
return isClosed(gravity);
}
@Override
protected void performAction(UiController uiController, DrawerLayout view) {
view.openDrawer(gravity);
}
};
}
/**
* @deprecated Use {@link #close()} with {@code perform} after matching a view. This method will
* be removed in the next release.
*/
@Deprecated
public static void closeDrawer(int drawerLayoutId) {
closeDrawer(drawerLayoutId, GravityCompat.START);
}
/**
* @deprecated Use {@link #open(int)} with {@code perform} after matching a view. This method will
* be removed in the next release.
*/
@Deprecated
public static void closeDrawer(int drawerLayoutId, int gravity) {
onView(withId(drawerLayoutId)).perform(close(gravity));
}
/**
* Creates an action which closes the {@link DrawerLayout} with gravity START. This method blocks
* until the drawer is fully closed. No operation if the drawer is already closed.
*/
// TODO alias to closeDrawer before 3.0 and deprecate this method.
public static ViewAction close() {
return close(GravityCompat.START);
}
/**
* Creates an action which closes the {@link DrawerLayout} with the gravity. This method blocks
* until the drawer is fully closed. No operation if the drawer is already closed.
*/
// TODO alias to closeDrawer before 3.0 and deprecate this method.
public static ViewAction close(final int gravity) {
return new DrawerAction() {
@Override
public String getDescription() {
return "close drawer with gravity " + gravity;
}
@Override
protected Matcher<View> checkAction() {
return isOpen(gravity);
}
@Override
protected void performAction(UiController uiController, DrawerLayout view) {
view.closeDrawer(gravity);
uiController.loopMainThreadUntilIdle();
// If still open wait some more...
if (view.isDrawerVisible(gravity)) {
uiController.loopMainThreadForAtLeast(300);
}
}
};
}
/** Drawer listener that functions as an {@link IdlingResource} for Espresso. */
private static final class IdlingDrawerListener extends SimpleDrawerListener
implements IdlingResource {
private final int id = nextId.getAndIncrement();
private ResourceCallback callback;
// Idle state is only accessible from main thread.
private boolean idle = true;
@Override
public void onDrawerStateChanged(int newState) {
if (newState == DrawerLayout.STATE_IDLE) {
idle = true;
if (callback != null) {
callback.onTransitionToIdle();
}
} else {
idle = false;
}
}
@Override
public String getName() {
return "IdlingDrawerListener::" + id;
}
@Override
public boolean isIdleNow() {
return idle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
}
}