1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.tasks.tab_management;
6 
7 import static androidx.test.espresso.Espresso.onView;
8 import static androidx.test.espresso.Espresso.pressBack;
9 import static androidx.test.espresso.action.ViewActions.click;
10 import static androidx.test.espresso.assertion.ViewAssertions.matches;
11 import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
12 import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
13 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
14 import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
15 import static androidx.test.espresso.matcher.ViewMatchers.withId;
16 import static androidx.test.espresso.matcher.ViewMatchers.withParent;
17 
18 import static org.hamcrest.Matchers.allOf;
19 import static org.hamcrest.Matchers.is;
20 import static org.hamcrest.Matchers.not;
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertTrue;
24 
25 import static org.chromium.base.test.util.CallbackHelper.WAIT_TIMEOUT_SECONDS;
26 import static org.chromium.base.test.util.CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL;
27 import static org.chromium.base.test.util.CriteriaHelper.DEFAULT_POLLING_INTERVAL;
28 import static org.chromium.chrome.test.util.ViewUtils.onViewWaiting;
29 import static org.chromium.components.browser_ui.widget.RecyclerViewTestUtils.waitForStableRecyclerView;
30 
31 import android.app.Activity;
32 import android.content.pm.ActivityInfo;
33 import android.content.res.Configuration;
34 import android.provider.Settings;
35 import android.support.test.InstrumentationRegistry;
36 import android.view.View;
37 
38 import androidx.annotation.IntDef;
39 import androidx.annotation.Nullable;
40 import androidx.recyclerview.widget.RecyclerView;
41 import androidx.test.espresso.NoMatchingRootException;
42 import androidx.test.espresso.NoMatchingViewException;
43 import androidx.test.espresso.UiController;
44 import androidx.test.espresso.ViewAction;
45 import androidx.test.espresso.ViewAssertion;
46 import androidx.test.espresso.action.GeneralLocation;
47 import androidx.test.espresso.action.GeneralSwipeAction;
48 import androidx.test.espresso.action.Press;
49 import androidx.test.espresso.action.Swipe;
50 import androidx.test.espresso.contrib.RecyclerViewActions;
51 
52 import org.hamcrest.Matcher;
53 
54 import org.chromium.base.ContextUtils;
55 import org.chromium.base.test.util.ApplicationTestUtils;
56 import org.chromium.base.test.util.Criteria;
57 import org.chromium.base.test.util.CriteriaHelper;
58 import org.chromium.chrome.browser.ChromeTabbedActivity;
59 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
60 import org.chromium.chrome.browser.tab.Tab;
61 import org.chromium.chrome.browser.tab.TabSelectionType;
62 import org.chromium.chrome.browser.tabmodel.TabModel;
63 import org.chromium.chrome.browser.tasks.pseudotab.PseudoTab;
64 import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
65 import org.chromium.chrome.tab_ui.R;
66 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
67 import org.chromium.chrome.test.util.ChromeTabUtils;
68 import org.chromium.chrome.test.util.OverviewModeBehaviorWatcher;
69 import org.chromium.content_public.browser.test.util.TestThreadUtils;
70 
71 import java.io.File;
72 import java.lang.annotation.Retention;
73 import java.lang.annotation.RetentionPolicy;
74 import java.util.ArrayList;
75 import java.util.List;
76 
77 /**
78  * Utilities helper class for tab grid/group tests.
79  */
80 public class TabUiTestHelper {
81     /**
82      * Create {@code tabsCount} tabs for {@code cta} in certain tab model based on {@code
83      * isIncognito}.
84      * @param cta            A current running activity to create tabs.
85      * @param isIncognito    Indicator for whether to create tabs in normal model or incognito
86      *         model.
87      * @param tabsCount      Number of tabs to be created.
88      */
createTabs(ChromeTabbedActivity cta, boolean isIncognito, int tabsCount)89     public static void createTabs(ChromeTabbedActivity cta, boolean isIncognito, int tabsCount) {
90         for (int i = 0; i < (isIncognito ? tabsCount : tabsCount - 1); i++) {
91             ChromeTabUtils.newTabFromMenu(
92                     InstrumentationRegistry.getInstrumentation(), cta, isIncognito, true);
93         }
94     }
95 
96     /**
97      * Enter tab switcher from a tab page.
98      * @param cta  The current running activity.
99      */
enterTabSwitcher(ChromeTabbedActivity cta)100     public static void enterTabSwitcher(ChromeTabbedActivity cta) {
101         OverviewModeBehaviorWatcher showWatcher = createOverviewShowWatcher(cta);
102         assertFalse(cta.getLayoutManager().overviewVisible());
103         onViewWaiting(allOf(withId(R.id.tab_switcher_button), isDisplayed())).perform(click());
104         showWatcher.waitForBehavior();
105     }
106 
107     /**
108      * Leave tab switcher by tapping "back".
109      * @param cta  The current running activity.
110      */
leaveTabSwitcher(ChromeTabbedActivity cta)111     public static void leaveTabSwitcher(ChromeTabbedActivity cta) {
112         OverviewModeBehaviorWatcher hideWatcher = createOverviewHideWatcher(cta);
113         assertTrue(cta.getLayoutManager().overviewVisible());
114         pressBack();
115         hideWatcher.waitForBehavior();
116     }
117 
118     /**
119      * Click the first card in grid tab switcher. When group is enabled and the first card is a
120      * group, this will open up the dialog; otherwise this will open up the tab page.
121      * @param cta  The current running activity.
122      */
clickFirstCardFromTabSwitcher(ChromeTabbedActivity cta)123     public static void clickFirstCardFromTabSwitcher(ChromeTabbedActivity cta) {
124         clickNthCardFromTabSwitcher(cta, 0);
125     }
126 
127     /**
128      * Click the Nth card in grid tab switcher. When group is enabled and the Nth card is a
129      * group, this will open up the dialog; otherwise this will open up the tab page.
130      * @param cta  The current running activity.
131      * @param index The index of the target card.
132      */
clickNthCardFromTabSwitcher(ChromeTabbedActivity cta, int index)133     public static void clickNthCardFromTabSwitcher(ChromeTabbedActivity cta, int index) {
134         assertTrue(cta.getLayoutManager().overviewVisible());
135         onView(allOf(withParent(withId(org.chromium.chrome.R.id.compositor_view_holder)),
136                        withId(R.id.tab_list_view)))
137                 .perform(RecyclerViewActions.actionOnItemAtPosition(index, click()));
138     }
139 
140     /**
141      * Click the first tab in tab grid dialog to open a tab page.
142      * @param cta  The current running activity.
143      */
clickFirstTabInDialog(ChromeTabbedActivity cta)144     static void clickFirstTabInDialog(ChromeTabbedActivity cta) {
145         clickNthTabInDialog(cta, 0);
146     }
147 
148     /**
149      * Click the Nth tab in tab grid dialog to open a tab page.
150      * @param cta  The current running activity.
151      * @param index The index of the target tab.
152      */
clickNthTabInDialog(ChromeTabbedActivity cta, int index)153     static void clickNthTabInDialog(ChromeTabbedActivity cta, int index) {
154         OverviewModeBehaviorWatcher hideWatcher = createOverviewHideWatcher(cta);
155         onView(allOf(withId(R.id.tab_list_view), withParent(withId(R.id.dialog_container_view))))
156                 .perform(RecyclerViewActions.actionOnItemAtPosition(index, click()));
157         hideWatcher.waitForBehavior();
158     }
159 
160     /**
161      * Close the first tab in tab gri dialog.
162      * @param cta  The current running activity.
163      */
closeFirstTabInDialog()164     static void closeFirstTabInDialog() {
165         closeNthTabInDialog(0);
166     }
167 
168     /**
169      * Close the Nth tab in tab gri dialog.
170      * @param index The index of the target tab to close.
171      */
closeNthTabInDialog(int index)172     static void closeNthTabInDialog(int index) {
173         onView(allOf(withId(R.id.tab_list_view), withParent(withId(R.id.dialog_container_view))))
174                 .perform(new ViewAction() {
175                     @Override
176                     public Matcher<View> getConstraints() {
177                         return isDisplayed();
178                     }
179 
180                     @Override
181                     public String getDescription() {
182                         return "close tab with index " + String.valueOf(index);
183                     }
184 
185                     @Override
186                     public void perform(UiController uiController, View view) {
187                         RecyclerView recyclerView = (RecyclerView) view;
188                         RecyclerView.ViewHolder viewHolder =
189                                 recyclerView.findViewHolderForAdapterPosition(index);
190                         assert viewHolder != null;
191                         viewHolder.itemView.findViewById(R.id.action_button).performClick();
192                     }
193                 });
194     }
195 
196     /** Close the first tab in grid tab switcher. */
closeFirstTabInTabSwitcher()197     public static void closeFirstTabInTabSwitcher() {
198         closeNthTabInTabSwitcher(0);
199     }
200 
201     /**
202      * Close the Nth tab in grid tab switcher.
203      * @param index The index of the target tab to close.
204      */
closeNthTabInTabSwitcher(int index)205     static void closeNthTabInTabSwitcher(int index) {
206         onView(allOf(withParent(withId(R.id.compositor_view_holder)), withId(R.id.tab_list_view)))
207                 .perform(new ViewAction() {
208                     @Override
209                     public Matcher<View> getConstraints() {
210                         return isDisplayed();
211                     }
212 
213                     @Override
214                     public String getDescription() {
215                         return "close tab with index " + String.valueOf(index);
216                     }
217 
218                     @Override
219                     public void perform(UiController uiController, View view) {
220                         RecyclerView recyclerView = (RecyclerView) view;
221                         RecyclerView.ViewHolder viewHolder =
222                                 recyclerView.findViewHolderForAdapterPosition(index);
223                         assert viewHolder != null;
224                         viewHolder.itemView.findViewById(R.id.action_button).performClick();
225                     }
226                 });
227     }
228 
229     /**
230      * Check whether the tab list in {@link android.widget.PopupWindow} is completely showing. This
231      * can be used for tab grid dialog and tab group popup UI.
232      * @param cta  The current running activity.
233      * @return Whether the tab list in a popup component is completely showing.
234      */
isPopupTabListCompletelyShowing(ChromeTabbedActivity cta)235     static boolean isPopupTabListCompletelyShowing(ChromeTabbedActivity cta) {
236         boolean isShowing = true;
237         try {
238             onView(withId(R.id.tab_list_view))
239                     .inRoot(withDecorView(not(cta.getWindow().getDecorView())))
240                     .check(matches(isCompletelyDisplayed()))
241                     .check((v, e) -> assertEquals(1f, v.getAlpha(), 0.0));
242         } catch (NoMatchingRootException | AssertionError e) {
243             isShowing = false;
244         } catch (Exception e) {
245             assert false : "error when inspecting pop up tab list.";
246         }
247         return isShowing;
248     }
249 
250     /**
251      * Check whether the tab list in {@link android.widget.PopupWindow} is completely hidden. This
252      * can be used for tab grid dialog and tab group popup UI.
253      * @param cta  The current running activity.
254      * @return Whether the tab list in a popup component is completely hidden.
255      */
isPopupTabListCompletelyHidden(ChromeTabbedActivity cta)256     static boolean isPopupTabListCompletelyHidden(ChromeTabbedActivity cta) {
257         boolean isHidden = false;
258         try {
259             onView(withId(R.id.tab_list_view))
260                     .inRoot(withDecorView(not(cta.getWindow().getDecorView())))
261                     .check(matches(isDisplayed()));
262         } catch (NoMatchingRootException e) {
263             isHidden = true;
264         } catch (Exception e) {
265             assert false : "error when inspecting pop up tab list.";
266         }
267         return isHidden;
268     }
269 
270     /**
271      * Verify the number of tabs in the tab list showing in a popup component.
272      * @param cta   The current running activity.
273      * @param count The count of the tabs in the tab list.
274      */
verifyShowingPopupTabList(ChromeTabbedActivity cta, int count)275     static void verifyShowingPopupTabList(ChromeTabbedActivity cta, int count) {
276         onView(withId(R.id.tab_list_view))
277                 .inRoot(withDecorView(not(cta.getWindow().getDecorView())))
278                 .check(ChildrenCountAssertion.havingTabCount(count));
279     }
280 
281     /**
282      * Merge all normal tabs into a single tab group.
283      * @param cta   The current running activity.
284      */
mergeAllNormalTabsToAGroup(ChromeTabbedActivity cta)285     public static void mergeAllNormalTabsToAGroup(ChromeTabbedActivity cta) {
286         mergeAllTabsToAGroup(cta, false);
287     }
288 
289     /**
290      * Merge all incognito tabs into a single tab group.
291      * @param cta   The current running activity.
292      */
mergeAllIncognitoTabsToAGroup(ChromeTabbedActivity cta)293     static void mergeAllIncognitoTabsToAGroup(ChromeTabbedActivity cta) {
294         mergeAllTabsToAGroup(cta, true);
295     }
296 
297     /**
298      * Merge all tabs in one tab model into a single tab group.
299      * @param cta           The current running activity.
300      * @param isIncognito   indicates the tab model that we are creating tab group in.
301      */
mergeAllTabsToAGroup(ChromeTabbedActivity cta, boolean isIncognito)302     static void mergeAllTabsToAGroup(ChromeTabbedActivity cta, boolean isIncognito) {
303         List<Tab> tabGroup = new ArrayList<>();
304         TabModel tabModel = cta.getTabModelSelector().getModel(isIncognito);
305         for (int i = 0; i < tabModel.getCount(); i++) {
306             tabGroup.add(tabModel.getTabAt(i));
307         }
308         createTabGroup(cta, isIncognito, tabGroup);
309         assertTrue(cta.getTabModelSelector().getTabModelFilterProvider().getCurrentTabModelFilter()
310                            instanceof TabGroupModelFilter);
311         TabGroupModelFilter filter = (TabGroupModelFilter) cta.getTabModelSelector()
312                                              .getTabModelFilterProvider()
313                                              .getTabModelFilter(isIncognito);
314         assertEquals(1, filter.getCount());
315     }
316 
317     /**
318      * Verify that current tab models hold correct number of tabs.
319      * @param cta            The current running activity.
320      * @param normalTabs     The correct number of normal tabs.
321      * @param incognitoTabs  The correct number of incognito tabs.
322      */
verifyTabModelTabCount( ChromeTabbedActivity cta, int normalTabs, int incognitoTabs)323     public static void verifyTabModelTabCount(
324             ChromeTabbedActivity cta, int normalTabs, int incognitoTabs) {
325         CriteriaHelper.pollUiThread(() -> {
326             Criteria.checkThat(
327                     cta.getTabModelSelector().getModel(false).getCount(), is(normalTabs));
328         });
329         CriteriaHelper.pollUiThread(() -> {
330             Criteria.checkThat(
331                     cta.getTabModelSelector().getModel(true).getCount(), is(incognitoTabs));
332         });
333     }
334 
335     /**
336      * Verify there are correct number of cards in tab switcher.
337      * @param cta       The current running activity.
338      * @param count     The correct number of cards in tab switcher.
339      */
verifyTabSwitcherCardCount(ChromeTabbedActivity cta, int count)340     public static void verifyTabSwitcherCardCount(ChromeTabbedActivity cta, int count) {
341         assertTrue(cta.getLayoutManager().overviewVisible());
342         onView(allOf(withParent(withId(org.chromium.chrome.R.id.compositor_view_holder)),
343                        withId(R.id.tab_list_view)))
344                 .check(ChildrenCountAssertion.havingTabCount(count));
345     }
346 
347     /**
348      * Verify there are correct number of favicons in tab strip.
349      * @param cta       The current running activity.
350      * @param count     The correct number of favicons in tab strip.
351      */
verifyTabStripFaviconCount(ChromeTabbedActivity cta, int count)352     static void verifyTabStripFaviconCount(ChromeTabbedActivity cta, int count) {
353         assertFalse(cta.getLayoutManager().overviewVisible());
354         onView(allOf(withParent(withId(R.id.toolbar_container_view)), withId(R.id.tab_list_view)))
355                 .check(ChildrenCountAssertion.havingTabCount(count));
356     }
357 
358     /**
359      * Create a tab group using {@code tabs}.
360      * @param cta             The current running activity.
361      * @param isIncognito     Whether the group is in normal model or incognito model.
362      * @param tabs            A list of {@link Tab} to create group.
363      */
createTabGroup( ChromeTabbedActivity cta, boolean isIncognito, List<Tab> tabs)364     public static void createTabGroup(
365             ChromeTabbedActivity cta, boolean isIncognito, List<Tab> tabs) {
366         if (tabs.size() == 0) return;
367         assert cta.getTabModelSelector().getTabModelFilterProvider().getCurrentTabModelFilter()
368                         instanceof TabGroupModelFilter;
369         TabGroupModelFilter filter = (TabGroupModelFilter) cta.getTabModelSelector()
370                                              .getTabModelFilterProvider()
371                                              .getTabModelFilter(isIncognito);
372         Tab rootTab = tabs.get(0);
373         for (int i = 1; i < tabs.size(); i++) {
374             Tab tab = tabs.get(i);
375             assertEquals(isIncognito, tab.isIncognito());
376             TestThreadUtils.runOnUiThreadBlocking(
377                     () -> filter.mergeTabsToGroup(tab.getId(), rootTab.getId()));
378         }
379     }
380 
381     /**
382      * Create a {@link OverviewModeBehaviorWatcher} to inspect overview show.
383      */
createOverviewShowWatcher(ChromeTabbedActivity cta)384     public static OverviewModeBehaviorWatcher createOverviewShowWatcher(ChromeTabbedActivity cta) {
385         return new OverviewModeBehaviorWatcher(cta.getLayoutManager(), true, false);
386     }
387 
388     /**
389      * Create a {@link OverviewModeBehaviorWatcher} to inspect overview hide.
390      */
createOverviewHideWatcher(ChromeTabbedActivity cta)391     public static OverviewModeBehaviorWatcher createOverviewHideWatcher(ChromeTabbedActivity cta) {
392         return new OverviewModeBehaviorWatcher(cta.getLayoutManager(), false, true);
393     }
394 
395     /**
396      * Rotate device to the target orientation. Do nothing if the screen is already in that
397      * orientation.
398      * @param cta             The current running activity.
399      * @param orientation     The target orientation we want the screen to rotate to.
400      */
rotateDeviceToOrientation(ChromeTabbedActivity cta, int orientation)401     public static void rotateDeviceToOrientation(ChromeTabbedActivity cta, int orientation) {
402         if (cta.getResources().getConfiguration().orientation == orientation) return;
403         assertTrue(orientation == Configuration.ORIENTATION_LANDSCAPE
404                 || orientation == Configuration.ORIENTATION_PORTRAIT);
405         cta.setRequestedOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE
406                         ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
407                         : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
408         CriteriaHelper.pollUiThread(() -> {
409             Criteria.checkThat(cta.getResources().getConfiguration().orientation, is(orientation));
410         });
411     }
412 
413     /**
414      * @return whether animators are enabled on device by checking whether the animation duration
415      * scale is set to 0.0.
416      */
areAnimatorsEnabled()417     public static boolean areAnimatorsEnabled() {
418         // We default to assuming that animations are enabled in case ANIMATOR_DURATION_SCALE is not
419         // defined.
420         final float defaultScale = 1f;
421         float durationScale =
422                 Settings.Global.getFloat(ContextUtils.getApplicationContext().getContentResolver(),
423                         Settings.Global.ANIMATOR_DURATION_SCALE, defaultScale);
424         return !(durationScale == 0.0);
425     }
426 
427     /**
428      * Make Chrome have {@code numTabs} of regular Tabs and {@code numIncognitoTabs} of incognito
429      * tabs with {@code url} loaded.
430      * @param rule The {@link ChromeTabbedActivityTestRule}.
431      * @param numTabs The number of regular tabs.
432      * @param numIncognitoTabs The number of incognito tabs.
433      * @param url The URL to load.
434      */
prepareTabsWithThumbnail(ChromeTabbedActivityTestRule rule, int numTabs, int numIncognitoTabs, @Nullable String url)435     public static void prepareTabsWithThumbnail(ChromeTabbedActivityTestRule rule, int numTabs,
436             int numIncognitoTabs, @Nullable String url) {
437         assertTrue(numTabs >= 1);
438         assertTrue(numIncognitoTabs >= 0);
439 
440         assertEquals(1, rule.getActivity().getTabModelSelector().getModel(false).getCount());
441         assertEquals(0, rule.getActivity().getTabModelSelector().getModel(true).getCount());
442 
443         if (url != null) rule.loadUrl(url);
444         if (numTabs > 1) {
445             // When Chrome started, there is already one Tab created by default.
446             createTabsWithThumbnail(rule, numTabs - 1, url, false);
447         }
448         if (numIncognitoTabs > 0) createTabsWithThumbnail(rule, numIncognitoTabs, url, true);
449 
450         assertEquals(numTabs, rule.getActivity().getTabModelSelector().getModel(false).getCount());
451         assertEquals(numIncognitoTabs,
452                 rule.getActivity().getTabModelSelector().getModel(true).getCount());
453         if (url != null) {
454             verifyAllTabsHaveUrl(rule.getActivity().getTabModelSelector().getModel(false), url);
455             verifyAllTabsHaveUrl(rule.getActivity().getTabModelSelector().getModel(true), url);
456         }
457     }
458 
verifyAllTabsHaveUrl(TabModel tabModel, String url)459     private static void verifyAllTabsHaveUrl(TabModel tabModel, String url) {
460         for (int i = 0; i < tabModel.getCount(); i++) {
461             assertEquals(url, ChromeTabUtils.getUrlStringOnUiThread(tabModel.getTabAt(i)));
462         }
463     }
464 
465     /**
466      * Create {@code numTabs} of {@link Tab}s with {@code url} loaded to Chrome.
467      * Note that if the test doesn't care about thumbnail, use {@link TabUiTestHelper#createTabs}
468      * instead since it's faster.
469      *
470      * @param rule The {@link ChromeTabbedActivityTestRule}.
471      * @param numTabs The number of tabs to create.
472      * @param url The URL to load. Skip loading when null, but the thumbnail for the NTP might not
473      *            be saved.
474      * @param isIncognito Whether the tab is incognito tab.
475      */
createTabsWithThumbnail(ChromeTabbedActivityTestRule rule, int numTabs, @Nullable String url, boolean isIncognito)476     private static void createTabsWithThumbnail(ChromeTabbedActivityTestRule rule, int numTabs,
477             @Nullable String url, boolean isIncognito) {
478         assertTrue(numTabs >= 1);
479 
480         int previousTabCount =
481                 rule.getActivity().getTabModelSelector().getModel(isIncognito).getCount();
482 
483         for (int i = 0; i < numTabs; i++) {
484             TabModel previousTabModel = rule.getActivity().getTabModelSelector().getCurrentModel();
485             int previousTabIndex = previousTabModel.index();
486             Tab previousTab = previousTabModel.getTabAt(previousTabIndex);
487 
488             ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(),
489                     rule.getActivity(), isIncognito, url == null);
490 
491             if (url != null) rule.loadUrl(url);
492 
493             TabModel currentTabModel = rule.getActivity().getTabModelSelector().getCurrentModel();
494             int currentTabIndex = currentTabModel.index();
495 
496             boolean fixPendingReadbacks =
497                     rule.getActivity().getTabContentManager().getPendingReadbacksForTesting() != 0;
498 
499             // When there are pending readbacks due to detached Tabs, try to fix it by switching
500             // back to that tab.
501             if (fixPendingReadbacks && previousTabIndex != TabModel.INVALID_TAB_INDEX) {
502                 // clang-format off
503                 TestThreadUtils.runOnUiThreadBlocking(() ->
504                         previousTabModel.setIndex(previousTabIndex, TabSelectionType.FROM_USER)
505                 );
506                 // clang-format on
507             }
508 
509             checkThumbnailsExist(previousTab);
510 
511             if (fixPendingReadbacks) {
512                 // clang-format off
513                 TestThreadUtils.runOnUiThreadBlocking(() -> currentTabModel.setIndex(
514                         currentTabIndex, TabSelectionType.FROM_USER)
515                 );
516                 // clang-format on
517             }
518         }
519 
520         ChromeTabUtils.waitForTabPageLoaded(
521                 rule.getActivity().getActivityTab(), null, null, WAIT_TIMEOUT_SECONDS * 10);
522 
523         assertEquals(numTabs + previousTabCount,
524                 rule.getActivity().getTabModelSelector().getModel(isIncognito).getCount());
525 
526         CriteriaHelper.pollUiThread(() -> {
527             Criteria.checkThat(
528                     rule.getActivity().getTabContentManager().getPendingReadbacksForTesting(),
529                     is(0));
530         });
531     }
532 
verifyAllTabsHaveThumbnail(TabModel tabModel)533     public static void verifyAllTabsHaveThumbnail(TabModel tabModel) {
534         for (int i = 0; i < tabModel.getCount(); i++) {
535             checkThumbnailsExist(tabModel.getTabAt(i));
536         }
537     }
538 
checkThumbnailsExist(Tab tab)539     public static void checkThumbnailsExist(Tab tab) {
540         File etc1File = TabContentManager.getTabThumbnailFileEtc1(tab);
541         CriteriaHelper.pollInstrumentationThread(etc1File::exists,
542                 "The thumbnail " + etc1File.getName() + " is not found",
543                 DEFAULT_MAX_TIME_TO_POLL * 10, DEFAULT_POLLING_INTERVAL);
544 
545         File jpegFile = TabContentManager.getTabThumbnailFileJpeg(tab.getId());
546         CriteriaHelper.pollInstrumentationThread(jpegFile::exists,
547                 "The thumbnail " + jpegFile.getName() + " is not found",
548                 DEFAULT_MAX_TIME_TO_POLL * 10, DEFAULT_POLLING_INTERVAL);
549     }
550 
551     /**
552      * Verify that the snack bar is showing and click on the snack bar button. Right now it is only
553      * used for undoing a tab closure. This should be used with
554      * CriteriaHelper.pollInstrumentationThread().
555      * @return whether the visibility checking and the clicking have finished or not.
556      */
verifyUndoBarShowingAndClickUndo()557     public static boolean verifyUndoBarShowingAndClickUndo() {
558         boolean hasClicked = true;
559         try {
560             onView(withId(R.id.snackbar_button)).check(matches(isCompletelyDisplayed()));
561             onView(withId(R.id.snackbar_button)).perform(click());
562         } catch (NoMatchingRootException | AssertionError e) {
563             hasClicked = false;
564         } catch (Exception e) {
565             assert false : "error when verifying undo snack bar.";
566         }
567         return hasClicked;
568     }
569 
570     /**
571      * Get the {@link GeneralSwipeAction} used to perform a swipe-to-dismiss action in tab grid
572      * layout.
573      * @param isLeftToRight  decides whether the swipe is from left to right or from right to left.
574      * @return {@link GeneralSwipeAction} to perform swipe-to-dismiss.
575      */
getSwipeToDismissAction(boolean isLeftToRight)576     public static GeneralSwipeAction getSwipeToDismissAction(boolean isLeftToRight) {
577         if (isLeftToRight) {
578             return new GeneralSwipeAction(Swipe.FAST, GeneralLocation.CENTER_LEFT,
579                     GeneralLocation.CENTER_RIGHT, Press.FINGER);
580         } else {
581             return new GeneralSwipeAction(Swipe.FAST, GeneralLocation.CENTER_RIGHT,
582                     GeneralLocation.CENTER_LEFT, Press.FINGER);
583         }
584     }
585 
586     /** Finishes the given activity and do tab_ui-specific cleanup. */
finishActivity(final Activity activity)587     public static void finishActivity(final Activity activity) throws Exception {
588         ApplicationTestUtils.finishActivity(activity);
589         PseudoTab.clearForTesting();
590     }
591 
592     /**
593      * Click on the incognito toggle within grid tab switcher top toolbar to switch between normal
594      * and incognito tab model.
595      * @param cta          The current running activity.
596      * @param isIncognito  indicates whether the incognito or normal tab model is selected after
597      *         switch.
598      */
switchTabModel(ChromeTabbedActivity cta, boolean isIncognito)599     public static void switchTabModel(ChromeTabbedActivity cta, boolean isIncognito) {
600         assertTrue(isIncognito != cta.getTabModelSelector().isIncognitoSelected());
601         assertTrue(cta.getOverviewModeBehavior().overviewVisible());
602 
603         onView(withContentDescription(isIncognito
604                                ? R.string.accessibility_tab_switcher_incognito_stack
605                                : R.string.accessibility_tab_switcher_standard_stack))
606                 .perform(click());
607 
608         CriteriaHelper.pollUiThread(() -> {
609             Criteria.checkThat(cta.getTabModelSelector().isIncognitoSelected(), is(isIncognito));
610         });
611         // Wait for tab list recyclerView to finish animation after tab model switch.
612         RecyclerView recyclerView = cta.findViewById(R.id.tab_list_view);
613         waitForStableRecyclerView(recyclerView);
614     }
615 
616     /**
617      * Implementation of {@link ViewAssertion} to verify the {@link RecyclerView} has correct number
618      * of children.
619      */
620     public static class ChildrenCountAssertion implements ViewAssertion {
621         @IntDef({ChildrenType.TAB, ChildrenType.TAB_SUGGESTION_MESSAGE})
622         @Retention(RetentionPolicy.SOURCE)
623         public @interface ChildrenType {
624             int TAB = 0;
625             int TAB_SUGGESTION_MESSAGE = 1;
626         }
627 
628         private int mExpectedCount;
629         @ChildrenType
630         private int mExpectedChildrenType;
631 
havingTabCount(int tabCount)632         public static ChildrenCountAssertion havingTabCount(int tabCount) {
633             return new ChildrenCountAssertion(ChildrenType.TAB, tabCount);
634         }
635 
havingTabSuggestionMessageCardCount(int count)636         public static ChildrenCountAssertion havingTabSuggestionMessageCardCount(int count) {
637             return new ChildrenCountAssertion(ChildrenType.TAB_SUGGESTION_MESSAGE, count);
638         }
639 
ChildrenCountAssertion(@hildrenType int expectedChildrenType, int expectedCount)640         public ChildrenCountAssertion(@ChildrenType int expectedChildrenType, int expectedCount) {
641             mExpectedChildrenType = expectedChildrenType;
642             mExpectedCount = expectedCount;
643         }
644 
645         @Override
check(View view, NoMatchingViewException noMatchException)646         public void check(View view, NoMatchingViewException noMatchException) {
647             if (noMatchException != null) throw noMatchException;
648 
649             switch (mExpectedChildrenType) {
650                 case ChildrenType.TAB:
651                     checkTabCount(view);
652                     break;
653                 case ChildrenType.TAB_SUGGESTION_MESSAGE:
654                     checkTabSuggestionMessageCard(view);
655                     break;
656             }
657         }
658 
checkTabCount(View view)659         private void checkTabCount(View view) {
660             RecyclerView recyclerView = ((RecyclerView) view);
661             recyclerView.setItemAnimator(null); // Disable animation to reduce flakiness.
662             RecyclerView.Adapter adapter = recyclerView.getAdapter();
663 
664             int itemCount = adapter.getItemCount();
665             int nonTabCardCount = 0;
666 
667             for (int i = 0; i < itemCount; i++) {
668                 RecyclerView.ViewHolder viewHolder =
669                         recyclerView.findViewHolderForAdapterPosition(i);
670                 if (viewHolder == null) return;
671                 if (viewHolder.getItemViewType() != TabProperties.UiType.CLOSABLE
672                         && viewHolder.getItemViewType() != TabProperties.UiType.SELECTABLE
673                         && viewHolder.getItemViewType() != TabProperties.UiType.STRIP) {
674                     nonTabCardCount += 1;
675                 }
676             }
677             assertEquals(mExpectedCount + nonTabCardCount, itemCount);
678         }
679 
checkTabSuggestionMessageCard(View view)680         private void checkTabSuggestionMessageCard(View view) {
681             RecyclerView recyclerView = ((RecyclerView) view);
682             recyclerView.setItemAnimator(null); // Disable animation to reduce flakiness.
683             RecyclerView.Adapter adapter = recyclerView.getAdapter();
684 
685             int itemCount = adapter.getItemCount();
686             int tabSuggestionMessageCount = 0;
687 
688             for (int i = 0; i < itemCount; i++) {
689                 RecyclerView.ViewHolder viewHolder =
690                         recyclerView.findViewHolderForAdapterPosition(i);
691                 if (viewHolder == null) return;
692                 if (viewHolder.getItemViewType() == TabProperties.UiType.MESSAGE) {
693                     tabSuggestionMessageCount += 1;
694                 }
695             }
696             assertEquals(mExpectedCount, tabSuggestionMessageCount);
697         }
698     }
699 }
700