1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.wear.widget; 18 19import static android.support.test.espresso.Espresso.onView; 20import static android.support.test.espresso.action.ViewActions.swipeRight; 21import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; 22import static android.support.test.espresso.matcher.ViewMatchers.withId; 23import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView; 24import static android.support.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset; 25 26import static org.hamcrest.Matchers.allOf; 27import static org.junit.Assert.assertFalse; 28import static org.junit.Assert.assertTrue; 29 30import android.app.Activity; 31import android.content.Intent; 32import android.graphics.RectF; 33import android.support.annotation.IdRes; 34import android.support.test.InstrumentationRegistry; 35import android.support.test.espresso.ViewAction; 36import android.support.test.espresso.action.GeneralLocation; 37import android.support.test.espresso.action.GeneralSwipeAction; 38import android.support.test.espresso.action.Press; 39import android.support.test.espresso.action.Swipe; 40import android.support.test.espresso.matcher.ViewMatchers; 41import android.support.test.filters.SmallTest; 42import android.support.test.rule.ActivityTestRule; 43import android.support.test.runner.AndroidJUnit4; 44import android.support.v7.widget.RecyclerView; 45import android.support.wear.test.R; 46import android.support.wear.widget.util.ArcSwipe; 47import android.support.wear.widget.util.WakeLockRule; 48import android.view.View; 49 50import org.junit.Rule; 51import org.junit.Test; 52import org.junit.runner.RunWith; 53 54@RunWith(AndroidJUnit4.class) 55public class SwipeDismissFrameLayoutTest { 56 57 private static final long MAX_WAIT_TIME = 4000; //ms 58 private final SwipeDismissFrameLayout.Callback mDismissCallback = new DismissCallback(); 59 60 @Rule 61 public final WakeLockRule wakeLock = new WakeLockRule(); 62 63 @Rule 64 public final ActivityTestRule<SwipeDismissFrameLayoutTestActivity> activityRule = 65 new ActivityTestRule<>( 66 SwipeDismissFrameLayoutTestActivity.class, 67 true, /** initial touch mode */ 68 false /** launchActivity */ 69 ); 70 71 private int mLayoutWidth; 72 private int mLayoutHeight; 73 74 @Test 75 @SmallTest 76 public void testCanScrollHorizontally() { 77 // GIVEN a freshly setup SwipeDismissFrameLayout 78 setUpSimpleLayout(); 79 Activity activity = activityRule.getActivity(); 80 SwipeDismissFrameLayout testLayout = 81 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 82 // WHEN we check that the layout is horizontally scrollable from left to right. 83 // THEN the layout is found to be horizontally swipeable from left to right. 84 assertTrue(testLayout.canScrollHorizontally(-20)); 85 // AND the layout is found to NOT be horizontally swipeable from right to left. 86 assertFalse(testLayout.canScrollHorizontally(20)); 87 88 // WHEN we switch off the swipe-to-dismiss functionality for the layout 89 testLayout.setSwipeable(false); 90 // THEN the layout is found NOT to be horizontally swipeable from left to right. 91 assertFalse(testLayout.canScrollHorizontally(-20)); 92 // AND the layout is found to NOT be horizontally swipeable from right to left. 93 assertFalse(testLayout.canScrollHorizontally(20)); 94 } 95 96 @Test 97 @SmallTest 98 public void canScrollHorizontallyShouldBeFalseWhenInvisible() { 99 // GIVEN a freshly setup SwipeDismissFrameLayout 100 setUpSimpleLayout(); 101 Activity activity = activityRule.getActivity(); 102 final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); 103 // GIVEN the layout is invisible 104 // Note: We have to run this on the main thread, because of thread checks in View.java. 105 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 106 @Override 107 public void run() { 108 testLayout.setVisibility(View.INVISIBLE); 109 } 110 }); 111 // WHEN we check that the layout is horizontally scrollable 112 // THEN the layout is found to be NOT horizontally swipeable from left to right. 113 assertFalse(testLayout.canScrollHorizontally(-20)); 114 // AND the layout is found to NOT be horizontally swipeable from right to left. 115 assertFalse(testLayout.canScrollHorizontally(20)); 116 } 117 118 @Test 119 @SmallTest 120 public void canScrollHorizontallyShouldBeFalseWhenGone() { 121 // GIVEN a freshly setup SwipeDismissFrameLayout 122 setUpSimpleLayout(); 123 Activity activity = activityRule.getActivity(); 124 final SwipeDismissFrameLayout testLayout = 125 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 126 // GIVEN the layout is gone 127 // Note: We have to run this on the main thread, because of thread checks in View.java. 128 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 129 @Override 130 public void run() { 131 testLayout.setVisibility(View.GONE); 132 } 133 }); 134 // WHEN we check that the layout is horizontally scrollable 135 // THEN the layout is found to be NOT horizontally swipeable from left to right. 136 assertFalse(testLayout.canScrollHorizontally(-20)); 137 // AND the layout is found to NOT be horizontally swipeable from right to left. 138 assertFalse(testLayout.canScrollHorizontally(20)); 139 } 140 141 @Test 142 @SmallTest 143 public void testSwipeDismissEnabledByDefault() { 144 // GIVEN a freshly setup SwipeDismissFrameLayout 145 setUpSimpleLayout(); 146 Activity activity = activityRule.getActivity(); 147 SwipeDismissFrameLayout testLayout = 148 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 149 // WHEN we check that the layout is dismissible 150 // THEN the layout is find to be dismissible 151 assertTrue(testLayout.isSwipeable()); 152 } 153 154 @Test 155 @SmallTest 156 public void testSwipeDismissesViewIfEnabled() { 157 // GIVEN a freshly setup SwipeDismissFrameLayout 158 setUpSimpleLayout(); 159 // WHEN we perform a swipe to dismiss 160 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); 161 // AND hidden 162 assertHidden(R.id.swipe_dismiss_root); 163 } 164 165 @Test 166 @SmallTest 167 public void testSwipeDoesNotDismissViewIfDisabled() { 168 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. 169 setUpSimpleLayout(); 170 Activity activity = activityRule.getActivity(); 171 SwipeDismissFrameLayout testLayout = 172 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 173 testLayout.setSwipeable(false); 174 // WHEN we perform a swipe to dismiss 175 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); 176 // THEN the layout is not hidden 177 assertNotHidden(R.id.swipe_dismiss_root); 178 } 179 180 @Test 181 @SmallTest 182 public void testAddRemoveCallback() { 183 // GIVEN a freshly setup SwipeDismissFrameLayout 184 setUpSimpleLayout(); 185 Activity activity = activityRule.getActivity(); 186 SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); 187 // WHEN we remove the swipe callback 188 testLayout.removeCallback(mDismissCallback); 189 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); 190 // THEN the layout is not hidden 191 assertNotHidden(R.id.swipe_dismiss_root); 192 } 193 194 @Test 195 @SmallTest 196 public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable { 197 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. 198 setUpSwipeDismissWithHorizontalRecyclerView(); 199 activityRule.runOnUiThread(new Runnable() { 200 @Override 201 public void run() { 202 Activity activity = activityRule.getActivity(); 203 RecyclerView testLayout = activity.findViewById(R.id.recycler_container); 204 // Scroll to a position from which the child is scrollable. 205 testLayout.scrollToPosition(50); 206 } 207 }); 208 209 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 210 // WHEN we perform a swipe to dismiss from the center of the screen. 211 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); 212 // THEN the layout is not hidden 213 assertNotHidden(R.id.swipe_dismiss_root); 214 } 215 216 217 @Test 218 @SmallTest 219 public void testEdgeSwipeDoesDismissViewIfScrollable() { 220 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. 221 setUpSwipeDismissWithHorizontalRecyclerView(); 222 // WHEN we perform a swipe to dismiss from the left edge of the screen. 223 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); 224 // THEN the layout is hidden 225 assertHidden(R.id.swipe_dismiss_root); 226 } 227 228 @Test 229 @SmallTest 230 public void testSwipeDoesNotDismissViewIfStartsInWrongPosition() { 231 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an 232 // inner circle. 233 setUpSwipeableRegion(); 234 // WHEN we perform a swipe to dismiss from the left edge of the screen. 235 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); 236 // THEN the layout is not not hidden 237 assertNotHidden(R.id.swipe_dismiss_root); 238 } 239 240 @Test 241 @SmallTest 242 public void testSwipeDoesDismissViewIfStartsInRightPosition() { 243 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an 244 // inner circle. 245 setUpSwipeableRegion(); 246 // WHEN we perform a swipe to dismiss from the center of the screen. 247 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); 248 // THEN the layout is hidden 249 assertHidden(R.id.swipe_dismiss_root); 250 } 251 252 /** 253 @Test public void testSwipeInPreferenceFragmentAndNavDrawer() { 254 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an inner 255 // circle. 256 setUpPreferenceFragmentAndNavDrawer(); 257 // WHEN we perform a swipe to dismiss from the center of the screen to the bottom. 258 onView(withId(R.id.drawer_layout)).perform(swipeBottomFromCenter()); 259 // THEN the navigation drawer is shown. 260 assertPeeking(R.id.top_drawer); 261 }*/ 262 263 @Test 264 @SmallTest 265 public void testArcSwipeDoesNotTriggerDismiss() throws Throwable { 266 // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content 267 setUpSwipeDismissWithVerticalRecyclerView(); 268 int center = mLayoutHeight / 2; 269 int halfBound = mLayoutWidth / 2; 270 RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound); 271 // WHEN the view is scrolled on an arc from top to bottom. 272 onView(withId(R.id.swipe_dismiss_root)).perform(swipeTopFromBottomOnArc(bounds)); 273 // THEN the layout is not dismissed and not hidden. 274 assertNotHidden(R.id.swipe_dismiss_root); 275 // AND the content view is scrolled. 276 assertScrolledY(R.id.recycler_container); 277 } 278 279 /** 280 * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with 281 * a single static child. 282 */ 283 private void setUpSimpleLayout() { 284 activityRule.launchActivity( 285 new Intent() 286 .putExtra( 287 LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 288 R.layout.swipe_dismiss_layout_testcase_1)); 289 setDismissCallback(); 290 } 291 292 293 /** 294 * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable 295 * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link 296 * android.support.v7.widget.RecyclerView} as a child, ready to accept an adapter. 297 */ 298 private void setUpSwipeDismissWithHorizontalRecyclerView() { 299 Intent launchIntent = new Intent(); 300 launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 301 R.layout.swipe_dismiss_layout_testcase_2); 302 launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true); 303 activityRule.launchActivity(launchIntent); 304 setDismissCallback(); 305 } 306 307 /** 308 * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable 309 * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link 310 * WearableRecyclerView} as a child, ready to accept an adapter. 311 */ 312 private void setUpSwipeDismissWithVerticalRecyclerView() { 313 Intent launchIntent = new Intent(); 314 launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 315 R.layout.swipe_dismiss_layout_testcase_2); 316 launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false); 317 activityRule.launchActivity(launchIntent); 318 setDismissCallback(); 319 } 320 321 /** 322 * Sets up a {@link SwipeDismissFrameLayout} in which only a certain region is allowed to react 323 * to swipe-dismiss gestures. 324 */ 325 private void setUpSwipeableRegion() { 326 activityRule.launchActivity( 327 new Intent() 328 .putExtra( 329 LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 330 R.layout.swipe_dismiss_layout_testcase_1)); 331 setCallback( 332 new DismissCallback() { 333 @Override 334 public boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float x, 335 float y) { 336 float normalizedX = x - mLayoutWidth / 2; 337 float normalizedY = y - mLayoutWidth / 2; 338 float squareX = normalizedX * normalizedX; 339 float squareY = normalizedY * normalizedY; 340 // 30 is an arbitrary number limiting the circle. 341 return Math.sqrt(squareX + squareY) < (mLayoutWidth / 2 - 30); 342 } 343 }); 344 } 345 346 /** 347 * Sets up a more involved test case where the layout consists of a 348 * {@code WearableNavigationDrawer} and a 349 * {@code android.support.wear.internal.view.SwipeDismissPreferenceFragment} 350 */ 351 /* 352 private void setUpPreferenceFragmentAndNavDrawer() { 353 activityRule.launchActivity( 354 new Intent() 355 .putExtra( 356 LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 357 R.layout.swipe_dismiss_layout_testcase_3)); 358 Activity activity = activityRule.getActivity(); 359 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 360 WearableNavigationDrawer wearableNavigationDrawer = 361 (WearableNavigationDrawer) activity.findViewById(R.id.top_drawer); 362 wearableNavigationDrawer.setAdapter( 363 new WearableNavigationDrawer.WearableNavigationDrawerAdapter() { 364 @Override 365 public String getItemText(int pos) { 366 return "test"; 367 } 368 369 @Override 370 public Drawable getItemDrawable(int pos) { 371 return null; 372 } 373 374 @Override 375 public void onItemSelected(int pos) { 376 return; 377 } 378 379 @Override 380 public int getCount() { 381 return 3; 382 } 383 }); 384 }); 385 }*/ 386 private void setDismissCallback() { 387 setCallback(mDismissCallback); 388 } 389 390 private void setCallback(SwipeDismissFrameLayout.Callback callback) { 391 Activity activity = activityRule.getActivity(); 392 SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); 393 mLayoutWidth = testLayout.getWidth(); 394 mLayoutHeight = testLayout.getHeight(); 395 testLayout.addCallback(callback); 396 } 397 398 /** 399 * private static void assertPeeking(@IdRes int layoutId) { 400 * onView(withId(layoutId)) 401 * .perform( 402 * waitForMatchingView( 403 * allOf(withId(layoutId), isOpened(true)), MAX_WAIT_TIME)); 404 * } 405 */ 406 407 private static void assertHidden(@IdRes int layoutId) { 408 onView(withId(layoutId)) 409 .perform( 410 waitForMatchingView( 411 allOf(withId(layoutId), 412 withEffectiveVisibility(ViewMatchers.Visibility.GONE)), 413 MAX_WAIT_TIME)); 414 } 415 416 private static void assertNotHidden(@IdRes int layoutId) { 417 onView(withId(layoutId)) 418 .perform( 419 waitForMatchingView( 420 allOf(withId(layoutId), 421 withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)), 422 MAX_WAIT_TIME)); 423 } 424 425 private static void assertScrolledY(@IdRes int layoutId) { 426 onView(withId(layoutId)) 427 .perform( 428 waitForMatchingView( 429 allOf(withId(layoutId), withPositiveVerticalScrollOffset()), 430 MAX_WAIT_TIME)); 431 } 432 433 private static ViewAction swipeRightFromCenter() { 434 return new GeneralSwipeAction( 435 Swipe.SLOW, GeneralLocation.CENTER, GeneralLocation.CENTER_RIGHT, Press.FINGER); 436 } 437 438 private static ViewAction swipeRightFromLeftEdge() { 439 return new GeneralSwipeAction( 440 Swipe.SLOW, GeneralLocation.CENTER_LEFT, GeneralLocation.CENTER_RIGHT, 441 Press.FINGER); 442 } 443 444 private static ViewAction swipeTopFromBottomOnArc(RectF bounds) { 445 return new GeneralSwipeAction( 446 new ArcSwipe(ArcSwipe.Gesture.SLOW_ANTICLOCKWISE, bounds), 447 GeneralLocation.BOTTOM_CENTER, 448 GeneralLocation.TOP_CENTER, 449 Press.FINGER); 450 } 451 452 /** Helper class hiding the view after a successful swipe-to-dismiss. */ 453 private static class DismissCallback extends SwipeDismissFrameLayout.Callback { 454 455 @Override 456 public void onDismissed(SwipeDismissFrameLayout layout) { 457 layout.setVisibility(View.GONE); 458 } 459 } 460} 461