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