/* * 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; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.swipeRight; import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView; import static android.support.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.app.Activity; import android.content.Intent; import android.graphics.RectF; import android.support.annotation.IdRes; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.ViewAction; import android.support.test.espresso.action.GeneralLocation; import android.support.test.espresso.action.GeneralSwipeAction; import android.support.test.espresso.action.Press; import android.support.test.espresso.action.Swipe; import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.filters.SmallTest; 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.util.ArcSwipe; import android.support.wear.widget.util.WakeLockRule; import android.view.View; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class SwipeDismissFrameLayoutTest { private static final long MAX_WAIT_TIME = 4000; //ms private final SwipeDismissFrameLayout.Callback mDismissCallback = new DismissCallback(); @Rule public final WakeLockRule wakeLock = new WakeLockRule(); @Rule public final ActivityTestRule activityRule = new ActivityTestRule<>( SwipeDismissFrameLayoutTestActivity.class, true, /** initial touch mode */ false /** launchActivity */ ); private int mLayoutWidth; private int mLayoutHeight; @Test @SmallTest public void testCanScrollHorizontally() { // GIVEN a freshly setup SwipeDismissFrameLayout setUpSimpleLayout(); Activity activity = activityRule.getActivity(); SwipeDismissFrameLayout testLayout = (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); // WHEN we check that the layout is horizontally scrollable from left to right. // THEN the layout is found to be horizontally swipeable from left to right. assertTrue(testLayout.canScrollHorizontally(-20)); // AND the layout is found to NOT be horizontally swipeable from right to left. assertFalse(testLayout.canScrollHorizontally(20)); // WHEN we switch off the swipe-to-dismiss functionality for the layout testLayout.setSwipeable(false); // THEN the layout is found NOT to be horizontally swipeable from left to right. assertFalse(testLayout.canScrollHorizontally(-20)); // AND the layout is found to NOT be horizontally swipeable from right to left. assertFalse(testLayout.canScrollHorizontally(20)); } @Test @SmallTest public void canScrollHorizontallyShouldBeFalseWhenInvisible() { // GIVEN a freshly setup SwipeDismissFrameLayout setUpSimpleLayout(); Activity activity = activityRule.getActivity(); final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); // GIVEN the layout is invisible // Note: We have to run this on the main thread, because of thread checks in View.java. InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { testLayout.setVisibility(View.INVISIBLE); } }); // WHEN we check that the layout is horizontally scrollable // THEN the layout is found to be NOT horizontally swipeable from left to right. assertFalse(testLayout.canScrollHorizontally(-20)); // AND the layout is found to NOT be horizontally swipeable from right to left. assertFalse(testLayout.canScrollHorizontally(20)); } @Test @SmallTest public void canScrollHorizontallyShouldBeFalseWhenGone() { // GIVEN a freshly setup SwipeDismissFrameLayout setUpSimpleLayout(); Activity activity = activityRule.getActivity(); final SwipeDismissFrameLayout testLayout = (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); // GIVEN the layout is gone // Note: We have to run this on the main thread, because of thread checks in View.java. InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { testLayout.setVisibility(View.GONE); } }); // WHEN we check that the layout is horizontally scrollable // THEN the layout is found to be NOT horizontally swipeable from left to right. assertFalse(testLayout.canScrollHorizontally(-20)); // AND the layout is found to NOT be horizontally swipeable from right to left. assertFalse(testLayout.canScrollHorizontally(20)); } @Test @SmallTest public void testSwipeDismissEnabledByDefault() { // GIVEN a freshly setup SwipeDismissFrameLayout setUpSimpleLayout(); Activity activity = activityRule.getActivity(); SwipeDismissFrameLayout testLayout = (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); // WHEN we check that the layout is dismissible // THEN the layout is find to be dismissible assertTrue(testLayout.isSwipeable()); } @Test @SmallTest public void testSwipeDismissesViewIfEnabled() { // GIVEN a freshly setup SwipeDismissFrameLayout setUpSimpleLayout(); // WHEN we perform a swipe to dismiss onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); // AND hidden assertHidden(R.id.swipe_dismiss_root); } @Test @SmallTest public void testSwipeDoesNotDismissViewIfDisabled() { // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. setUpSimpleLayout(); Activity activity = activityRule.getActivity(); SwipeDismissFrameLayout testLayout = (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); testLayout.setSwipeable(false); // WHEN we perform a swipe to dismiss onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); // THEN the layout is not hidden assertNotHidden(R.id.swipe_dismiss_root); } @Test @SmallTest public void testAddRemoveCallback() { // GIVEN a freshly setup SwipeDismissFrameLayout setUpSimpleLayout(); Activity activity = activityRule.getActivity(); SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); // WHEN we remove the swipe callback testLayout.removeCallback(mDismissCallback); onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); // THEN the layout is not hidden assertNotHidden(R.id.swipe_dismiss_root); } @Test @SmallTest public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable { // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. setUpSwipeDismissWithHorizontalRecyclerView(); activityRule.runOnUiThread(new Runnable() { @Override public void run() { Activity activity = activityRule.getActivity(); RecyclerView testLayout = activity.findViewById(R.id.recycler_container); // Scroll to a position from which the child is scrollable. testLayout.scrollToPosition(50); } }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); // WHEN we perform a swipe to dismiss from the center of the screen. onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); // THEN the layout is not hidden assertNotHidden(R.id.swipe_dismiss_root); } @Test @SmallTest public void testEdgeSwipeDoesDismissViewIfScrollable() { // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. setUpSwipeDismissWithHorizontalRecyclerView(); // WHEN we perform a swipe to dismiss from the left edge of the screen. onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); // THEN the layout is hidden assertHidden(R.id.swipe_dismiss_root); } @Test @SmallTest public void testSwipeDoesNotDismissViewIfStartsInWrongPosition() { // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an // inner circle. setUpSwipeableRegion(); // WHEN we perform a swipe to dismiss from the left edge of the screen. onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); // THEN the layout is not not hidden assertNotHidden(R.id.swipe_dismiss_root); } @Test @SmallTest public void testSwipeDoesDismissViewIfStartsInRightPosition() { // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an // inner circle. setUpSwipeableRegion(); // WHEN we perform a swipe to dismiss from the center of the screen. onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); // THEN the layout is hidden assertHidden(R.id.swipe_dismiss_root); } /** @Test public void testSwipeInPreferenceFragmentAndNavDrawer() { // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an inner // circle. setUpPreferenceFragmentAndNavDrawer(); // WHEN we perform a swipe to dismiss from the center of the screen to the bottom. onView(withId(R.id.drawer_layout)).perform(swipeBottomFromCenter()); // THEN the navigation drawer is shown. assertPeeking(R.id.top_drawer); }*/ @Test @SmallTest public void testArcSwipeDoesNotTriggerDismiss() throws Throwable { // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content setUpSwipeDismissWithVerticalRecyclerView(); int center = mLayoutHeight / 2; int halfBound = mLayoutWidth / 2; RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound); // WHEN the view is scrolled on an arc from top to bottom. onView(withId(R.id.swipe_dismiss_root)).perform(swipeTopFromBottomOnArc(bounds)); // THEN the layout is not dismissed and not hidden. assertNotHidden(R.id.swipe_dismiss_root); // AND the content view is scrolled. assertScrolledY(R.id.recycler_container); } /** * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with * a single static child. */ private void setUpSimpleLayout() { activityRule.launchActivity( new Intent() .putExtra( LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.swipe_dismiss_layout_testcase_1)); setDismissCallback(); } /** * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link * android.support.v7.widget.RecyclerView} as a child, ready to accept an adapter. */ private void setUpSwipeDismissWithHorizontalRecyclerView() { Intent launchIntent = new Intent(); launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.swipe_dismiss_layout_testcase_2); launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true); activityRule.launchActivity(launchIntent); setDismissCallback(); } /** * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link * WearableRecyclerView} as a child, ready to accept an adapter. */ private void setUpSwipeDismissWithVerticalRecyclerView() { Intent launchIntent = new Intent(); launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.swipe_dismiss_layout_testcase_2); launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false); activityRule.launchActivity(launchIntent); setDismissCallback(); } /** * Sets up a {@link SwipeDismissFrameLayout} in which only a certain region is allowed to react * to swipe-dismiss gestures. */ private void setUpSwipeableRegion() { activityRule.launchActivity( new Intent() .putExtra( LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.swipe_dismiss_layout_testcase_1)); setCallback( new DismissCallback() { @Override public boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float x, float y) { float normalizedX = x - mLayoutWidth / 2; float normalizedY = y - mLayoutWidth / 2; float squareX = normalizedX * normalizedX; float squareY = normalizedY * normalizedY; // 30 is an arbitrary number limiting the circle. return Math.sqrt(squareX + squareY) < (mLayoutWidth / 2 - 30); } }); } /** * Sets up a more involved test case where the layout consists of a * {@code WearableNavigationDrawer} and a * {@code android.support.wear.internal.view.SwipeDismissPreferenceFragment} */ /* private void setUpPreferenceFragmentAndNavDrawer() { activityRule.launchActivity( new Intent() .putExtra( LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.swipe_dismiss_layout_testcase_3)); Activity activity = activityRule.getActivity(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { WearableNavigationDrawer wearableNavigationDrawer = (WearableNavigationDrawer) activity.findViewById(R.id.top_drawer); wearableNavigationDrawer.setAdapter( new WearableNavigationDrawer.WearableNavigationDrawerAdapter() { @Override public String getItemText(int pos) { return "test"; } @Override public Drawable getItemDrawable(int pos) { return null; } @Override public void onItemSelected(int pos) { return; } @Override public int getCount() { return 3; } }); }); }*/ private void setDismissCallback() { setCallback(mDismissCallback); } private void setCallback(SwipeDismissFrameLayout.Callback callback) { Activity activity = activityRule.getActivity(); SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); mLayoutWidth = testLayout.getWidth(); mLayoutHeight = testLayout.getHeight(); testLayout.addCallback(callback); } /** * private static void assertPeeking(@IdRes int layoutId) { * onView(withId(layoutId)) * .perform( * waitForMatchingView( * allOf(withId(layoutId), isOpened(true)), MAX_WAIT_TIME)); * } */ private static void assertHidden(@IdRes int layoutId) { onView(withId(layoutId)) .perform( waitForMatchingView( allOf(withId(layoutId), withEffectiveVisibility(ViewMatchers.Visibility.GONE)), MAX_WAIT_TIME)); } private static void assertNotHidden(@IdRes int layoutId) { onView(withId(layoutId)) .perform( waitForMatchingView( allOf(withId(layoutId), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)), MAX_WAIT_TIME)); } private static void assertScrolledY(@IdRes int layoutId) { onView(withId(layoutId)) .perform( waitForMatchingView( allOf(withId(layoutId), withPositiveVerticalScrollOffset()), MAX_WAIT_TIME)); } private static ViewAction swipeRightFromCenter() { return new GeneralSwipeAction( Swipe.SLOW, GeneralLocation.CENTER, GeneralLocation.CENTER_RIGHT, Press.FINGER); } private static ViewAction swipeRightFromLeftEdge() { return new GeneralSwipeAction( Swipe.SLOW, GeneralLocation.CENTER_LEFT, GeneralLocation.CENTER_RIGHT, Press.FINGER); } private static ViewAction swipeTopFromBottomOnArc(RectF bounds) { return new GeneralSwipeAction( new ArcSwipe(ArcSwipe.Gesture.SLOW_ANTICLOCKWISE, bounds), GeneralLocation.BOTTOM_CENTER, GeneralLocation.TOP_CENTER, Press.FINGER); } /** Helper class hiding the view after a successful swipe-to-dismiss. */ private static class DismissCallback extends SwipeDismissFrameLayout.Callback { @Override public void onDismissed(SwipeDismissFrameLayout layout) { layout.setVisibility(View.GONE); } } }