blob: 3fcbccb55919b8a2afdac36e63b669a1cd8f2182 [file] [log] [blame]
Jakub Gielzak325a1982018-07-27 17:10:38 +01001/*
Jakub Gielzak5784b782018-08-08 15:35:01 +01002 * Copyright (C) 2018 The Android Open Source Project
Jakub Gielzak325a1982018-07-27 17:10:38 +01003 *
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 androidx.viewpager2.widget
18
19import android.os.Build
20import android.view.View
21import android.view.View.OVER_SCROLL_NEVER
22import androidx.recyclerview.widget.RecyclerView
23import androidx.test.espresso.Espresso.onView
24import androidx.test.espresso.action.CoordinatesProvider
25import androidx.test.espresso.action.GeneralLocation
26import androidx.test.espresso.action.GeneralSwipeAction
27import androidx.test.espresso.action.Press
28import androidx.test.espresso.action.Swipe
29import androidx.test.espresso.action.ViewActions.actionWithAssertions
30import androidx.test.espresso.assertion.ViewAssertions.matches
31import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
32import androidx.test.espresso.matcher.ViewMatchers.withId
33import androidx.test.espresso.matcher.ViewMatchers.withText
Jakub Gielzak325a1982018-07-27 17:10:38 +010034import androidx.test.rule.ActivityTestRule
35import androidx.testutils.FragmentActivityUtils
36import androidx.viewpager2.test.R
37import androidx.viewpager2.widget.ViewPager2.Orientation.HORIZONTAL
38import androidx.viewpager2.widget.ViewPager2.ScrollState.IDLE
39import androidx.viewpager2.widget.swipe.BaseActivity
40import androidx.viewpager2.widget.swipe.PageSwiper
41import androidx.viewpager2.widget.swipe.ViewAdapterActivity
42import org.hamcrest.CoreMatchers.equalTo
43import org.hamcrest.Matcher
44import org.hamcrest.Matchers.allOf
45import org.hamcrest.Matchers.greaterThanOrEqualTo
46import org.hamcrest.Matchers.lessThan
47import org.hamcrest.Matchers.lessThanOrEqualTo
48import org.junit.Assert.assertThat
49import java.util.concurrent.CountDownLatch
Jelle Fresen2436f952018-08-14 17:16:49 +010050import java.util.concurrent.TimeUnit
Jakub Gielzak325a1982018-07-27 17:10:38 +010051
Jakub Gielzak325a1982018-07-27 17:10:38 +010052open class BaseTest {
53 fun setUpTest(
54 totalPages: Int,
55 @ViewPager2.Orientation orientation: Int,
56 activityClass: Class<out BaseActivity> = ViewAdapterActivity::class.java
57 ): Context {
58 val activityTestRule = ActivityTestRule(activityClass, true, false)
59 activityTestRule.launchActivity(BaseActivity.createIntent(totalPages))
60
61 val viewPager: ViewPager2 = activityTestRule.activity.findViewById(R.id.view_pager)
62 activityTestRule.runOnUiThread { viewPager.orientation = orientation }
63 onView(withId(R.id.view_pager)).check(matches(isDisplayed()))
64
65 val mPageSwiper = PageSwiper(totalPages, viewPager.orientation)
66
67 // animations getting in the way on API < 16
68 if (Build.VERSION.SDK_INT < 16) {
69 val recyclerView: RecyclerView = viewPager.getChildAt(0) as RecyclerView
70 recyclerView.overScrollMode = OVER_SCROLL_NEVER
71 }
72
73 return Context(activityTestRule, mPageSwiper).apply {
74 assertBasicState(0) // sanity check
75 }
76 }
77
78 data class Context(
79 val activityTestRule: ActivityTestRule<out BaseActivity>,
80 val swiper: PageSwiper
81 ) {
82 fun recreateActivity() {
83 activity = FragmentActivityUtils.recreateActivity(activityTestRule, activity)
84 }
85
86 var activity: BaseActivity = activityTestRule.activity
87 private set(value) {
88 field = value
89 }
90
91 fun runOnUiThread(f: () -> Unit) = activity.runOnUiThread(f)
92
93 val viewPager: ViewPager2 get() = activity.findViewById(R.id.view_pager)
94 }
95
96 fun peekForward(@ViewPager2.Orientation orientation: Int) {
97 peek(orientation, -50f)
98 }
99
100 fun peekBackward(@ViewPager2.Orientation orientation: Int) {
101 peek(orientation, 50f)
102 }
103
104 private fun peek(@ViewPager2.Orientation orientation: Int, offset: Float) {
105 onView(allOf(isDisplayed(), withId(R.id.text_view))).perform(actionWithAssertions(
106 GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
107 CoordinatesProvider { view ->
108 val coordinates = GeneralLocation.CENTER.calculateCoordinates(view)
109 if (orientation == HORIZONTAL) {
110 coordinates[0] += offset
111 } else {
112 coordinates[1] += offset
113 }
114 coordinates
115 }, Press.FINGER)))
116 }
117
118 /**
119 * Note: returned latch relies on the tested API, so it's critical to check that the final
120 * visible page is correct using [assertBasicState].
121 */
122 fun ViewPager2.addWaitForScrolledLatch(
123 targetPage: Int,
124 waitForIdle: Boolean = true
125 ): CountDownLatch {
126 val latch = CountDownLatch(if (waitForIdle) 2 else 1)
127 var lastScrollFired = false
128
129 addOnPageChangeListener(object : ViewPager2.OnPageChangeListener {
130 override fun onPageScrollStateChanged(state: Int) {
131 if (lastScrollFired && state == IDLE) {
132 latch.countDown()
133 }
134 }
135
136 override fun onPageSelected(position: Int) {
137 // nothing
138 }
139
140 override fun onPageScrolled(
141 position: Int,
142 positionOffset: Float,
143 positionOffsetPixels: Int
144 ) {
145 if (position == targetPage && positionOffsetPixels == 0) {
146 latch.countDown()
147 lastScrollFired = true
148 }
149 }
150 })
151
152 return latch
153 }
154
155 val ViewPager2.pageSize: Int
156 get() {
157 return if (orientation == HORIZONTAL) {
158 measuredWidth - paddingLeft - paddingRight
159 } else {
160 measuredHeight - paddingTop - paddingBottom
161 }
162 }
163
164 /**
165 * Checks:
166 * 1. Expected page is the current ViewPager2 page
167 * 2. Expected text is displayed
168 * 3. Internal activity state is valid (as per activity self-test)
169 */
170 fun Context.assertBasicState(pageIx: Int, value: Int = pageIx) {
171 assertThat<Int>(viewPager.currentItem, equalTo(pageIx))
172 onView(allOf<View>(withId(R.id.text_view), isDisplayed())).check(
173 matches(withText(value.toString())))
174 activity.validateState()
175 }
176
Jelle Fresen2436f952018-08-14 17:16:49 +0100177 fun ViewPager2.setCurrentItemSync(
178 targetPage: Int,
179 smoothScroll: Boolean,
180 timeout: Long,
181 unit: TimeUnit
182 ) {
183 val latch = addWaitForScrolledLatch(targetPage, false)
184
185 // temporary hack to stop the tests from failing
186 // this most likely shows a bug in PageChangeListener - communicating IDLE before
187 // RecyclerView is ready; TODO: investigate further and fix
188 val latchRV = CountDownLatch(1)
189 val rv = getChildAt(0) as RecyclerView
190 rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
191 override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
192 if (newState == 0) {
193 latchRV.countDown()
194 }
195 }
196 })
197 post { setCurrentItem(targetPage, smoothScroll) }
198 latch.await(timeout, unit)
199 latchRV.await(timeout, unit)
200 }
201
Jakub Gielzak325a1982018-07-27 17:10:38 +0100202 enum class SortOrder(val sign: Int) {
203 ASC(1),
204 DESC(-1)
205 }
206
207 fun <T, R : Comparable<R>> List<T>.assertSorted(selector: (T) -> R) {
208 assertThat(this, equalTo(this.sortedBy(selector)))
209 }
210
211 /**
212 * Is between [min, max)
213 * @param min - inclusive
214 * @param max - exclusive
215 */
216 fun <T : Comparable<T>> isBetweenInEx(min: T, max: T): Matcher<T> {
217 return allOf(greaterThanOrEqualTo<T>(min), lessThan<T>(max))
218 }
219
220 /**
221 * Is between [min, max]
222 * @param min - inclusive
223 * @param max - inclusive
224 */
225 fun <T : Comparable<T>> isBetweenInIn(min: T, max: T): Matcher<T> {
226 return allOf(greaterThanOrEqualTo<T>(min), lessThanOrEqualTo<T>(max))
227 }
Jelle Fresen0c779ff2018-09-07 14:58:16 +0100228
229 /**
230 * Is between [min(a, b), max(a, b)]
231 * @param a - inclusive
232 * @param b - inclusive
233 */
234 fun <T : Comparable<T>> isBetweenInInMinMax(a: T, b: T): Matcher<T> {
235 return allOf(greaterThanOrEqualTo<T>(minOf(a, b)), lessThanOrEqualTo<T>(maxOf(a, b)))
236 }
Jakub Gielzak325a1982018-07-27 17:10:38 +0100237}