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