Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 1 | /* |
Jakub Gielzak | bf61842 | 2019-01-21 15:58:47 +0000 | [diff] [blame] | 2 | * Copyright 2018 The Android Open Source Project |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 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 | |
| 17 | package androidx.viewpager2.widget |
| 18 | |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 19 | import android.content.Intent |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 20 | import android.os.Build |
Jelle Fresen | fa6c931 | 2019-05-01 18:06:23 +0100 | [diff] [blame] | 21 | import android.util.Log |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 22 | import android.view.View |
| 23 | import android.view.View.OVER_SCROLL_NEVER |
Jakub Gielzak | 866fb5a | 2019-07-11 17:03:27 +0100 | [diff] [blame] | 24 | import android.view.ViewConfiguration |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 25 | import android.view.accessibility.AccessibilityNodeInfo |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 26 | import androidx.core.view.ViewCompat |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 27 | import androidx.core.view.accessibility.AccessibilityNodeInfoCompat |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 28 | import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD |
| 29 | import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 30 | import androidx.recyclerview.widget.LinearLayoutManager |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 31 | import androidx.recyclerview.widget.RecyclerView |
Alan Viverette | badf2f8 | 2018-12-18 12:14:10 -0500 | [diff] [blame] | 32 | import androidx.test.core.app.ApplicationProvider |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 33 | import androidx.test.espresso.Espresso.onView |
| 34 | import androidx.test.espresso.action.CoordinatesProvider |
| 35 | import androidx.test.espresso.action.GeneralLocation |
| 36 | import androidx.test.espresso.action.GeneralSwipeAction |
| 37 | import androidx.test.espresso.action.Press |
| 38 | import androidx.test.espresso.action.Swipe |
| 39 | import androidx.test.espresso.action.ViewActions.actionWithAssertions |
| 40 | import androidx.test.espresso.assertion.ViewAssertions.matches |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 41 | import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom |
Jelle Fresen | 450e0c3 | 2019-08-13 10:18:41 +0100 | [diff] [blame^] | 42 | import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 43 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed |
| 44 | import androidx.test.espresso.matcher.ViewMatchers.withId |
| 45 | import androidx.test.espresso.matcher.ViewMatchers.withText |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 46 | import androidx.test.rule.ActivityTestRule |
Cătălin Tudor | 212b927 | 2019-05-09 16:38:28 +0100 | [diff] [blame] | 47 | import androidx.testutils.LocaleTestUtils |
Ian Lake | c9b7a7a | 2019-05-16 14:58:30 -0700 | [diff] [blame] | 48 | import androidx.testutils.recreate |
Jelle Fresen | 51a3aa6 | 2019-07-31 09:45:59 +0100 | [diff] [blame] | 49 | import androidx.testutils.waitForExecution |
Jakub Gielzak | acc3021 | 2018-11-23 15:10:33 +0000 | [diff] [blame] | 50 | import androidx.viewpager2.adapter.FragmentStateAdapter |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 51 | import androidx.viewpager2.test.R |
Jakub Gielzak | b60fc00 | 2018-10-19 16:36:59 +0100 | [diff] [blame] | 52 | import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 53 | import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING |
Jakub Gielzak | b60fc00 | 2018-10-19 16:36:59 +0100 | [diff] [blame] | 54 | import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 55 | import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_SETTLING |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 56 | import androidx.viewpager2.widget.swipe.FragmentAdapter |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 57 | import androidx.viewpager2.widget.swipe.PageSwiper |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 58 | import androidx.viewpager2.widget.swipe.PageSwiperEspresso |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 59 | import androidx.viewpager2.widget.swipe.PageSwiperFakeDrag |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 60 | import androidx.viewpager2.widget.swipe.PageSwiperManual |
Jakub Gielzak | d111249 | 2019-02-19 12:30:28 +0000 | [diff] [blame] | 61 | import androidx.viewpager2.widget.swipe.SelfChecking |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 62 | import androidx.viewpager2.widget.swipe.TestActivity |
| 63 | import androidx.viewpager2.widget.swipe.ViewAdapter |
Jelle Fresen | 5e10216 | 2019-05-15 15:35:13 +0100 | [diff] [blame] | 64 | import androidx.viewpager2.widget.swipe.WaitForInjectMotionEventsAction.Companion.waitForInjectMotionEvents |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 65 | import org.hamcrest.CoreMatchers.equalTo |
| 66 | import org.hamcrest.Matcher |
| 67 | import org.hamcrest.Matchers.allOf |
| 68 | import org.hamcrest.Matchers.greaterThanOrEqualTo |
| 69 | import org.hamcrest.Matchers.lessThan |
| 70 | import org.hamcrest.Matchers.lessThanOrEqualTo |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 71 | import org.junit.After |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 72 | import org.junit.Assert.assertThat |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 73 | import org.junit.Before |
Jelle Fresen | 9684c93 | 2018-12-04 15:52:08 +0000 | [diff] [blame] | 74 | import org.junit.Rule |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 75 | import java.util.concurrent.CountDownLatch |
Jelle Fresen | 2436f95 | 2018-08-14 17:16:49 +0100 | [diff] [blame] | 76 | import java.util.concurrent.TimeUnit |
Jelle Fresen | 6164139 | 2018-10-02 12:07:57 +0100 | [diff] [blame] | 77 | import kotlin.math.abs |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 78 | |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 79 | open class BaseTest { |
Jelle Fresen | fa6c931 | 2019-05-01 18:06:23 +0100 | [diff] [blame] | 80 | companion object { |
| 81 | const val TAG = "VP2_TESTS" |
Jakub Gielzak | cef25ef | 2019-07-15 17:52:09 +0100 | [diff] [blame] | 82 | const val ACTION_ID_PAGE_LEFT = android.R.id.accessibilityActionPageLeft |
| 83 | const val ACTION_ID_PAGE_RIGHT = android.R.id.accessibilityActionPageRight |
| 84 | const val ACTION_ID_PAGE_UP = android.R.id.accessibilityActionPageUp |
| 85 | const val ACTION_ID_PAGE_DOWN = android.R.id.accessibilityActionPageDown |
Jelle Fresen | fa6c931 | 2019-05-01 18:06:23 +0100 | [diff] [blame] | 86 | } |
| 87 | |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 88 | lateinit var localeUtil: LocaleTestUtils |
| 89 | |
Jelle Fresen | 9684c93 | 2018-12-04 15:52:08 +0000 | [diff] [blame] | 90 | @get:Rule |
| 91 | val activityTestRule = ActivityTestRule<TestActivity>(TestActivity::class.java, false, false) |
| 92 | |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 93 | @Before |
| 94 | open fun setUp() { |
Jelle Fresen | d92b2c4 | 2018-12-21 12:28:32 +0000 | [diff] [blame] | 95 | localeUtil = LocaleTestUtils( |
Cătălin Tudor | 212b927 | 2019-05-09 16:38:28 +0100 | [diff] [blame] | 96 | ApplicationProvider.getApplicationContext() as android.content.Context |
| 97 | ) |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 98 | // Ensure a predictable test environment by explicitly setting a locale |
| 99 | localeUtil.setLocale(LocaleTestUtils.DEFAULT_TEST_LANGUAGE) |
| 100 | } |
| 101 | |
| 102 | @After |
| 103 | open fun tearDown() { |
| 104 | localeUtil.resetLocale() |
| 105 | } |
| 106 | |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 107 | fun setUpTest(@ViewPager2.Orientation orientation: Int): Context { |
Jelle Fresen | 9684c93 | 2018-12-04 15:52:08 +0000 | [diff] [blame] | 108 | val intent = Intent() |
| 109 | if (localeUtil.isLocaleChangedAndLock()) { |
| 110 | intent.putExtra(TestActivity.EXTRA_LANGUAGE, localeUtil.getLocale().toString()) |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 111 | } |
Jelle Fresen | 9684c93 | 2018-12-04 15:52:08 +0000 | [diff] [blame] | 112 | activityTestRule.launchActivity(intent) |
Jelle Fresen | 5e10216 | 2019-05-15 15:35:13 +0100 | [diff] [blame] | 113 | onView(withId(R.id.view_pager)).perform(waitForInjectMotionEvents()) |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 114 | |
| 115 | val viewPager: ViewPager2 = activityTestRule.activity.findViewById(R.id.view_pager) |
| 116 | activityTestRule.runOnUiThread { viewPager.orientation = orientation } |
| 117 | onView(withId(R.id.view_pager)).check(matches(isDisplayed())) |
| 118 | |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 119 | // animations getting in the way on API < 16 |
| 120 | if (Build.VERSION.SDK_INT < 16) { |
Jelle Fresen | 6c1744d | 2019-04-05 11:26:12 +0100 | [diff] [blame] | 121 | viewPager.recyclerView.overScrollMode = OVER_SCROLL_NEVER |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 122 | } |
| 123 | |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 124 | return Context(activityTestRule) |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 125 | } |
| 126 | |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 127 | data class Context(val activityTestRule: ActivityTestRule<TestActivity>) { |
Jelle Fresen | 489dd2d | 2018-11-16 12:32:10 +0000 | [diff] [blame] | 128 | fun recreateActivity( |
| 129 | adapterProvider: AdapterProvider, |
| 130 | onCreateCallback: ((ViewPager2) -> Unit) = { } |
| 131 | ) { |
Jelle Fresen | b35fd56 | 2019-04-08 12:30:32 +0100 | [diff] [blame] | 132 | val orientation = viewPager.orientation |
| 133 | val isUserInputEnabled = viewPager.isUserInputEnabled |
Jelle Fresen | 489dd2d | 2018-11-16 12:32:10 +0000 | [diff] [blame] | 134 | TestActivity.onCreateCallback = { activity -> |
| 135 | val viewPager = activity.findViewById<ViewPager2>(R.id.view_pager) |
Jelle Fresen | b35fd56 | 2019-04-08 12:30:32 +0100 | [diff] [blame] | 136 | viewPager.orientation = orientation |
| 137 | viewPager.isUserInputEnabled = isUserInputEnabled |
Jelle Fresen | 489dd2d | 2018-11-16 12:32:10 +0000 | [diff] [blame] | 138 | viewPager.adapter = adapterProvider(activity) |
| 139 | onCreateCallback(viewPager) |
| 140 | } |
Ian Lake | c9b7a7a | 2019-05-16 14:58:30 -0700 | [diff] [blame] | 141 | activity = activityTestRule.recreate() |
Jelle Fresen | 489dd2d | 2018-11-16 12:32:10 +0000 | [diff] [blame] | 142 | TestActivity.onCreateCallback = { } |
Jelle Fresen | 5e10216 | 2019-05-15 15:35:13 +0100 | [diff] [blame] | 143 | onView(withId(R.id.view_pager)).perform(waitForInjectMotionEvents()) |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 144 | } |
| 145 | |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 146 | var activity: TestActivity = activityTestRule.activity |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 147 | private set(value) { |
| 148 | field = value |
| 149 | } |
| 150 | |
Jelle Fresen | 3a6d49d | 2019-07-30 14:22:07 +0100 | [diff] [blame] | 151 | fun runOnUiThreadSync(f: () -> Unit) { |
Jelle Fresen | 0057dd5 | 2019-07-29 18:00:39 +0100 | [diff] [blame] | 152 | var thrownError: Throwable? = null |
| 153 | activityTestRule.runOnUiThread { |
| 154 | try { |
| 155 | f() |
| 156 | } catch (t: Throwable) { |
| 157 | thrownError = t |
| 158 | } |
| 159 | } |
| 160 | val caughtError = thrownError |
| 161 | if (caughtError != null) { |
| 162 | throw caughtError |
| 163 | } |
| 164 | } |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 165 | |
| 166 | val viewPager: ViewPager2 get() = activity.findViewById(R.id.view_pager) |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 167 | |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 168 | fun peekForward() { |
Jakub Gielzak | 866fb5a | 2019-07-11 17:03:27 +0100 | [diff] [blame] | 169 | peek(adjustForRtl(adjustForTouchSlop(-50f))) |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 170 | } |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 171 | |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 172 | fun peekBackward() { |
Jakub Gielzak | 866fb5a | 2019-07-11 17:03:27 +0100 | [diff] [blame] | 173 | peek(adjustForRtl(adjustForTouchSlop(50f))) |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 174 | } |
| 175 | |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 176 | enum class SwipeMethod { |
| 177 | ESPRESSO, |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 178 | MANUAL, |
| 179 | FAKE_DRAG |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 180 | } |
| 181 | |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 182 | fun swipe(currentPageIx: Int, nextPageIx: Int, method: SwipeMethod = SwipeMethod.ESPRESSO) { |
Jakub Gielzak | 4368b72 | 2019-01-07 14:49:53 +0000 | [diff] [blame] | 183 | val lastPageIx = viewPager.adapter!!.itemCount - 1 |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 184 | |
| 185 | if (nextPageIx > lastPageIx) { |
| 186 | throw IllegalArgumentException("Invalid next page: beyond last page.") |
| 187 | } |
| 188 | |
| 189 | if (currentPageIx == nextPageIx) { // dedicated for testing edge behaviour |
| 190 | if (nextPageIx == 0) { |
| 191 | swipeBackward(method) // bounce off the "left" edge |
| 192 | return |
| 193 | } |
| 194 | if (nextPageIx == lastPageIx) { // bounce off the "right" edge |
| 195 | swipeForward(method) |
| 196 | return |
| 197 | } |
| 198 | throw IllegalArgumentException( |
| 199 | "Invalid sequence. Not on an edge, and current page = next page." |
| 200 | ) |
| 201 | } |
| 202 | |
| 203 | if (Math.abs(nextPageIx - currentPageIx) > 1) { |
| 204 | throw IllegalArgumentException( |
| 205 | "Specified next page not adjacent to the current page." |
| 206 | ) |
| 207 | } |
| 208 | |
| 209 | if (nextPageIx > currentPageIx) { |
| 210 | swipeForward(method) |
| 211 | } else { |
| 212 | swipeBackward(method) |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | fun swipeForward(method: SwipeMethod = SwipeMethod.ESPRESSO) { |
| 217 | swiper(method).swipeNext() |
| 218 | } |
| 219 | |
| 220 | fun swipeBackward(method: SwipeMethod = SwipeMethod.ESPRESSO) { |
| 221 | swiper(method).swipePrevious() |
| 222 | } |
| 223 | |
| 224 | private fun swiper(method: SwipeMethod = SwipeMethod.ESPRESSO): PageSwiper { |
| 225 | return when (method) { |
Jelle Fresen | a913290 | 2019-05-02 16:35:39 +0100 | [diff] [blame] | 226 | SwipeMethod.ESPRESSO -> PageSwiperEspresso(viewPager) |
| 227 | SwipeMethod.MANUAL -> PageSwiperManual(viewPager) |
Jelle Fresen | e94be33 | 2019-04-23 17:56:35 +0100 | [diff] [blame] | 228 | SwipeMethod.FAKE_DRAG -> PageSwiperFakeDrag(viewPager) { viewPager.pageSize } |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 229 | } |
| 230 | } |
| 231 | |
Jakub Gielzak | 866fb5a | 2019-07-11 17:03:27 +0100 | [diff] [blame] | 232 | private fun adjustForTouchSlop(offset: Float): Float { |
| 233 | val touchSlop = ViewConfiguration.get(viewPager.context).scaledPagingTouchSlop |
| 234 | return when { |
| 235 | offset < 0 -> offset - touchSlop |
| 236 | offset > 0 -> offset + touchSlop |
| 237 | else -> 0f |
| 238 | } |
| 239 | } |
| 240 | |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 241 | private fun adjustForRtl(offset: Float): Float { |
Jelle Fresen | a913290 | 2019-05-02 16:35:39 +0100 | [diff] [blame] | 242 | return if (viewPager.isHorizontal && viewPager.isRtl) -offset else offset |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 243 | } |
| 244 | |
| 245 | private fun peek(offset: Float) { |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 246 | onView(allOf(isDisplayed(), isAssignableFrom(ViewPager2::class.java))) |
Jakub Gielzak | 1c9c583 | 2018-12-02 18:42:16 +0000 | [diff] [blame] | 247 | .perform( |
| 248 | actionWithAssertions( |
| 249 | GeneralSwipeAction( |
| 250 | Swipe.SLOW, GeneralLocation.CENTER, |
| 251 | CoordinatesProvider { view -> |
| 252 | val coordinates = GeneralLocation.CENTER.calculateCoordinates(view) |
| 253 | if (viewPager.orientation == ORIENTATION_HORIZONTAL) { |
| 254 | coordinates[0] += offset |
| 255 | } else { |
| 256 | coordinates[1] += offset |
| 257 | } |
| 258 | coordinates |
| 259 | }, Press.FINGER |
| 260 | ) |
| 261 | ) |
| 262 | ) |
Jelle Fresen | 9b269ad | 2018-10-15 14:13:41 +0100 | [diff] [blame] | 263 | } |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 264 | |
| 265 | fun assertPageActions() { |
Jakub Gielzak | 3702888 | 2019-06-20 16:59:17 +0100 | [diff] [blame] | 266 | if (!ViewPager2.sFeatureEnhancedA11yEnabled) { |
| 267 | return // these assertions only apply to enhanced a11y |
| 268 | } |
| 269 | |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 270 | var customActions = getActionList(viewPager) |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 271 | var currentPage = viewPager.currentItem |
| 272 | var numPages = viewPager.adapter!!.itemCount |
| 273 | var isUserInputEnabled = viewPager.isUserInputEnabled |
| 274 | var isHorizontalOrientation = viewPager.orientation == ViewPager2.ORIENTATION_HORIZONTAL |
| 275 | var isVerticalOrientation = viewPager.orientation == ViewPager2.ORIENTATION_VERTICAL |
| 276 | |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 277 | val expectPageLeftAction = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && |
| 278 | isUserInputEnabled && isHorizontalOrientation && |
Aurimas Liutikas | 95dcc6c | 2019-05-24 12:27:10 -0700 | [diff] [blame] | 279 | (if (viewPager.isRtl) currentPage < numPages - 1 else currentPage > 0) |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 280 | |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 281 | val expectPageRightAction = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && |
| 282 | isUserInputEnabled && isHorizontalOrientation && |
Aurimas Liutikas | 95dcc6c | 2019-05-24 12:27:10 -0700 | [diff] [blame] | 283 | (if (viewPager.isRtl) currentPage > 0 else currentPage < numPages - 1) |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 284 | |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 285 | val expectPageUpAction = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && |
| 286 | isUserInputEnabled && isVerticalOrientation && |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 287 | currentPage > 0 |
| 288 | |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 289 | val expectPageDownAction = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && |
| 290 | isUserInputEnabled && isVerticalOrientation && |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 291 | currentPage < numPages - 1 |
| 292 | |
sallyyuen | 84e5caf | 2019-04-24 17:59:13 -0700 | [diff] [blame] | 293 | val expectScrollBackwardAction = |
| 294 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && isUserInputEnabled && |
| 295 | currentPage > 0 |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 296 | |
sallyyuen | 84e5caf | 2019-04-24 17:59:13 -0700 | [diff] [blame] | 297 | val expectScrollForwardAction = |
| 298 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && isUserInputEnabled && |
| 299 | currentPage < numPages - 1 |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 300 | |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 301 | assertThat("Left action expected: $expectPageLeftAction", |
Jakub Gielzak | cef25ef | 2019-07-15 17:52:09 +0100 | [diff] [blame] | 302 | hasPageAction(customActions, ACTION_ID_PAGE_LEFT), |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 303 | equalTo(expectPageLeftAction) |
| 304 | ) |
| 305 | |
| 306 | assertThat("Right action expected: $expectPageRightAction", |
Jakub Gielzak | cef25ef | 2019-07-15 17:52:09 +0100 | [diff] [blame] | 307 | hasPageAction(customActions, ACTION_ID_PAGE_RIGHT), |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 308 | equalTo(expectPageRightAction) |
| 309 | ) |
| 310 | assertThat("Up action expected: $expectPageUpAction", |
Jakub Gielzak | cef25ef | 2019-07-15 17:52:09 +0100 | [diff] [blame] | 311 | hasPageAction(customActions, ACTION_ID_PAGE_UP), |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 312 | equalTo(expectPageUpAction) |
| 313 | ) |
| 314 | assertThat("Down action expected: $expectPageDownAction", |
Jakub Gielzak | cef25ef | 2019-07-15 17:52:09 +0100 | [diff] [blame] | 315 | hasPageAction(customActions, ACTION_ID_PAGE_DOWN), |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 316 | equalTo(expectPageDownAction) |
| 317 | ) |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 318 | |
| 319 | var node = AccessibilityNodeInfo.obtain() |
Jelle Fresen | 3a6d49d | 2019-07-30 14:22:07 +0100 | [diff] [blame] | 320 | runOnUiThreadSync { viewPager.onInitializeAccessibilityNodeInfo(node) } |
Aurimas Liutikas | 6b2ae5a | 2019-05-15 16:15:30 -0700 | [diff] [blame] | 321 | @Suppress("DEPRECATION") var standardActions = node.actions |
sallyyuen | 0b984ad | 2019-03-01 11:38:19 -0800 | [diff] [blame] | 322 | |
| 323 | assertThat("scroll backward action expected: $expectScrollBackwardAction", |
| 324 | hasScrollAction(standardActions, ACTION_SCROLL_BACKWARD), |
| 325 | equalTo(expectScrollBackwardAction) |
| 326 | ) |
| 327 | |
| 328 | assertThat("Scroll forward action expected: $expectScrollForwardAction", |
| 329 | hasScrollAction(standardActions, ACTION_SCROLL_FORWARD), |
| 330 | equalTo(expectScrollForwardAction) |
| 331 | ) |
| 332 | } |
| 333 | |
| 334 | private fun hasScrollAction( |
| 335 | actions: Int, |
| 336 | accessibilityActionId: Int |
| 337 | ): Boolean { |
| 338 | return actions and accessibilityActionId != 0 |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 339 | } |
| 340 | |
| 341 | private fun hasPageAction( |
| 342 | actions: List<AccessibilityNodeInfoCompat.AccessibilityActionCompat>, |
| 343 | accessibilityActionId: Int |
| 344 | ): Boolean { |
| 345 | return actions.any { it.id == accessibilityActionId } |
| 346 | } |
| 347 | |
Aurimas Liutikas | 6b2ae5a | 2019-05-15 16:15:30 -0700 | [diff] [blame] | 348 | @Suppress("UNCHECKED_CAST") |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 349 | private fun getActionList(view: View): |
| 350 | List<AccessibilityNodeInfoCompat.AccessibilityActionCompat> { |
| 351 | return view.getTag(R.id.tag_accessibility_actions) as? |
| 352 | ArrayList<AccessibilityNodeInfoCompat.AccessibilityActionCompat> ?: ArrayList() |
| 353 | } |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 354 | } |
| 355 | |
| 356 | /** |
| 357 | * Note: returned latch relies on the tested API, so it's critical to check that the final |
| 358 | * visible page is correct using [assertBasicState]. |
| 359 | */ |
| 360 | fun ViewPager2.addWaitForScrolledLatch( |
| 361 | targetPage: Int, |
| 362 | waitForIdle: Boolean = true |
| 363 | ): CountDownLatch { |
| 364 | val latch = CountDownLatch(if (waitForIdle) 2 else 1) |
| 365 | var lastScrollFired = false |
| 366 | |
Jakub Gielzak | 4368b72 | 2019-01-07 14:49:53 +0000 | [diff] [blame] | 367 | registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 368 | override fun onPageScrollStateChanged(state: Int) { |
Jakub Gielzak | b60fc00 | 2018-10-19 16:36:59 +0100 | [diff] [blame] | 369 | if (lastScrollFired && state == SCROLL_STATE_IDLE) { |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 370 | latch.countDown() |
| 371 | } |
| 372 | } |
| 373 | |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 374 | override fun onPageScrolled( |
| 375 | position: Int, |
| 376 | positionOffset: Float, |
| 377 | positionOffsetPixels: Int |
| 378 | ) { |
| 379 | if (position == targetPage && positionOffsetPixels == 0) { |
| 380 | latch.countDown() |
| 381 | lastScrollFired = true |
| 382 | } |
| 383 | } |
| 384 | }) |
| 385 | |
| 386 | return latch |
| 387 | } |
| 388 | |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 389 | fun Context.setAdapterSync(adapterProvider: AdapterProvider) { |
Jelle Fresen | a2cb4bc | 2019-03-06 18:59:54 +0000 | [diff] [blame] | 390 | lateinit var waitForRenderLatch: CountDownLatch |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 391 | |
Jelle Fresen | 3a6d49d | 2019-07-30 14:22:07 +0100 | [diff] [blame] | 392 | runOnUiThreadSync { |
Jelle Fresen | a2cb4bc | 2019-03-06 18:59:54 +0000 | [diff] [blame] | 393 | waitForRenderLatch = viewPager.addWaitForLayoutChangeLatch() |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 394 | viewPager.adapter = adapterProvider(activity) |
| 395 | } |
| 396 | |
| 397 | waitForRenderLatch.await(5, TimeUnit.SECONDS) |
Jelle Fresen | a6628e6 | 2018-11-14 19:39:35 +0000 | [diff] [blame] | 398 | |
| 399 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { |
| 400 | // Give slow devices some time to warm up, |
| 401 | // to prevent severe frame drops in the smooth scroll |
| 402 | Thread.sleep(1000) |
| 403 | } |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 404 | } |
| 405 | |
Jakub Gielzak | 1dec56a | 2018-11-22 23:28:01 +0000 | [diff] [blame] | 406 | fun ViewPager2.addWaitForLayoutChangeLatch(): CountDownLatch { |
| 407 | return CountDownLatch(1).also { |
| 408 | addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> it.countDown() } |
| 409 | } |
| 410 | } |
| 411 | |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 412 | fun ViewPager2.addWaitForIdleLatch(): CountDownLatch { |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 413 | return addWaitForStateLatch(SCROLL_STATE_IDLE) |
| 414 | } |
| 415 | |
| 416 | fun ViewPager2.addWaitForStateLatch(targetState: Int): CountDownLatch { |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 417 | val latch = CountDownLatch(1) |
| 418 | |
Jakub Gielzak | 4368b72 | 2019-01-07 14:49:53 +0000 | [diff] [blame] | 419 | registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 420 | override fun onPageScrollStateChanged(state: Int) { |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 421 | if (state == targetState) { |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 422 | latch.countDown() |
Jakub Gielzak | 4368b72 | 2019-01-07 14:49:53 +0000 | [diff] [blame] | 423 | post { unregisterOnPageChangeCallback(this) } |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 424 | } |
| 425 | } |
| 426 | }) |
| 427 | |
| 428 | return latch |
| 429 | } |
| 430 | |
Jelle Fresen | 6164139 | 2018-10-02 12:07:57 +0100 | [diff] [blame] | 431 | fun ViewPager2.addWaitForDistanceToTarget(target: Int, distance: Float): CountDownLatch { |
| 432 | val latch = CountDownLatch(1) |
| 433 | |
Jakub Gielzak | 4368b72 | 2019-01-07 14:49:53 +0000 | [diff] [blame] | 434 | registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { |
Jelle Fresen | 6164139 | 2018-10-02 12:07:57 +0100 | [diff] [blame] | 435 | override fun onPageScrolled( |
| 436 | position: Int, |
| 437 | positionOffset: Float, |
| 438 | positionOffsetPixels: Int |
| 439 | ) { |
| 440 | if (abs(target - position - positionOffset) <= distance) { |
| 441 | latch.countDown() |
Jakub Gielzak | 4368b72 | 2019-01-07 14:49:53 +0000 | [diff] [blame] | 442 | post { unregisterOnPageChangeCallback(this) } |
Jelle Fresen | 6164139 | 2018-10-02 12:07:57 +0100 | [diff] [blame] | 443 | } |
| 444 | } |
Jelle Fresen | 6164139 | 2018-10-02 12:07:57 +0100 | [diff] [blame] | 445 | }) |
| 446 | |
| 447 | return latch |
| 448 | } |
| 449 | |
Jelle Fresen | fa6c931 | 2019-05-01 18:06:23 +0100 | [diff] [blame] | 450 | fun ViewPager2.addWaitForFirstScrollEventLatch(): CountDownLatch { |
| 451 | val latch = CountDownLatch(1) |
| 452 | registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { |
| 453 | override fun onPageScrolled(position: Int, offset: Float, offsetPx: Int) { |
| 454 | latch.countDown() |
| 455 | post { unregisterOnPageChangeCallback(this) } |
| 456 | } |
| 457 | }) |
| 458 | return latch |
| 459 | } |
| 460 | |
Jelle Fresen | 3e47bc7 | 2019-07-26 12:16:43 +0100 | [diff] [blame] | 461 | val ViewPager2.linearLayoutManager: LinearLayoutManager |
| 462 | get() = recyclerView.layoutManager as LinearLayoutManager |
| 463 | |
Jelle Fresen | 6c1744d | 2019-04-05 11:26:12 +0100 | [diff] [blame] | 464 | val ViewPager2.recyclerView: RecyclerView |
| 465 | get() { |
| 466 | return getChildAt(0) as RecyclerView |
| 467 | } |
| 468 | |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 469 | val ViewPager2.currentCompletelyVisibleItem: Int |
| 470 | get() { |
Jelle Fresen | 97b098a | 2019-07-01 17:00:30 +0100 | [diff] [blame] | 471 | var position = RecyclerView.NO_POSITION |
| 472 | activityTestRule.runOnUiThread { |
Jelle Fresen | 3e47bc7 | 2019-07-26 12:16:43 +0100 | [diff] [blame] | 473 | position = linearLayoutManager.findFirstCompletelyVisibleItemPosition() |
Jelle Fresen | 97b098a | 2019-07-01 17:00:30 +0100 | [diff] [blame] | 474 | } |
| 475 | return position |
Jelle Fresen | 25c0d5f | 2018-09-14 15:29:48 +0100 | [diff] [blame] | 476 | } |
| 477 | |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 478 | /** |
| 479 | * Checks: |
| 480 | * 1. Expected page is the current ViewPager2 page |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 481 | * 2. Expected state is SCROLL_STATE_IDLE |
| 482 | * 3. Expected text is displayed |
| 483 | * 4. Internal activity state is valid (as per activity self-test) |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 484 | */ |
Jelle Fresen | a2cb4bc | 2019-03-06 18:59:54 +0000 | [diff] [blame] | 485 | fun Context.assertBasicState( |
| 486 | pageIx: Int, |
Jelle Fresen | 2ed00fe | 2019-07-01 11:45:15 +0100 | [diff] [blame] | 487 | value: String? = pageIx.toString(), |
Jelle Fresen | a2cb4bc | 2019-03-06 18:59:54 +0000 | [diff] [blame] | 488 | performSelfCheck: Boolean = true |
| 489 | ) { |
Jakub Gielzak | 1c9c583 | 2018-12-02 18:42:16 +0000 | [diff] [blame] | 490 | assertThat<Int>( |
| 491 | "viewPager.getCurrentItem() should return $pageIx", |
| 492 | viewPager.currentItem, equalTo(pageIx) |
| 493 | ) |
Jelle Fresen | 81f1c97 | 2019-07-10 14:07:43 +0100 | [diff] [blame] | 494 | assertThat("viewPager should be IDLE", viewPager.scrollState, equalTo(SCROLL_STATE_IDLE)) |
Jelle Fresen | 2ed00fe | 2019-07-01 11:45:15 +0100 | [diff] [blame] | 495 | if (value != null) { |
Jelle Fresen | 450e0c3 | 2019-08-13 10:18:41 +0100 | [diff] [blame^] | 496 | onView(allOf<View>(withId(R.id.text_view), isCompletelyDisplayed())).check( |
Jelle Fresen | 2ed00fe | 2019-07-01 11:45:15 +0100 | [diff] [blame] | 497 | matches(withText(value)) |
| 498 | ) |
| 499 | } |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 500 | |
Jelle Fresen | a2cb4bc | 2019-03-06 18:59:54 +0000 | [diff] [blame] | 501 | // TODO(b/130153109): Wire offscreenPageLimit into FragmentAdapter, remove performSelfCheck |
| 502 | if (performSelfCheck && viewPager.adapter is SelfChecking) { |
Jakub Gielzak | d111249 | 2019-02-19 12:30:28 +0000 | [diff] [blame] | 503 | (viewPager.adapter as SelfChecking).selfCheck() |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 504 | } |
sallyyuen | 65b52fc | 2019-01-25 16:09:00 -0800 | [diff] [blame] | 505 | assertPageActions() |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 506 | } |
| 507 | |
Jelle Fresen | 51a3aa6 | 2019-07-31 09:45:59 +0100 | [diff] [blame] | 508 | fun Context.resetViewPagerTo(page: Int) { |
| 509 | viewPager.setCurrentItemSync(page, false, 2, TimeUnit.SECONDS) |
| 510 | // VP2 was potentially settling while the RetryException was raised, |
| 511 | // in which case we must wait until the IDLE event has been fired |
| 512 | activityTestRule.waitForExecution(1) |
| 513 | } |
| 514 | |
Jelle Fresen | f418aaa | 2019-07-29 13:16:41 +0100 | [diff] [blame] | 515 | fun Context.modifyDataSetSync(block: () -> Unit) { |
| 516 | val layoutChangedLatch = viewPager.addWaitForLayoutChangeLatch() |
Jelle Fresen | 3a6d49d | 2019-07-30 14:22:07 +0100 | [diff] [blame] | 517 | runOnUiThreadSync { |
Jelle Fresen | f418aaa | 2019-07-29 13:16:41 +0100 | [diff] [blame] | 518 | block() |
| 519 | } |
| 520 | layoutChangedLatch.await(1, TimeUnit.SECONDS) |
| 521 | |
| 522 | // Let animations run |
| 523 | val animationLatch = CountDownLatch(1) |
| 524 | viewPager.recyclerView.itemAnimator!!.isRunning { |
| 525 | animationLatch.countDown() |
| 526 | } |
| 527 | animationLatch.await(1, TimeUnit.SECONDS) |
| 528 | } |
| 529 | |
Jelle Fresen | 2436f95 | 2018-08-14 17:16:49 +0100 | [diff] [blame] | 530 | fun ViewPager2.setCurrentItemSync( |
| 531 | targetPage: Int, |
| 532 | smoothScroll: Boolean, |
| 533 | timeout: Long, |
Jelle Fresen | 965e50a | 2019-01-25 12:52:30 +0000 | [diff] [blame] | 534 | unit: TimeUnit, |
| 535 | expectEvents: Boolean = (targetPage != currentItem) |
Jelle Fresen | 2436f95 | 2018-08-14 17:16:49 +0100 | [diff] [blame] | 536 | ) { |
Jelle Fresen | 965e50a | 2019-01-25 12:52:30 +0000 | [diff] [blame] | 537 | val latch = |
| 538 | if (expectEvents) |
| 539 | addWaitForScrolledLatch(targetPage, smoothScroll) |
| 540 | else |
| 541 | CountDownLatch(1) |
| 542 | post { |
| 543 | setCurrentItem(targetPage, smoothScroll) |
| 544 | if (!expectEvents) { |
| 545 | latch.countDown() |
| 546 | } |
| 547 | } |
Jelle Fresen | 2436f95 | 2018-08-14 17:16:49 +0100 | [diff] [blame] | 548 | latch.await(timeout, unit) |
Jelle Fresen | 2436f95 | 2018-08-14 17:16:49 +0100 | [diff] [blame] | 549 | } |
| 550 | |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 551 | enum class SortOrder(val sign: Int) { |
| 552 | ASC(1), |
| 553 | DESC(-1) |
| 554 | } |
| 555 | |
| 556 | fun <T, R : Comparable<R>> List<T>.assertSorted(selector: (T) -> R) { |
| 557 | assertThat(this, equalTo(this.sortedBy(selector))) |
| 558 | } |
| 559 | |
| 560 | /** |
Jelle Fresen | a014843 | 2019-05-17 11:20:01 +0100 | [diff] [blame] | 561 | * Returns the slice between the first and second element. First and second element are not |
| 562 | * included in the results. Search for the second element starts on the element after the first |
| 563 | * element. If first element is not found, an empty list is returned. If second element is not |
| 564 | * found, all elements after the first are returned. |
| 565 | * |
| 566 | * @return A list with all elements between the first and the second element |
| 567 | */ |
| 568 | fun <T> List<T>.slice(first: T, second: T): List<T> { |
| 569 | return dropWhile { it != first }.drop(1).takeWhile { it != second } |
| 570 | } |
| 571 | |
| 572 | /** |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 573 | * Is between [min, max) |
| 574 | * @param min - inclusive |
| 575 | * @param max - exclusive |
| 576 | */ |
| 577 | fun <T : Comparable<T>> isBetweenInEx(min: T, max: T): Matcher<T> { |
| 578 | return allOf(greaterThanOrEqualTo<T>(min), lessThan<T>(max)) |
| 579 | } |
| 580 | |
| 581 | /** |
| 582 | * Is between [min, max] |
| 583 | * @param min - inclusive |
| 584 | * @param max - inclusive |
| 585 | */ |
| 586 | fun <T : Comparable<T>> isBetweenInIn(min: T, max: T): Matcher<T> { |
| 587 | return allOf(greaterThanOrEqualTo<T>(min), lessThanOrEqualTo<T>(max)) |
| 588 | } |
Jelle Fresen | 0c779ff | 2018-09-07 14:58:16 +0100 | [diff] [blame] | 589 | |
| 590 | /** |
| 591 | * Is between [min(a, b), max(a, b)] |
| 592 | * @param a - inclusive |
| 593 | * @param b - inclusive |
| 594 | */ |
| 595 | fun <T : Comparable<T>> isBetweenInInMinMax(a: T, b: T): Matcher<T> { |
Jelle Fresen | 2c342f2 | 2018-10-03 11:58:08 +0100 | [diff] [blame] | 596 | return isBetweenInIn(minOf(a, b), maxOf(a, b)) |
Jelle Fresen | 0c779ff | 2018-09-07 14:58:16 +0100 | [diff] [blame] | 597 | } |
Jakub Gielzak | 325a198 | 2018-07-27 17:10:38 +0100 | [diff] [blame] | 598 | } |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 599 | |
| 600 | typealias AdapterProvider = (TestActivity) -> RecyclerView.Adapter<out RecyclerView.ViewHolder> |
| 601 | |
| 602 | typealias AdapterProviderForItems = (items: List<String>) -> AdapterProvider |
| 603 | |
| 604 | val fragmentAdapterProvider: AdapterProviderForItems = { items -> |
Jakub Gielzak | bd58b64 | 2019-03-25 16:54:39 +0000 | [diff] [blame] | 605 | { activity: TestActivity -> FragmentAdapter(activity, items) } |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 606 | } |
| 607 | |
Jakub Gielzak | acc3021 | 2018-11-23 15:10:33 +0000 | [diff] [blame] | 608 | /** |
| 609 | * Same as [fragmentAdapterProvider] but with a custom implementation of |
| 610 | * [FragmentStateAdapter.getItemId] and [FragmentStateAdapter.containsItem]. |
Jakub Gielzak | 1c9c583 | 2018-12-02 18:42:16 +0000 | [diff] [blame] | 611 | * Not suitable for testing [RecyclerView.Adapter.notifyDataSetChanged]. |
Jakub Gielzak | acc3021 | 2018-11-23 15:10:33 +0000 | [diff] [blame] | 612 | */ |
| 613 | val fragmentAdapterProviderCustomIds: AdapterProviderForItems = { items -> |
| 614 | { activity -> |
| 615 | fragmentAdapterProvider(items)(activity).also { |
| 616 | // more than position can represent, so a good test if ids are used consistently |
| 617 | val offset = 3L * Int.MAX_VALUE |
| 618 | val adapter = it as FragmentAdapter |
| 619 | adapter.positionToItemId = { position -> position + offset } |
| 620 | adapter.itemIdToContains = { itemId -> |
| 621 | val position = itemId - offset |
| 622 | position in (0 until adapter.itemCount) |
| 623 | } |
| 624 | } |
| 625 | } |
| 626 | } |
| 627 | |
Jakub Gielzak | 1c9c583 | 2018-12-02 18:42:16 +0000 | [diff] [blame] | 628 | /** |
| 629 | * Same as [fragmentAdapterProvider] but with a custom implementation of |
| 630 | * [FragmentStateAdapter.getItemId] and [FragmentStateAdapter.containsItem]. |
| 631 | * Suitable for testing [RecyclerView.Adapter.notifyDataSetChanged]. |
| 632 | */ |
| 633 | val fragmentAdapterProviderValueId: AdapterProviderForItems = { items -> |
| 634 | { activity -> |
| 635 | fragmentAdapterProvider(items)(activity).also { |
| 636 | val adapter = it as FragmentAdapter |
| 637 | adapter.positionToItemId = { position -> items[position].getId() } |
| 638 | adapter.itemIdToContains = { itemId -> items.any { item -> item.getId() == itemId } } |
| 639 | } |
| 640 | } |
| 641 | } |
| 642 | |
| 643 | /** Extracts the sole number from a [String] and converts it to a [Long] */ |
| 644 | private fun (String).getId(): Long { |
| 645 | val matches = Regex("[0-9]+").findAll(this).toList() |
| 646 | if (matches.size != 1) { |
| 647 | throw IllegalStateException("There should be exactly one number in the input string") |
| 648 | } |
| 649 | return matches.first().value.toLong() |
| 650 | } |
| 651 | |
| 652 | /** |
| 653 | * Same as [viewAdapterProvider] but with a custom implementation of |
| 654 | * [RecyclerView.Adapter.getItemId]. |
| 655 | * Suitable for testing [RecyclerView.Adapter.notifyDataSetChanged].mu |
| 656 | */ |
| 657 | val viewAdapterProviderValueId: AdapterProviderForItems = { items -> |
| 658 | { activity -> |
| 659 | viewAdapterProvider(items)(activity).also { |
| 660 | val adapter = it as ViewAdapter |
| 661 | adapter.positionToItemId = { position -> items[position].getId() } |
| 662 | adapter.setHasStableIds(true) |
| 663 | } |
| 664 | } |
| 665 | } |
| 666 | |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 667 | val viewAdapterProvider: AdapterProviderForItems = { items -> { ViewAdapter(items) } } |
| 668 | |
Jakub Gielzak | 788ff15 | 2019-01-03 12:51:55 +0000 | [diff] [blame] | 669 | fun stringSequence(pageCount: Int) = (0 until pageCount).map { it.toString() } |
Jakub Gielzak | ca2641d | 2018-09-25 16:32:32 +0100 | [diff] [blame] | 670 | |
| 671 | val AdapterProviderForItems.supportsMutations: Boolean |
| 672 | get() { |
| 673 | return this == fragmentAdapterProvider |
| 674 | } |
Jelle Fresen | 86dbc42 | 2019-01-31 17:01:40 +0000 | [diff] [blame] | 675 | |
| 676 | fun scrollStateToString(@ViewPager2.ScrollState state: Int): String { |
| 677 | return when (state) { |
| 678 | SCROLL_STATE_IDLE -> "IDLE" |
| 679 | SCROLL_STATE_DRAGGING -> "DRAGGING" |
| 680 | SCROLL_STATE_SETTLING -> "SETTLING" |
| 681 | else -> throw IllegalArgumentException("Scroll state $state doesn't exist") |
| 682 | } |
| 683 | } |
| 684 | |
| 685 | fun scrollStateGlossary(): String { |
| 686 | return "Scroll states: " + |
| 687 | "$SCROLL_STATE_IDLE=${scrollStateToString(SCROLL_STATE_IDLE)}, " + |
| 688 | "$SCROLL_STATE_DRAGGING=${scrollStateToString(SCROLL_STATE_DRAGGING)}, " + |
| 689 | "$SCROLL_STATE_SETTLING=${scrollStateToString(SCROLL_STATE_SETTLING)})" |
| 690 | } |
Jelle Fresen | fa6c931 | 2019-05-01 18:06:23 +0100 | [diff] [blame] | 691 | |
| 692 | class RetryException(msg: String) : Exception(msg) |
| 693 | |
| 694 | fun tryNTimes(n: Int, resetBlock: () -> Unit, tryBlock: () -> Unit) { |
| 695 | repeat(n) { i -> |
| 696 | try { |
| 697 | tryBlock() |
| 698 | return |
| 699 | } catch (e: RetryException) { |
| 700 | if (i < n - 1) { |
| 701 | Log.w(BaseTest.TAG, "Bad state, retrying block", e) |
| 702 | } else { |
| 703 | throw AssertionError("Block hit bad state $n times", e) |
| 704 | } |
| 705 | resetBlock() |
| 706 | } |
| 707 | } |
| 708 | } |
Jelle Fresen | a913290 | 2019-05-02 16:35:39 +0100 | [diff] [blame] | 709 | |
| 710 | val View.isRtl: Boolean |
| 711 | get() = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL |
| 712 | |
| 713 | val ViewPager2.isHorizontal: Boolean get() = orientation == ORIENTATION_HORIZONTAL |