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