blob: d1e44e671c24b3b332d593b8e12f817f4a9d4d5a [file] [log] [blame]
/*
* Copyright (C) 2017 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 android.support.wear.widget.drawer;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeDown;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.Intent;
import android.support.test.espresso.PerformException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.util.HumanReadables;
import android.support.test.espresso.util.TreeIterables;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.widget.RecyclerView;
import android.support.wear.test.R;
import android.support.wear.widget.drawer.DrawerTestActivity.DrawerStyle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
/**
* Espresso tests for {@link WearableDrawerLayout}.
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class WearableDrawerLayoutEspressoTest {
private static final long MAX_WAIT_MS = 4000;
@Rule public final ActivityTestRule<DrawerTestActivity> activityRule =
new ActivityTestRule<>(
DrawerTestActivity.class, true /* touchMode */, false /* initialLaunch*/);
private final Intent mSinglePageIntent =
new DrawerTestActivity.Builder().setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
.build();
@Mock WearableNavigationDrawerView.OnItemSelectedListener mNavDrawerItemSelectedListener;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void openingNavigationDrawerDoesNotCloseActionDrawer() {
// GIVEN a drawer layout with a peeking action and navigation drawer
activityRule.launchActivity(mSinglePageIntent);
DrawerTestActivity activity = activityRule.getActivity();
WearableDrawerView actionDrawer =
(WearableDrawerView) activity.findViewById(R.id.action_drawer);
WearableDrawerView navigationDrawer =
(WearableDrawerView) activity.findViewById(R.id.navigation_drawer);
assertTrue(actionDrawer.isPeeking());
assertTrue(navigationDrawer.isPeeking());
// WHEN the top drawer is opened
openDrawer(navigationDrawer);
onView(withId(R.id.navigation_drawer))
.perform(
waitForMatchingView(
allOf(withId(R.id.navigation_drawer), isOpened(true)),
MAX_WAIT_MS));
// THEN the action drawer should still be peeking
assertTrue(actionDrawer.isPeeking());
}
@Test
public void swipingDownNavigationDrawerDoesNotCloseActionDrawer() {
// GIVEN a drawer layout with a peeking action and navigation drawer
activityRule.launchActivity(mSinglePageIntent);
onView(withId(R.id.action_drawer)).check(matches(isPeeking()));
onView(withId(R.id.navigation_drawer)).check(matches(isPeeking()));
// WHEN the top drawer is opened by swiping down
onView(withId(R.id.drawer_layout)).perform(swipeDown());
onView(withId(R.id.navigation_drawer))
.perform(
waitForMatchingView(
allOf(withId(R.id.navigation_drawer), isOpened(true)),
MAX_WAIT_MS));
// THEN the action drawer should still be peeking
onView(withId(R.id.action_drawer)).check(matches(isPeeking()));
}
@Test
public void firstNavDrawerItemShouldBeSelectedInitially() {
// GIVEN a top drawer
// WHEN it is first opened
activityRule.launchActivity(mSinglePageIntent);
onView(withId(R.id.drawer_layout)).perform(swipeDown());
onView(withId(R.id.navigation_drawer))
.perform(
waitForMatchingView(
allOf(withId(R.id.navigation_drawer), isOpened(true)),
MAX_WAIT_MS));
// THEN the text should display "0".
onView(withId(R.id.ws_nav_drawer_text)).check(matches(withText("0")));
}
@Test
public void selectingNavItemChangesTextAndClosedDrawer() {
// GIVEN an open top drawer
activityRule.launchActivity(mSinglePageIntent);
onView(withId(R.id.drawer_layout)).perform(swipeDown());
onView(withId(R.id.navigation_drawer))
.perform(
waitForMatchingView(
allOf(withId(R.id.navigation_drawer), isOpened(true)),
MAX_WAIT_MS));
// WHEN the second item is selected
onView(withId(R.id.ws_nav_drawer_icon_1)).perform(click());
// THEN the text should display "1" and it should close.
onView(withId(R.id.ws_nav_drawer_text))
.perform(
waitForMatchingView(
allOf(withId(R.id.ws_nav_drawer_text), withText("1")),
MAX_WAIT_MS));
onView(withId(R.id.navigation_drawer))
.perform(
waitForMatchingView(
allOf(withId(R.id.navigation_drawer), isClosed(true)),
MAX_WAIT_MS));
}
@Test
public void programmaticallySelectingNavItemChangesTextInSinglePage() {
// GIVEN an open top drawer
activityRule.launchActivity(new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
.openTopDrawerInOnCreate()
.build());
final WearableNavigationDrawerView navDrawer =
activityRule.getActivity().findViewById(R.id.navigation_drawer);
navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener);
// WHEN the second item is selected programmatically
selectNavItem(navDrawer, 1);
// THEN the text should display "1" and the listener should be notified.
onView(withId(R.id.ws_nav_drawer_text))
.check(matches(withText("1")));
verify(mNavDrawerItemSelectedListener).onItemSelected(1);
}
@Test
public void programmaticallySelectingNavItemChangesTextInMultiPage() {
// GIVEN an open top drawer
activityRule.launchActivity(new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE)
.openTopDrawerInOnCreate()
.build());
final WearableNavigationDrawerView navDrawer =
activityRule.getActivity().findViewById(R.id.navigation_drawer);
navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener);
// WHEN the second item is selected programmatically
selectNavItem(navDrawer, 1);
// THEN the text should display "1" and the listener should be notified.
onView(allOf(withId(R.id.ws_navigation_drawer_item_text), isDisplayed()))
.check(matches(withText("1")));
verify(mNavDrawerItemSelectedListener).onItemSelected(1);
}
@Test
public void navDrawerShouldOpenWhenCalledInOnCreate() {
// GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate
// WHEN it is launched
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
.openTopDrawerInOnCreate()
.build());
// THEN the nav drawer should be open
onView(withId(R.id.navigation_drawer)).check(matches(isOpened(true)));
}
@Test
public void actionDrawerShouldOpenWhenCalledInOnCreate() {
// GIVEN an activity with only an action drawer which is opened in onCreate
// WHEN it is launched
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
.openBottomDrawerInOnCreate()
.build());
// THEN the action drawer should be open
onView(withId(R.id.action_drawer)).check(matches(isOpened(true)));
}
@Test
public void navDrawerShouldOpenWhenCalledInOnCreateAndThenCloseWhenRequested() {
// GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate, then closes it
// WHEN it is launched
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
.openTopDrawerInOnCreate()
.closeFirstDrawerOpened()
.build());
// THEN the nav drawer should be open and then close
onView(withId(R.id.navigation_drawer))
.check(matches(isOpened(true)))
.perform(
waitForMatchingView(
allOf(withId(R.id.navigation_drawer), isClosed(true)),
MAX_WAIT_MS));
}
@Test
public void openedNavDrawerShouldPreventSwipeToClose() {
// GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
.openTopDrawerInOnCreate()
.build());
// THEN the view should prevent swipe to close
onView(withId(R.id.navigation_drawer)).check(matches(not(allowsSwipeToClose())));
}
@Test
public void closedNavDrawerShouldNotPreventSwipeToClose() {
// GIVEN an activity which doesn't start with the nav drawer open
activityRule.launchActivity(mSinglePageIntent);
// THEN the view should allow swipe to close
onView(withId(R.id.navigation_drawer)).check(matches(allowsSwipeToClose()));
}
@Test
public void scrolledDownActionDrawerCanScrollUpWhenReOpened() {
// GIVEN a freshly launched activity
activityRule.launchActivity(mSinglePageIntent);
WearableActionDrawerView actionDrawer =
(WearableActionDrawerView) activityRule.getActivity()
.findViewById(R.id.action_drawer);
RecyclerView recyclerView = (RecyclerView) actionDrawer.getDrawerContent();
// WHEN the action drawer is opened and scrolled to the last item (Item 6)
openDrawer(actionDrawer);
scrollToPosition(recyclerView, 5);
onView(withId(R.id.action_drawer))
.perform(
waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)),
MAX_WAIT_MS))
.perform(
waitForMatchingView(allOf(withText("Item 6"), isCompletelyDisplayed()),
MAX_WAIT_MS));
// and then it is peeked
peekDrawer(actionDrawer);
onView(withId(R.id.action_drawer))
.perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()),
MAX_WAIT_MS));
// and re-opened
openDrawer(actionDrawer);
onView(withId(R.id.action_drawer))
.perform(
waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)),
MAX_WAIT_MS));
// THEN item 6 should be visible, but swiping down should scroll up, not close the drawer.
onView(withText("Item 6")).check(matches(isDisplayed()));
onView(withId(R.id.action_drawer)).perform(swipeDown()).check(matches(isOpened(true)));
}
@Test
public void actionDrawerPeekIconShouldNotBeNull() {
// GIVEN a drawer layout with a peeking action drawer whose menu is initialized in XML
activityRule.launchActivity(mSinglePageIntent);
DrawerTestActivity activity = activityRule.getActivity();
ImageView peekIconView =
(ImageView) activity
.findViewById(R.id.ws_action_drawer_peek_action_icon);
// THEN its peek icon should not be null
assertNotNull(peekIconView.getDrawable());
}
@Test
public void tappingActionDrawerPeekIconShouldTriggerFirstAction() {
// GIVEN a drawer layout with a peeking action drawer, title, and mock click listener
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
.build());
WearableActionDrawerView actionDrawer =
(WearableActionDrawerView) activityRule.getActivity()
.findViewById(R.id.action_drawer);
OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class);
actionDrawer.setOnMenuItemClickListener(mockClickListener);
// WHEN the action drawer peek view is tapped
onView(withId(R.id.ws_drawer_view_peek_container))
.perform(waitForMatchingView(
allOf(
withId(R.id.ws_drawer_view_peek_container),
isCompletelyDisplayed()),
MAX_WAIT_MS))
.perform(click());
// THEN its click listener should be notified
verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
}
@Test
public void tappingActionDrawerPeekIconShouldTriggerFirstActionAfterItWasOpened() {
// GIVEN a drawer layout with an open action drawer with a title, and mock click listener
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
.openBottomDrawerInOnCreate()
.build());
WearableActionDrawerView actionDrawer =
(WearableActionDrawerView) activityRule.getActivity()
.findViewById(R.id.action_drawer);
OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class);
actionDrawer.setOnMenuItemClickListener(mockClickListener);
// WHEN the action drawer is closed to its peek state and then tapped
peekDrawer(actionDrawer);
onView(withId(R.id.action_drawer))
.perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()),
MAX_WAIT_MS));
actionDrawer.getPeekContainer().callOnClick();
// THEN its click listener should be notified
verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
}
@Test
public void changingActionDrawerItemShouldUpdateView() {
// GIVEN a drawer layout with an open action drawer
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
.openBottomDrawerInOnCreate()
.build());
WearableActionDrawerView actionDrawer =
activityRule.getActivity().findViewById(R.id.action_drawer);
final MenuItem secondItem = actionDrawer.getMenu().getItem(1);
// WHEN its second item is changed
actionDrawer.post(new Runnable() {
@Override
public void run() {
secondItem.setTitle("Modified item");
}
});
// THEN the new item should be displayed
onView(withText("Modified item")).check(matches(isDisplayed()));
}
@Test
public void removingActionDrawerItemShouldUpdateView() {
// GIVEN a drawer layout with an open action drawer
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
.openBottomDrawerInOnCreate()
.build());
final WearableActionDrawerView actionDrawer =
activityRule.getActivity().findViewById(R.id.action_drawer);
MenuItem secondItem = actionDrawer.getMenu().getItem(1);
final int itemId = secondItem.getItemId();
final String title = secondItem.getTitle().toString();
final int initialSize = getChildByType(actionDrawer, RecyclerView.class)
.getAdapter()
.getItemCount();
// WHEN its second item is removed
actionDrawer.post(new Runnable() {
@Override
public void run() {
actionDrawer.getMenu().removeItem(itemId);
}
});
// THEN it should decrease by 1 in size and it should no longer contain the item's text
onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class)))
.perform(waitForRecyclerToBeSize(initialSize - 1, MAX_WAIT_MS))
.perform(waitForMatchingView(recyclerWithoutText(is(title)), MAX_WAIT_MS));
}
@Test
public void addingActionDrawerItemShouldUpdateView() {
// GIVEN a drawer layout with an open action drawer
activityRule.launchActivity(
new DrawerTestActivity.Builder()
.setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
.openBottomDrawerInOnCreate()
.build());
final WearableActionDrawerView actionDrawer =
activityRule.getActivity().findViewById(R.id.action_drawer);
RecyclerView recycler = getChildByType(actionDrawer, RecyclerView.class);
final RecyclerView.LayoutManager layoutManager = recycler.getLayoutManager();
final int initialSize = recycler.getAdapter().getItemCount();
// WHEN an item is added and the view is scrolled down (to make sure the view is created)
actionDrawer.post(new Runnable() {
@Override
public void run() {
actionDrawer.getMenu().add(0, 42, Menu.NONE, "New Item");
layoutManager.scrollToPosition(initialSize);
}
});
// THEN it should decrease by 1 in size and the there should be a view with the item's text
onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class)))
.perform(waitForRecyclerToBeSize(initialSize + 1, MAX_WAIT_MS))
.perform(waitForMatchingView(withText("New Item"), MAX_WAIT_MS));
}
private void scrollToPosition(final RecyclerView recyclerView, final int position) {
recyclerView.post(new Runnable() {
@Override
public void run() {
recyclerView.scrollToPosition(position);
}
});
}
private void selectNavItem(final WearableNavigationDrawerView navDrawer, final int index) {
navDrawer.post(new Runnable() {
@Override
public void run() {
navDrawer.setCurrentItem(index, false);
}
});
}
private void peekDrawer(final WearableDrawerView drawer) {
drawer.post(new Runnable() {
@Override
public void run() {
drawer.getController().peekDrawer();
}
});
}
private void openDrawer(final WearableDrawerView drawer) {
drawer.post(new Runnable() {
@Override
public void run() {
drawer.getController().openDrawer();
}
});
}
private static TypeSafeMatcher<View> isOpened(final boolean isOpened) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("is opened == " + isOpened);
}
@Override
public boolean matchesSafely(View view) {
return ((WearableDrawerView) view).isOpened() == isOpened;
}
};
}
private static TypeSafeMatcher<View> isClosed(final boolean isClosed) {
return new TypeSafeMatcher<View>() {
@Override
protected boolean matchesSafely(View view) {
WearableDrawerView drawer = (WearableDrawerView) view;
return drawer.isClosed() == isClosed;
}
@Override
public void describeTo(Description description) {
description.appendText("is closed");
}
};
}
private TypeSafeMatcher<View> isPeeking() {
return new TypeSafeMatcher<View>() {
@Override
protected boolean matchesSafely(View view) {
WearableDrawerView drawer = (WearableDrawerView) view;
return drawer.isPeeking();
}
@Override
public void describeTo(Description description) {
description.appendText("is peeking");
}
};
}
private TypeSafeMatcher<View> allowsSwipeToClose() {
return new TypeSafeMatcher<View>() {
@Override
protected boolean matchesSafely(View view) {
return !view.canScrollHorizontally(-2) && !view.canScrollHorizontally(2);
}
@Override
public void describeTo(Description description) {
description.appendText("can be swiped closed");
}
};
}
/**
* Returns a {@link TypeSafeMatcher} that returns {@code true} when the {@link RecyclerView}
* does not contain a {@link TextView} with text matched by {@code textMatcher}.
*/
private TypeSafeMatcher<View> recyclerWithoutText(final Matcher<String> textMatcher) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Without recycler text ");
textMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
if (!(view instanceof RecyclerView)) {
return false;
}
RecyclerView recycler = ((RecyclerView) view);
if (recycler.isAnimating()) {
// While the RecyclerView is animating, it will return null ViewHolders and we
// won't be able to tell whether the item has been removed or not.
return false;
}
for (int i = 0; i < recycler.getAdapter().getItemCount(); i++) {
RecyclerView.ViewHolder holder = recycler.findViewHolderForAdapterPosition(i);
if (holder != null) {
TextView text = getChildByType(holder.itemView, TextView.class);
if (text != null && textMatcher.matches(text.getText())) {
return false;
}
}
}
return true;
}
};
}
/**
* Waits for the {@link RecyclerView} to contain {@code targetCount} items, up to {@code millis}
* milliseconds. Throws exception if the time limit is reached before reaching the desired
* number of items.
*/
public ViewAction waitForRecyclerToBeSize(final int targetCount, final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(RecyclerView.class);
}
@Override
public String getDescription() {
return "Waiting for recycler to be size=" + targetCount;
}
@Override
public void perform(UiController uiController, View view) {
if (!(view instanceof RecyclerView)) {
return;
}
RecyclerView recycler = (RecyclerView) view;
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
do {
if (recycler.getAdapter().getItemCount() == targetCount) {
return;
}
uiController.loopMainThreadForAtLeast(100); // at least 3 frames
} while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
/**
* Returns the first child of {@code root} to be an instance of class {@code T}, or {@code null}
* if none were found.
*/
@Nullable
private <T> T getChildByType(View root, Class<T> classOfChildToFind) {
for (View child : TreeIterables.breadthFirstViewTraversal(root)) {
if (classOfChildToFind.isInstance(child)) {
return (T) child;
}
}
return null;
}
}