1 // Copyright 2016 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.webview_ui_test.test;
6 
7 import static androidx.test.espresso.Espresso.onData;
8 import static androidx.test.espresso.Espresso.onView;
9 import static androidx.test.espresso.action.ViewActions.actionWithAssertions;
10 import static androidx.test.espresso.action.ViewActions.click;
11 import static androidx.test.espresso.assertion.ViewAssertions.matches;
12 import static androidx.test.espresso.intent.Intents.assertNoUnverifiedIntents;
13 import static androidx.test.espresso.intent.Intents.intended;
14 import static androidx.test.espresso.intent.Intents.intending;
15 import static androidx.test.espresso.intent.matcher.BundleMatchers.hasEntry;
16 import static androidx.test.espresso.intent.matcher.IntentMatchers.anyIntent;
17 import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
18 import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
19 import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtras;
20 import static androidx.test.espresso.intent.matcher.IntentMatchers.hasType;
21 import static androidx.test.espresso.matcher.RootMatchers.DEFAULT;
22 import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
23 import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
24 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
25 import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
26 import static androidx.test.espresso.matcher.ViewMatchers.withChild;
27 import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
28 import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
29 import static androidx.test.espresso.matcher.ViewMatchers.withId;
30 import static androidx.test.espresso.matcher.ViewMatchers.withText;
31 import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
32 import static androidx.test.espresso.web.sugar.Web.onWebView;
33 import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
34 import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
35 
36 import static org.hamcrest.CoreMatchers.allOf;
37 import static org.hamcrest.CoreMatchers.endsWith;
38 import static org.hamcrest.Matchers.containsString;
39 import static org.hamcrest.Matchers.equalTo;
40 import static org.hamcrest.core.AnyOf.anyOf;
41 import static org.junit.Assert.assertTrue;
42 import static org.junit.Assume.assumeTrue;
43 
44 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
45 
46 import android.app.Activity;
47 import android.app.Instrumentation;
48 import android.content.Intent;
49 import android.os.Build;
50 import android.support.test.InstrumentationRegistry;
51 import android.support.test.uiautomator.UiDevice;
52 import android.support.test.uiautomator.UiObject;
53 import android.support.test.uiautomator.UiSelector;
54 import android.view.MenuItem;
55 
56 import androidx.test.espresso.NoMatchingViewException;
57 import androidx.test.espresso.PerformException;
58 import androidx.test.espresso.Root;
59 import androidx.test.espresso.action.GeneralClickAction;
60 import androidx.test.espresso.action.GeneralLocation;
61 import androidx.test.espresso.action.Press;
62 import androidx.test.espresso.action.Tap;
63 import androidx.test.espresso.intent.Intents;
64 import androidx.test.espresso.web.webdriver.Locator;
65 import androidx.test.filters.SmallTest;
66 
67 import junit.framework.AssertionFailedError;
68 
69 import org.hamcrest.Description;
70 import org.hamcrest.Matcher;
71 import org.hamcrest.TypeSafeMatcher;
72 import org.junit.Before;
73 import org.junit.Rule;
74 import org.junit.Test;
75 import org.junit.runner.RunWith;
76 
77 import org.chromium.base.test.BaseJUnit4ClassRunner;
78 import org.chromium.webview_ui_test.R;
79 import org.chromium.webview_ui_test.WebViewUiTestActivity;
80 import org.chromium.webview_ui_test.test.util.UseLayout;
81 import org.chromium.webview_ui_test.test.util.WebViewUiTestRule;
82 
83 /**
84  * Tests for WebView ActionMode.
85  */
86 @RunWith(BaseJUnit4ClassRunner.class)
87 public class ActionModeTest {
88     private static final String TAG = "ActionModeTest";
89 
90     // Actions available in action mode
91     private static final String ASSIST_ACTION = "Assist";
92     private static final String COPY_ACTION = "Copy";
93     private static final String MORE_OPTIONS_ACTION = "More options";
94     private static final String PASTE_ACTION = "Paste";
95     private static final String SHARE_ACTION = "Share";
96     private static final String SELECT_ALL_ACTION = "Select all";
97     private static final String WEB_SEARCH_ACTION = "Web search";
98 
99     private static final String QUICK_SEARCH_BOX_PKG = "com.google.android.googlequicksearchbox";
100     private static final long ASSIST_TIMEOUT = scaleTimeout(5000);
101 
102     @Rule
103     public WebViewUiTestRule mWebViewActivityRule =
104             new WebViewUiTestRule(WebViewUiTestActivity.class);
105 
106     @Before
setUp()107     public void setUp() {
108         mWebViewActivityRule.launchActivity();
109         onWebView().forceJavascriptEnabled();
110         mWebViewActivityRule.loadDataSync(
111                 "<html><body><p>Hello world</p></body></html>", "text/html", "utf-8", false);
112         onWebView(withId(R.id.webview))
113                 .withElement(findElement(Locator.TAG_NAME, "p"))
114                 .check(webMatches(getText(), containsString("Hello world")));
115     }
116 
117     /**
118      * Test Copy and Paste
119      */
120     @Test
121     @SmallTest
122     @UseLayout("edittext_webview")
testCopyPaste()123     public void testCopyPaste() {
124         longClickOnLastWord(R.id.webview);
125         clickPopupAction(COPY_ACTION);
126         longClickOnLastWord(R.id.edittext);
127         clickPopupAction(PASTE_ACTION);
128         onView(withId(R.id.edittext))
129                 .check(matches(withText("world")));
130     }
131 
132     /**
133      * Test Select All
134      */
135     @Test
136     @SmallTest
137     @UseLayout("edittext_webview")
testSelectAll()138     public void testSelectAll() {
139         longClickOnLastWord(R.id.webview);
140         clickPopupAction(SELECT_ALL_ACTION);
141         clickPopupAction(COPY_ACTION);
142         longClickOnLastWord(R.id.edittext);
143         clickPopupAction(PASTE_ACTION);
144         onView(withId(R.id.edittext))
145                 .check(matches(withText("Hello world")));
146     }
147 
148     /**
149      * Test Share
150      */
151     @Test
152     @SmallTest
153     @UseLayout("edittext_webview")
testShare()154     public void testShare() {
155         Intents.init();
156         intending(anyIntent())
157                 .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, new Intent()));
158 
159         longClickOnLastWord(R.id.webview);
160         clickPopupAction(SHARE_ACTION);
161 
162         intended(allOf(hasAction(Intent.ACTION_CHOOSER),
163                 hasExtras(allOf(hasEntry(Intent.EXTRA_TITLE, SHARE_ACTION),
164                         hasEntry(Intent.EXTRA_INTENT,
165                                 allOf(hasAction(Intent.ACTION_SEND), hasType("text/plain"),
166                                         hasExtra(Intent.EXTRA_TEXT, "world")))))));
167         assertNoUnverifiedIntents();
168     }
169 
170     /**
171      * Test Web Search
172      */
173     @Test
174     @SmallTest
175     @UseLayout("edittext_webview")
testWebSearch()176     public void testWebSearch() {
177         Intents.init();
178         intending(anyIntent())
179                 .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, new Intent()));
180         longClickOnLastWord(R.id.webview);
181         clickPopupAction(WEB_SEARCH_ACTION);
182         intended(allOf(hasAction(Intent.ACTION_WEB_SEARCH),
183                 hasExtras(allOf(hasEntry("com.android.browser.application_id",
184                                          "org.chromium.webview_ui_test"),
185                                 hasEntry("query", "world"),
186                                 hasEntry("new_search", true)))));
187         assertNoUnverifiedIntents();
188     }
189 
190     /**
191      * Test Assist
192      */
193     @Test
194     @SmallTest
195     @UseLayout("edittext_webview")
testAssist()196     public void testAssist() {
197         // The assist option is only available on N
198         assumeTrue(Build.VERSION.SDK_INT == Build.VERSION_CODES.N);
199         longClickOnLastWord(R.id.webview);
200         clickPopupAction(ASSIST_ACTION);
201         UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
202         UiObject assistUi = device.findObject(new UiSelector().packageName(QUICK_SEARCH_BOX_PKG));
203         assertTrue(assistUi.waitForExists(ASSIST_TIMEOUT));
204         device.pressBack();
205     }
206 
207     /**
208      * Click an item on the Action Mode popup
209      */
clickPopupAction(final String name)210     public void clickPopupAction(final String name) {
211         Matcher<Root> rootMatcher;
212 
213         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
214             try {
215                 // On L and lower, use the espresso DEFAULT root matcher if ActionBar is detected
216                 onView(withClassName(endsWith("ActionBarContextView")))
217                         .check(matches(isDisplayed()));
218                 rootMatcher = DEFAULT;
219             } catch (NoMatchingViewException | AssertionFailedError e) {
220                 // Else match in a popup
221                 rootMatcher = withDecorView(withChild(withText(name)));
222             }
223         } else {
224             // On M and above, can use the decoreView matcher
225             rootMatcher = withDecorView(isEnabled());
226         }
227 
228         try {
229             onView(allOf(anyOf(withText(name), withContentDescription(name)), isClickable()))
230                     .inRoot(rootMatcher)
231                     .perform(click());
232         } catch (PerformException | NoMatchingViewException e) {
233             // Take care of case when the item is in the overflow menu
234             onView(allOf(withContentDescription(MORE_OPTIONS_ACTION), isClickable()))
235                     .inRoot(rootMatcher)
236                     .perform(click());
237             onData(new MenuItemMatcher(equalTo(name))).inRoot(rootMatcher).perform(click());
238         }
239 
240         /**
241          * After select all action is clicked, the PopUp Menu may disappear
242          * briefly due to selection change, wait for the menu to reappear
243          */
244         if (name.equals(SELECT_ALL_ACTION)) {
245             assertTrue(mWebViewActivityRule.waitForActionBarPopup());
246         }
247     }
248 
249     /**
250      * Perform a view action that clicks on the last word and start the idling resource
251      * to wait for completion of the popup menu
252      */
longClickOnLastWord(int viewId)253     private final void longClickOnLastWord(int viewId) {
254         // TODO(aluo): This function is not guaranteed to click on element. Change to
255         // implementation that gets bounding box for elements using Javascript.
256         onView(withId(viewId)).perform(actionWithAssertions(
257                 new GeneralClickAction(Tap.LONG, GeneralLocation.CENTER_RIGHT, Press.FINGER)));
258         assertTrue(mWebViewActivityRule.waitForActionBarPopup());
259     }
260 
261     /**
262      * Matches an item on the Action Mode popup by the title
263      */
264     private static class MenuItemMatcher extends TypeSafeMatcher<MenuItem> {
265         private Matcher<String> mTitleMatcher;
266 
MenuItemMatcher(Matcher<String> titleMatcher)267         public MenuItemMatcher(Matcher<String> titleMatcher) {
268             mTitleMatcher = titleMatcher;
269         }
270 
271         @Override
matchesSafely(MenuItem item)272         protected boolean matchesSafely(MenuItem item) {
273             return mTitleMatcher.matches(item.getTitle());
274         }
275 
276         @Override
describeTo(Description description)277         public void describeTo(Description description) {
278             description.appendText("has MenuItem with title: ");
279             description.appendDescriptionOf(mTitleMatcher);
280         }
281     }
282 }
283