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