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