1/* 2 * Copyright (C) 2018 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 androidx.viewpager2.widget; 18 19import static android.support.test.espresso.Espresso.onView; 20import static android.support.test.espresso.assertion.ViewAssertions.matches; 21import static android.support.test.espresso.matcher.ViewMatchers.assertThat; 22import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 23import static android.support.test.espresso.matcher.ViewMatchers.withId; 24import static android.support.test.espresso.matcher.ViewMatchers.withText; 25import static android.view.View.OVER_SCROLL_NEVER; 26 27import static androidx.viewpager2.widget.ViewPager2.Orientation.HORIZONTAL; 28import static androidx.viewpager2.widget.ViewPager2.Orientation.VERTICAL; 29 30import static org.hamcrest.CoreMatchers.allOf; 31import static org.hamcrest.CoreMatchers.equalTo; 32import static org.hamcrest.CoreMatchers.is; 33 34import static java.util.Arrays.asList; 35import static java.util.Collections.singletonList; 36 37import android.os.Build; 38import android.support.test.filters.LargeTest; 39import android.support.test.rule.ActivityTestRule; 40import android.util.Log; 41import android.util.Pair; 42 43import androidx.recyclerview.widget.RecyclerView; 44import androidx.testutils.FragmentActivityUtils; 45import androidx.viewpager2.test.R; 46import androidx.viewpager2.widget.swipe.BaseActivity; 47import androidx.viewpager2.widget.swipe.FragmentAdapterActivity; 48import androidx.viewpager2.widget.swipe.PageSwiper; 49import androidx.viewpager2.widget.swipe.ViewAdapterActivity; 50 51import org.junit.Before; 52import org.junit.Test; 53import org.junit.runner.RunWith; 54import org.junit.runners.Parameterized; 55 56import java.util.ArrayList; 57import java.util.Collection; 58import java.util.Collections; 59import java.util.HashMap; 60import java.util.HashSet; 61import java.util.List; 62import java.util.Map; 63import java.util.Random; 64import java.util.Set; 65 66@LargeTest 67@RunWith(Parameterized.class) 68public class SwipeTest { 69 private static final List<Class<? extends BaseActivity>> TEST_ACTIVITIES_ALL = asList( 70 ViewAdapterActivity.class, FragmentAdapterActivity.class); 71 private static final Set<Integer> NO_CONFIG_CHANGES = Collections.emptySet(); 72 private static final List<Pair<Integer, Integer>> NO_MUTATIONS = Collections.emptyList(); 73 private static final boolean RANDOM_PASS_ENABLED = false; 74 75 private final TestConfig mTestConfig; 76 private ActivityTestRule<? extends BaseActivity> mActivityTestRule; 77 private PageSwiper mSwiper; 78 79 public SwipeTest(TestConfig testConfig) { 80 mTestConfig = testConfig; 81 } 82 83 @Test 84 public void test() throws Throwable { 85 BaseActivity activity = mActivityTestRule.getActivity(); 86 87 final int[] expectedValues = new int[mTestConfig.mTotalPages]; 88 for (int i = 0; i < mTestConfig.mTotalPages; i++) { 89 expectedValues[i] = i; 90 } 91 92 int currentPage = 0, currentStep = 0; 93 assertStateCorrect(expectedValues[currentPage], activity); 94 for (int nextPage : mTestConfig.mPageSequence) { 95 // value change 96 if (mTestConfig.mStepToNewValue.containsKey(currentStep)) { 97 expectedValues[currentPage] = mTestConfig.mStepToNewValue.get(currentStep); 98 updatePage(currentPage, expectedValues[currentPage], activity); 99 assertStateCorrect(expectedValues[currentPage], activity); 100 } 101 102 // config change 103 if (mTestConfig.mConfigChangeSteps.contains(currentStep++)) { 104 activity = FragmentActivityUtils.recreateActivity(mActivityTestRule, activity); 105 assertStateCorrect(expectedValues[currentPage], activity); 106 } 107 108 // page swipe 109 mSwiper.swipe(currentPage, nextPage); 110 currentPage = nextPage; 111 assertStateCorrect(expectedValues[currentPage], activity); 112 } 113 } 114 115 private static void updatePage(final int pageIx, final int newValue, 116 final BaseActivity activity) { 117 activity.runOnUiThread(new Runnable() { 118 @Override 119 public void run() { 120 activity.updatePage(pageIx, newValue); 121 } 122 }); 123 } 124 125 private void assertStateCorrect(int expectedValue, BaseActivity activity) { 126 onView(allOf(withId(R.id.text_view), isDisplayed())).check( 127 matches(withText(String.valueOf(expectedValue)))); 128 activity.validateState(); 129 } 130 131 @Parameterized.Parameters(name = "{0}") 132 public static List<TestConfig> getParams() { 133 List<TestConfig> tests = new ArrayList<>(); 134 135 if (RANDOM_PASS_ENABLED) { // run locally after making larger changes 136 tests.addAll(generateRandomTests()); 137 } 138 139 for (Class<? extends BaseActivity> activityClass : TEST_ACTIVITIES_ALL) { 140 tests.add(new TestConfig("full pass", asList(1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0), 141 NO_CONFIG_CHANGES, NO_MUTATIONS, 8, activityClass, HORIZONTAL)); 142 143 tests.add(new TestConfig("basic vertical", asList(0, 1, 2, 3, 3, 2, 1, 0, 0), 144 NO_CONFIG_CHANGES, NO_MUTATIONS, 4, activityClass, VERTICAL)); 145 tests.add(new TestConfig("swipe beyond edge pages", 146 asList(0, 0, 1, 2, 3, 3, 3, 2, 1, 0, 0, 0), NO_CONFIG_CHANGES, NO_MUTATIONS, 4, 147 activityClass, HORIZONTAL)); 148 149 tests.add(new TestConfig("config change", asList(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0), 150 asList(3, 5, 7), NO_MUTATIONS, 7, activityClass, HORIZONTAL)); 151 152 tests.add( 153 new TestConfig("regression1", asList(1, 2, 3, 2, 1, 2, 3, 4), NO_CONFIG_CHANGES, 154 NO_MUTATIONS, 10, activityClass, HORIZONTAL)); 155 156 tests.add(new TestConfig("regression2", asList(1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 5), 157 NO_CONFIG_CHANGES, NO_MUTATIONS, 10, activityClass, HORIZONTAL)); 158 159 tests.add(new TestConfig("regression3", asList(1, 2, 3, 2, 1, 2, 3, 2, 1, 0), 160 NO_CONFIG_CHANGES, NO_MUTATIONS, 10, activityClass, HORIZONTAL)); 161 } 162 163 // mutations only apply to Fragment state persistence 164 tests.add(new TestConfig("mutations", asList(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0), 165 singletonList(8), 166 asList(Pair.create(0, 999), Pair.create(1, 100), Pair.create(3, 300), 167 Pair.create(5, 500)), 7, FragmentAdapterActivity.class, HORIZONTAL)); 168 169 return checkTestNamesUnique(tests); 170 } 171 172 private static Collection<TestConfig> generateRandomTests() { 173 List<TestConfig> result = new ArrayList<>(); 174 175 int id = 0; 176 for (int i = 0; i < 10; i++) { 177 // both adapters 178 for (Class<? extends BaseActivity> adapterClass : TEST_ACTIVITIES_ALL) { 179 result.add(createRandomTest(id++, 8, 50, 0, 0, 0.875, adapterClass)); 180 result.add(createRandomTest(id++, 8, 10, 0.5, 0, 0.875, adapterClass)); 181 } 182 183 // fragment adapter specific 184 result.add( 185 createRandomTest(id++, 8, 50, 0, 0.125, 0.875, FragmentAdapterActivity.class)); 186 result.add(createRandomTest(id++, 8, 10, 0.5, 0.125, 0.875, 187 FragmentAdapterActivity.class)); 188 } 189 190 return result; 191 } 192 193 /** 194 * @param advanceProbability determines the probability of a swipe direction being towards 195 * the next edge - e.g. if we start from the left, it's the 196 * probability that the next swipe will go right. <p> Setting it to 197 * values closer to 0.5 results in a lot of back and forth, while 198 * setting it closer to 1.0 results in going edge to edge with few 199 * back-swipes. 200 */ 201 @SuppressWarnings("SameParameterValue") 202 private static TestConfig createRandomTest(int id, int totalPages, int sequenceLength, 203 double configChangeProbability, double mutationProbability, double advanceProbability, 204 Class<? extends BaseActivity> activityClass) { 205 Random random = new Random(); 206 207 List<Integer> pageSequence = new ArrayList<>(); 208 List<Integer> configChanges = new ArrayList<>(); 209 List<Pair<Integer, Integer>> stepToNewValue = new ArrayList<>(); 210 211 int pageIx = 0; 212 Double goRightProbability = null; 213 for (int currentStep = 0; currentStep < sequenceLength; currentStep++) { 214 if (random.nextDouble() < configChangeProbability) { 215 configChanges.add(currentStep); 216 } 217 218 if (random.nextDouble() < mutationProbability) { 219 stepToNewValue.add(Pair.create(currentStep, random.nextInt(10_000))); 220 } 221 222 boolean goRight; 223 if (pageIx == 0) { 224 goRight = true; 225 goRightProbability = advanceProbability; 226 } else if (pageIx == totalPages - 1) { // last page 227 goRight = false; 228 goRightProbability = 1 - advanceProbability; 229 } else { 230 goRight = random.nextDouble() < goRightProbability; 231 } 232 pageSequence.add(goRight ? ++pageIx : --pageIx); 233 } 234 235 return new TestConfig("random_" + id, pageSequence, configChanges, stepToNewValue, 236 totalPages, activityClass, HORIZONTAL); 237 } 238 239 private static List<TestConfig> checkTestNamesUnique(List<TestConfig> configs) { 240 Set<String> names = new HashSet<>(); 241 for (TestConfig config : configs) { 242 names.add(config.toString()); 243 } 244 assertThat(names.size(), is(configs.size())); 245 return configs; 246 } 247 248 @Before 249 public void setUp() { 250 Log.i(getClass().getSimpleName(), mTestConfig.toFullSpecString()); 251 252 mActivityTestRule = new ActivityTestRule<>(mTestConfig.mActivityClass, true, false); 253 mActivityTestRule.launchActivity(BaseActivity.createIntent(mTestConfig.mTotalPages)); 254 255 final ViewPager2 viewPager = mActivityTestRule.getActivity().findViewById(R.id.view_pager); 256 RecyclerView recyclerView = (RecyclerView) viewPager.getChildAt(0); // HACK 257 mSwiper = new PageSwiper(mTestConfig.mTotalPages, recyclerView, mTestConfig.mOrientation); 258 259 mActivityTestRule.getActivity().runOnUiThread(new Runnable() { 260 @Override 261 public void run() { 262 viewPager.setOrientation(mTestConfig.mOrientation); 263 } 264 }); 265 266 // Disabling edge animations on API < 16. Espresso discourages animations altogether, but 267 // keeping them for now where they work - as closer to the real environment. 268 if (Build.VERSION.SDK_INT < 16) { 269 recyclerView.setOverScrollMode(OVER_SCROLL_NEVER); 270 } 271 272 onView(withId(R.id.view_pager)).check(matches(isDisplayed())); 273 } 274 275 private static class TestConfig { 276 final String mMessage; 277 final List<Integer> mPageSequence; 278 final Set<Integer> mConfigChangeSteps; 279 /** {@link Map.Entry#getKey()} = step, {@link Map.Entry#getValue()} = new value */ 280 final Map<Integer, Integer> mStepToNewValue; 281 final int mTotalPages; 282 final Class<? extends BaseActivity> mActivityClass; 283 final @ViewPager2.Orientation int mOrientation; 284 285 /** 286 * @param stepToNewValue {@link Pair#first} = step, {@link Pair#second} = new value 287 */ 288 TestConfig(String message, List<Integer> pageSequence, 289 Collection<Integer> configChangeSteps, 290 List<Pair<Integer, Integer>> stepToNewValue, 291 int totalPages, 292 Class<? extends BaseActivity> activityClass, 293 int orientation) { 294 mMessage = message; 295 mPageSequence = pageSequence; 296 mConfigChangeSteps = new HashSet<>(configChangeSteps); 297 mStepToNewValue = mapFromPairList(stepToNewValue); 298 mTotalPages = totalPages; 299 mActivityClass = activityClass; 300 mOrientation = orientation; 301 } 302 303 private static Map<Integer, Integer> mapFromPairList(List<Pair<Integer, Integer>> list) { 304 Map<Integer, Integer> result = new HashMap<>(); 305 for (Pair<Integer, Integer> pair : list) { 306 Integer prevValueAtKey = result.put(pair.first, pair.second); 307 assertThat("there should be only one value defined for a key", prevValueAtKey, 308 equalTo(null)); 309 } 310 return result; 311 } 312 313 @Override 314 public String toString() { 315 return mActivityClass.getSimpleName() + ": " + mMessage; 316 } 317 318 String toFullSpecString() { 319 return String.format( 320 "Test: %s\nPage sequence: %s\nTotal pages: %s\nMutations {step1:newValue1, " 321 + "step2:newValue2, ...}: %s", 322 toString(), 323 mPageSequence, 324 mTotalPages, 325 mStepToNewValue.toString().replace('=', ':')); 326 } 327 } 328} 329