1 // Copyright 2020 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.doesNotExist; 11 import static androidx.test.espresso.assertion.ViewAssertions.matches; 12 import static androidx.test.espresso.matcher.RootMatchers.withDecorView; 13 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 14 import static androidx.test.espresso.matcher.ViewMatchers.withId; 15 import static androidx.test.espresso.matcher.ViewMatchers.withParent; 16 import static androidx.test.espresso.matcher.ViewMatchers.withText; 17 18 import static org.hamcrest.CoreMatchers.allOf; 19 import static org.hamcrest.Matchers.not; 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertFalse; 22 import static org.junit.Assert.assertTrue; 23 24 import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.clickFirstCardFromTabSwitcher; 25 import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.closeFirstTabInTabSwitcher; 26 import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.enterTabSwitcher; 27 import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.getSwipeToDismissAction; 28 import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.rotateDeviceToOrientation; 29 import static org.chromium.chrome.browser.tasks.tab_management.TabUiTestHelper.verifyTabSwitcherCardCount; 30 import static org.chromium.chrome.test.util.ViewUtils.onViewWaiting; 31 32 import android.content.pm.ActivityInfo; 33 import android.content.res.Configuration; 34 import android.graphics.drawable.Animatable; 35 import android.support.test.InstrumentationRegistry; 36 import android.support.test.uiautomator.UiDevice; 37 import android.view.View; 38 import android.widget.ImageView; 39 import android.widget.TextView; 40 41 import androidx.recyclerview.widget.RecyclerView; 42 import androidx.test.espresso.NoMatchingRootException; 43 import androidx.test.espresso.contrib.RecyclerViewActions; 44 import androidx.test.filters.MediumTest; 45 46 import org.junit.After; 47 import org.junit.Before; 48 import org.junit.ClassRule; 49 import org.junit.Rule; 50 import org.junit.Test; 51 import org.junit.runner.RunWith; 52 53 import org.chromium.base.test.util.CommandLineFlags; 54 import org.chromium.base.test.util.CriteriaHelper; 55 import org.chromium.base.test.util.Feature; 56 import org.chromium.base.test.util.Restriction; 57 import org.chromium.chrome.browser.ChromeTabbedActivity; 58 import org.chromium.chrome.browser.compositor.layouts.Layout; 59 import org.chromium.chrome.browser.flags.ChromeFeatureList; 60 import org.chromium.chrome.browser.flags.ChromeSwitches; 61 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; 62 import org.chromium.chrome.features.start_surface.StartSurfaceLayout; 63 import org.chromium.chrome.tab_ui.R; 64 import org.chromium.chrome.test.ChromeJUnit4ClassRunner; 65 import org.chromium.chrome.test.ChromeTabbedActivityTestRule; 66 import org.chromium.chrome.test.util.ChromeRenderTestRule; 67 import org.chromium.chrome.test.util.ChromeTabUtils; 68 import org.chromium.chrome.test.util.browser.Features; 69 import org.chromium.content_public.browser.test.util.TestThreadUtils; 70 import org.chromium.ui.modaldialog.ModalDialogManager; 71 import org.chromium.ui.modaldialog.ModalDialogProperties; 72 import org.chromium.ui.test.util.DisableAnimationsTestRule; 73 import org.chromium.ui.test.util.UiRestriction; 74 75 import java.io.IOException; 76 77 /** End-to-end tests for TabGridIph component. */ 78 @RunWith(ChromeJUnit4ClassRunner.class) 79 // clang-format off 80 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, 81 "enable-features=IPH_TabGroupsDragAndDrop<TabGroupsDragAndDrop", 82 "force-fieldtrials=TabGroupsDragAndDrop/Enabled", 83 "force-fieldtrial-params=TabGroupsDragAndDrop.Enabled:availability/any/" + 84 "event_trigger/" + 85 "name%3Aiph_tabgroups_drag_and_drop;comparator%3A==0;window%3A30;storage%3A365/" + 86 "event_trigger2/" + 87 "name%3Aiph_tabgroups_drag_and_drop;comparator%3A<2;window%3A90;storage%3A365/" + 88 "event_used/" + 89 "name%3Atab_drag_and_drop_to_group;comparator%3A==0;window%3A365;storage%3A365/" + 90 "session_rate/<1" 91 }) 92 @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE) 93 @Features.EnableFeatures({ChromeFeatureList.TAB_GROUPS_ANDROID}) 94 @Features.DisableFeatures(ChromeFeatureList.CLOSE_TAB_SUGGESTIONS) 95 public class TabGridIphTest { 96 // clang-format on 97 private ModalDialogManager mModalDialogManager; 98 99 // Disable animations to reduce flakiness. 100 @ClassRule 101 public static DisableAnimationsTestRule sEnableAnimationsRule = new DisableAnimationsTestRule(); 102 103 @Rule 104 public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule(); 105 106 @Rule 107 public ChromeRenderTestRule mRenderTestRule = 108 ChromeRenderTestRule.Builder.withPublicCorpus().build(); 109 110 @Before setUp()111 public void setUp() { 112 mActivityTestRule.startMainActivityOnBlankPage(); 113 Layout layout = mActivityTestRule.getActivity().getLayoutManager().getOverviewLayout(); 114 assertTrue(layout instanceof StartSurfaceLayout); 115 CriteriaHelper.pollUiThread( 116 mActivityTestRule.getActivity().getTabModelSelector()::isTabStateInitialized); 117 mModalDialogManager = TestThreadUtils.runOnUiThreadBlockingNoException( 118 mActivityTestRule.getActivity()::getModalDialogManager); 119 } 120 121 @After tearDown()122 public void tearDown() { 123 mActivityTestRule.getActivity().setRequestedOrientation( 124 ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); 125 } 126 127 @Test 128 @MediumTest testShowAndHideIphDialog()129 public void testShowAndHideIphDialog() { 130 final ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 131 132 enterTabSwitcher(cta); 133 CriteriaHelper.pollUiThread( 134 TabSwitcherCoordinator::hasAppendedMessagesForTesting); 135 // Check the IPH message card is showing and open the IPH dialog. 136 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 137 onView(allOf(withId(R.id.action_button), withParent(withId(R.id.tab_grid_message_item)))) 138 .perform(click()); 139 verifyIphDialogShowing(cta); 140 141 // Exit by clicking the "OK" button. 142 exitIphDialogByClickingButton(cta); 143 verifyIphDialogHiding(cta); 144 145 // Check the IPH message card is showing and open the IPH dialog. 146 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 147 onView(allOf(withId(R.id.action_button), withParent(withId(R.id.tab_grid_message_item)))) 148 .perform(click()); 149 verifyIphDialogShowing(cta); 150 151 // Press back should dismiss the IPH dialog. 152 pressBack(); 153 verifyIphDialogHiding(cta); 154 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 155 156 // Check the IPH message card is showing and open the IPH dialog. 157 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 158 onView(allOf(withId(R.id.action_button), withParent(withId(R.id.tab_grid_message_item)))) 159 .perform(click()); 160 verifyIphDialogShowing(cta); 161 162 // Click outside of the dialog area to close the IPH dialog. 163 View dialogView = mModalDialogManager.getCurrentDialogForTest().get( 164 ModalDialogProperties.CUSTOM_VIEW); 165 int[] location = new int[2]; 166 // Get the position of the dialog view and click slightly above so that we essentially click 167 // on the scrim. 168 dialogView.getLocationOnScreen(location); 169 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 170 .click(location[0], location[1] / 2); 171 verifyIphDialogHiding(cta); 172 } 173 174 @Test 175 @MediumTest testDismissIphItem()176 public void testDismissIphItem() throws Exception { 177 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 178 179 enterTabSwitcher(cta); 180 CriteriaHelper.pollUiThread( 181 TabSwitcherCoordinator::hasAppendedMessagesForTesting); 182 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 183 184 // Restart chrome to verify that IPH message card is still there. 185 TabUiTestHelper.finishActivity(mActivityTestRule.getActivity()); 186 mActivityTestRule.startMainActivityFromLauncher(); 187 cta = mActivityTestRule.getActivity(); 188 enterTabSwitcher(cta); 189 CriteriaHelper.pollUiThread( 190 TabSwitcherCoordinator::hasAppendedMessagesForTesting); 191 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 192 193 // Remove the message card and dismiss the feature by clicking close button. 194 onView(allOf(withId(R.id.close_button), withParent(withId(R.id.tab_grid_message_item)))) 195 .perform(click()); 196 onView(withId(R.id.tab_grid_message_item)).check(doesNotExist()); 197 198 // Restart chrome to verify that IPH message card no longer shows. 199 TabUiTestHelper.finishActivity(mActivityTestRule.getActivity()); 200 mActivityTestRule.startMainActivityFromLauncher(); 201 cta = mActivityTestRule.getActivity(); 202 enterTabSwitcher(cta); 203 onView(withId(R.id.tab_grid_message_item)).check(doesNotExist()); 204 } 205 206 @Test 207 @MediumTest 208 @Feature({"RenderTest"}) testRenderIph_Portrait()209 public void testRenderIph_Portrait() throws IOException { 210 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 211 212 enterTabSwitcher(cta); 213 CriteriaHelper.pollUiThread( 214 TabSwitcherCoordinator::hasAppendedMessagesForTesting); 215 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 216 217 mRenderTestRule.render( 218 cta.findViewById(R.id.tab_grid_message_item), "iph_entrance_portrait"); 219 } 220 221 @Test 222 @MediumTest 223 @Feature({"RenderTest"}) testRenderIph_Landscape()224 public void testRenderIph_Landscape() throws IOException { 225 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 226 227 enterTabSwitcher(cta); 228 rotateDeviceToOrientation(cta, Configuration.ORIENTATION_LANDSCAPE); 229 CriteriaHelper.pollUiThread( 230 TabSwitcherCoordinator::hasAppendedMessagesForTesting); 231 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 232 233 mRenderTestRule.render( 234 cta.findViewById(R.id.tab_grid_message_item), "iph_entrance_landscape"); 235 } 236 237 @Test 238 @MediumTest 239 @Feature({"RenderTest"}) testRenderIphDialog_Portrait()240 public void testRenderIphDialog_Portrait() throws IOException { 241 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 242 243 enterTabSwitcher(cta); 244 CriteriaHelper.pollUiThread(TabSwitcherCoordinator::hasAppendedMessagesForTesting); 245 onView(allOf(withId(R.id.action_button), withParent(withId(R.id.tab_grid_message_item)))) 246 .perform(click()); 247 verifyIphDialogShowing(cta); 248 249 View iphDialogView = mModalDialogManager.getCurrentDialogForTest().get( 250 ModalDialogProperties.CUSTOM_VIEW); 251 // Freeze animation and wait until animation is really frozen. 252 ChromeRenderTestRule.sanitize(iphDialogView); 253 ImageView iphImageView = iphDialogView.findViewById(R.id.animation_drawable); 254 Animatable iphAnimation = (Animatable) iphImageView.getDrawable(); 255 CriteriaHelper.pollUiThread(() -> !iphAnimation.isRunning()); 256 257 mRenderTestRule.render(iphDialogView, "iph_dialog_portrait"); 258 } 259 260 @Test 261 @MediumTest 262 @Feature({"RenderTest"}) testRenderIphDialog_Landscape()263 public void testRenderIphDialog_Landscape() throws IOException { 264 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 265 266 enterTabSwitcher(cta); 267 rotateDeviceToOrientation(cta, Configuration.ORIENTATION_LANDSCAPE); 268 CriteriaHelper.pollUiThread(TabSwitcherCoordinator::hasAppendedMessagesForTesting); 269 // Scroll to the position of the IPH entrance so that it is completely showing for Espresso 270 // click. 271 onView(allOf(withParent(withId(R.id.compositor_view_holder)), withId(R.id.tab_list_view))) 272 .perform(RecyclerViewActions.scrollToPosition(1)); 273 onView(allOf(withId(R.id.action_button), withParent(withId(R.id.tab_grid_message_item)))) 274 .perform(click()); 275 verifyIphDialogShowing(cta); 276 277 View iphDialogView = mModalDialogManager.getCurrentDialogForTest().get( 278 ModalDialogProperties.CUSTOM_VIEW); 279 // Freeze animation and wait until animation is really frozen. 280 ChromeRenderTestRule.sanitize(iphDialogView); 281 ImageView iphImageView = iphDialogView.findViewById(R.id.animation_drawable); 282 Animatable iphAnimation = (Animatable) iphImageView.getDrawable(); 283 CriteriaHelper.pollUiThread(() -> !iphAnimation.isRunning()); 284 285 mRenderTestRule.render(iphDialogView, "iph_dialog_landscape"); 286 } 287 288 @Test 289 @MediumTest testIphItemChangeWithLastTab()290 public void testIphItemChangeWithLastTab() { 291 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 292 293 enterTabSwitcher(cta); 294 CriteriaHelper.pollUiThread(TabSwitcherCoordinator::hasAppendedMessagesForTesting); 295 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 296 297 // Close the last tab in tab switcher and the IPH item should not be showing. 298 closeFirstTabInTabSwitcher(); 299 CriteriaHelper.pollUiThread(() -> !TabSwitcherCoordinator.hasAppendedMessagesForTesting()); 300 verifyTabSwitcherCardCount(cta, 0); 301 onView(withId(R.id.tab_grid_message_item)).check(doesNotExist()); 302 303 // Undo the closure of the last tab and the IPH item should reshow. 304 CriteriaHelper.pollInstrumentationThread(TabUiTestHelper::verifyUndoBarShowingAndClickUndo); 305 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 306 307 // Close the last tab in the tab switcher. 308 closeFirstTabInTabSwitcher(); 309 CriteriaHelper.pollUiThread(() -> !TabSwitcherCoordinator.hasAppendedMessagesForTesting()); 310 verifyTabSwitcherCardCount(cta, 0); 311 onView(withId(R.id.tab_grid_message_item)).check(doesNotExist()); 312 313 // Add the first tab to an empty tab switcher and the IPH item should show. 314 ChromeTabUtils.newTabFromMenu( 315 InstrumentationRegistry.getInstrumentation(), cta, false, true); 316 enterTabSwitcher(cta); 317 verifyTabSwitcherCardCount(cta, 1); 318 CriteriaHelper.pollUiThread(TabSwitcherCoordinator::hasAppendedMessagesForTesting); 319 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 320 } 321 322 @Test 323 @MediumTest testSwipeToDismiss_IPH()324 public void testSwipeToDismiss_IPH() { 325 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 326 enterTabSwitcher(cta); 327 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 328 RecyclerView.ViewHolder viewHolder = ((RecyclerView) cta.findViewById(R.id.tab_list_view)) 329 .findViewHolderForAdapterPosition(1); 330 assertEquals(TabProperties.UiType.MESSAGE, viewHolder.getItemViewType()); 331 332 onView(allOf(withParent(withId(R.id.compositor_view_holder)), withId(R.id.tab_list_view))) 333 .perform(RecyclerViewActions.actionOnItemAtPosition( 334 1, getSwipeToDismissAction(true))); 335 336 onView(withId(R.id.tab_grid_message_item)).check(doesNotExist()); 337 } 338 339 @Test 340 @MediumTest testNotShowIPHInMultiWindowMode()341 public void testNotShowIPHInMultiWindowMode() { 342 ChromeTabbedActivity cta = mActivityTestRule.getActivity(); 343 enterTabSwitcher(cta); 344 onView(withId(R.id.tab_grid_message_item)).check(matches(isDisplayed())); 345 346 // Mock that user enters multi-window mode, and the IPH message should not show in tab 347 // switcher. 348 clickFirstCardFromTabSwitcher(cta); 349 MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true); 350 enterTabSwitcher(cta); 351 CriteriaHelper.pollUiThread(() -> cta.findViewById(R.id.tab_grid_message_item) == null); 352 353 // Mock that user exits multi-window mode, and the IPH message should show in tab switcher. 354 clickFirstCardFromTabSwitcher(cta); 355 MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(false); 356 enterTabSwitcher(cta); 357 onViewWaiting(allOf(withId(R.id.tab_grid_message_item), isDisplayed())); 358 } 359 verifyIphDialogShowing(ChromeTabbedActivity cta)360 private void verifyIphDialogShowing(ChromeTabbedActivity cta) { 361 // Verify IPH dialog view. 362 onView(withId(R.id.iph_dialog)) 363 .inRoot(withDecorView(not(cta.getWindow().getDecorView()))) 364 .check((v, noMatchException) -> { 365 if (noMatchException != null) throw noMatchException; 366 367 String title = cta.getString(R.string.iph_drag_and_drop_title); 368 assertEquals(title, ((TextView) v.findViewById(R.id.title)).getText()); 369 370 String description = cta.getString(R.string.iph_drag_and_drop_content); 371 assertEquals( 372 description, ((TextView) v.findViewById(R.id.description)).getText()); 373 }); 374 // Verify ModalDialog button. 375 onView(withId(R.id.positive_button)) 376 .inRoot(withDecorView(not(cta.getWindow().getDecorView()))) 377 .check(matches(withText(cta.getString(R.string.ok)))); 378 } 379 verifyIphDialogHiding(ChromeTabbedActivity cta)380 private void verifyIphDialogHiding(ChromeTabbedActivity cta) { 381 boolean isShowing = true; 382 try { 383 onView(withId(R.id.iph_dialog)) 384 .inRoot(withDecorView(not(cta.getWindow().getDecorView()))) 385 .check(matches(isDisplayed())); 386 } catch (NoMatchingRootException e) { 387 isShowing = false; 388 } catch (Exception e) { 389 assert false : "error when inspecting iph dialog."; 390 } 391 assertFalse(isShowing); 392 } 393 exitIphDialogByClickingButton(ChromeTabbedActivity cta)394 private void exitIphDialogByClickingButton(ChromeTabbedActivity cta) { 395 onView(withId(R.id.positive_button)) 396 .inRoot(withDecorView(not(cta.getWindow().getDecorView()))) 397 .perform(click()); 398 } 399 } 400