1 // Copyright 2013 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.autofill; 6 7 import android.test.suitebuilder.annotation.MediumTest; 8 import android.text.TextUtils; 9 import android.view.View; 10 import android.view.ViewGroup; 11 12 import org.chromium.base.ThreadUtils; 13 import org.chromium.base.test.util.Feature; 14 import org.chromium.base.test.util.UrlUtils; 15 import org.chromium.chrome.R; 16 import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile; 17 import org.chromium.chrome.shell.ChromeShellTestBase; 18 import org.chromium.content.browser.ContentViewCore; 19 import org.chromium.content.browser.test.util.Criteria; 20 import org.chromium.content.browser.test.util.CriteriaHelper; 21 import org.chromium.content.browser.test.util.DOMUtils; 22 import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper; 23 import org.chromium.content.browser.test.util.TouchCommon; 24 import org.chromium.ui.autofill.AutofillPopup; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 import java.util.concurrent.ExecutionException; 29 import java.util.concurrent.TimeoutException; 30 31 /** 32 * Integration tests for the AutofillPopup. 33 */ 34 public class AutofillPopupTest extends ChromeShellTestBase { 35 36 private static final String FIRST_NAME = "John"; 37 private static final String LAST_NAME = "Smith"; 38 private static final String COMPANY_NAME = "Acme Inc."; 39 private static final String ADDRESS_LINE1 = "1 Main"; 40 private static final String ADDRESS_LINE2 = "Apt A"; 41 private static final String STREET_ADDRESS_TEXTAREA = ADDRESS_LINE1 + "\n" + ADDRESS_LINE2; 42 private static final String CITY = "San Francisco"; 43 private static final String DEPENDENT_LOCALITY = ""; 44 private static final String STATE = "CA"; 45 private static final String ZIP_CODE = "94102"; 46 private static final String SORTING_CODE = ""; 47 private static final String COUNTRY = "US"; 48 private static final String PHONE_NUMBER = "4158889999"; 49 private static final String EMAIL = "john (at) acme.inc"; 50 private static final String LANGUAGE_CODE = ""; 51 private static final String ORIGIN = "https://www.example.com"; 52 53 private static final String BASIC_PAGE_DATA = UrlUtils.encodeHtmlDataUri( 54 "<html><head><meta name=\"viewport\"" + 55 "content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" /></head>" + 56 "<body><form method=\"POST\">" + 57 "<input type=\"text\" id=\"fn\" autocomplete=\"given-name\" /><br>" + 58 "<input type=\"text\" id=\"ln\" autocomplete=\"family-name\" /><br>" + 59 "<textarea id=\"sa\" autocomplete=\"street-address\"></textarea><br>" + 60 "<input type=\"text\" id=\"a1\" autocomplete=\"address-line1\" /><br>" + 61 "<input type=\"text\" id=\"a2\" autocomplete=\"address-line2\" /><br>" + 62 "<input type=\"text\" id=\"ct\" autocomplete=\"locality\" /><br>" + 63 "<input type=\"text\" id=\"zc\" autocomplete=\"postal-code\" /><br>" + 64 "<input type=\"text\" id=\"em\" autocomplete=\"email\" /><br>" + 65 "<input type=\"text\" id=\"ph\" autocomplete=\"tel\" /><br>" + 66 "<input type=\"text\" id=\"fx\" autocomplete=\"fax\" /><br>" + 67 "<select id=\"co\" autocomplete=\"country\"><br>" + 68 "<option value=\"BR\">Brazil</option>" + 69 "<option value=\"US\">United States</option>" + 70 "</select>" + 71 "<input type=\"submit\" />" + 72 "</form></body></html>"); 73 74 private static final String INITIATING_ELEMENT_FILLED = UrlUtils.encodeHtmlDataUri( 75 "<html><head><meta name=\"viewport\"" + 76 "content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" /></head>" + 77 "<body><form method=\"POST\">" + 78 "<input type=\"text\" id=\"fn\" autocomplete=\"given-name\" value=\"J\"><br>" + 79 "<input type=\"text\" id=\"ln\" autocomplete=\"family-name\"><br>" + 80 "<input type=\"text\" id=\"em\" autocomplete=\"email\"><br>" + 81 "<select id=\"co\" autocomplete=\"country\"><br>" + 82 "<option value=\"US\">United States</option>" + 83 "<option value=\"BR\">Brazil</option>" + 84 "</select>" + 85 "<input type=\"submit\" />" + 86 "</form></body></html>"); 87 88 private static final String ANOTHER_ELEMENT_FILLED = UrlUtils.encodeHtmlDataUri( 89 "<html><head><meta name=\"viewport\"" + 90 "content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" /></head>" + 91 "<body><form method=\"POST\">" + 92 "<input type=\"text\" id=\"fn\" autocomplete=\"given-name\"><br>" + 93 "<input type=\"text\" id=\"ln\" autocomplete=\"family-name\"><br>" + 94 "<input type=\"text\" id=\"em\" autocomplete=\"email\" value=\"foo (at) example.com\"><br>" + 95 "<select id=\"co\" autocomplete=\"country\"><br>" + 96 "<option></option>" + 97 "<option value=\"BR\">Brazil</option>" + 98 "<option value=\"US\">United States</option>" + 99 "</select>" + 100 "<input type=\"submit\" />" + 101 "</form></body></html>"); 102 103 private static final String INVALID_OPTION = UrlUtils.encodeHtmlDataUri( 104 "<html><head><meta name=\"viewport\"" + 105 "content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" /></head>" + 106 "<body><form method=\"POST\">" + 107 "<input type=\"text\" id=\"fn\" autocomplete=\"given-name\" value=\"J\"><br>" + 108 "<input type=\"text\" id=\"ln\" autocomplete=\"family-name\"><br>" + 109 "<input type=\"text\" id=\"em\" autocomplete=\"email\"><br>" + 110 "<select id=\"co\" autocomplete=\"country\"><br>" + 111 "<option value=\"GB\">Great Britain</option>" + 112 "<option value=\"BR\">Brazil</option>" + 113 "</select>" + 114 "<input type=\"submit\" />" + 115 "</form></body></html>"); 116 117 private AutofillTestHelper mHelper; 118 private List<AutofillLogger.LogEntry> mAutofillLoggedEntries; 119 120 @Override 121 public void setUp() throws Exception { 122 super.setUp(); 123 clearAppData(); 124 mAutofillLoggedEntries = new ArrayList<AutofillLogger.LogEntry>(); 125 AutofillLogger.setLogger( 126 new AutofillLogger.Logger() { 127 @Override 128 public void didFillField(AutofillLogger.LogEntry logEntry) { 129 mAutofillLoggedEntries.add(logEntry); 130 }} 131 ); 132 } 133 134 private void loadAndFillForm( 135 final String formDataUrl, final String inputText) 136 throws InterruptedException, ExecutionException, TimeoutException { 137 launchChromeShellWithUrl(formDataUrl); 138 assertTrue(waitForActiveShellToBeDoneLoading()); 139 mHelper = new AutofillTestHelper(); 140 141 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never 142 // brought up. 143 final ContentViewCore viewCore = getActivity().getActiveContentViewCore(); 144 final ViewGroup view = viewCore.getContainerView(); 145 final TestInputMethodManagerWrapper immw = 146 new TestInputMethodManagerWrapper(viewCore); 147 viewCore.getImeAdapterForTest().setInputMethodManagerWrapper(immw); 148 149 // Add an Autofill profile. 150 AutofillProfile profile = new AutofillProfile( 151 "" /* guid */, ORIGIN, FIRST_NAME + " " + LAST_NAME, COMPANY_NAME, 152 STREET_ADDRESS_TEXTAREA, 153 STATE, CITY, DEPENDENT_LOCALITY, 154 ZIP_CODE, SORTING_CODE, COUNTRY, PHONE_NUMBER, EMAIL, 155 LANGUAGE_CODE); 156 mHelper.setProfile(profile); 157 assertEquals(1, mHelper.getNumberOfProfiles()); 158 159 // Click the input field for the first name. 160 assertTrue(DOMUtils.waitForNonZeroNodeBounds(viewCore, "fn")); 161 DOMUtils.clickNode(this, viewCore, "fn"); 162 163 waitForKeyboardShowRequest(immw, 1); 164 165 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 166 @Override 167 public void run() { 168 viewCore.getInputConnectionForTest().setComposingText( 169 inputText, 1); 170 } 171 }); 172 173 waitForAnchorViewAdd(view); 174 View anchorView = view.findViewById(R.id.dropdown_popup_window); 175 176 assertTrue(anchorView.getTag() instanceof AutofillPopup); 177 final AutofillPopup popup = (AutofillPopup) anchorView.getTag(); 178 179 waitForAutofillPopopShow(popup); 180 181 TouchCommon touchCommon = new TouchCommon(this); 182 touchCommon.singleClickViewRelative(popup.getListView(), 10, 10); 183 184 waitForInputFieldFill(viewCore); 185 } 186 187 /** 188 * Tests that bringing up an Autofill and clicking on the first entry fills out the expected 189 * Autofill information. 190 */ 191 @MediumTest 192 @Feature({"autofill"}) 193 public void testClickAutofillPopupSuggestion() 194 throws InterruptedException, ExecutionException, TimeoutException { 195 loadAndFillForm(BASIC_PAGE_DATA, "J"); 196 final ContentViewCore viewCore = getActivity().getActiveContentViewCore(); 197 198 assertEquals("First name did not match", 199 FIRST_NAME, DOMUtils.getNodeValue(viewCore, "fn")); 200 assertEquals("Last name did not match", 201 LAST_NAME, DOMUtils.getNodeValue(viewCore, "ln")); 202 assertEquals("Street address (textarea) did not match", 203 STREET_ADDRESS_TEXTAREA, DOMUtils.getNodeValue(viewCore, "sa")); 204 assertEquals("Address line 1 did not match", 205 ADDRESS_LINE1, DOMUtils.getNodeValue(viewCore, "a1")); 206 assertEquals("Address line 2 did not match", 207 ADDRESS_LINE2, DOMUtils.getNodeValue(viewCore, "a2")); 208 assertEquals("City did not match", 209 CITY, DOMUtils.getNodeValue(viewCore, "ct")); 210 assertEquals("Zip code did not match", 211 ZIP_CODE, DOMUtils.getNodeValue(viewCore, "zc")); 212 assertEquals("Country did not match", 213 COUNTRY, DOMUtils.getNodeValue(viewCore, "co")); 214 assertEquals("Email did not match", 215 EMAIL, DOMUtils.getNodeValue(viewCore, "em")); 216 assertEquals("Phone number did not match", 217 PHONE_NUMBER, DOMUtils.getNodeValue(viewCore, "ph")); 218 219 final String profileFullName = FIRST_NAME + " " + LAST_NAME; 220 final int loggedEntries = 10; 221 assertEquals("Mismatched number of logged entries", 222 loggedEntries, mAutofillLoggedEntries.size()); 223 assertLogged(FIRST_NAME, profileFullName); 224 assertLogged(LAST_NAME, profileFullName); 225 assertLogged(STREET_ADDRESS_TEXTAREA, profileFullName); 226 assertLogged(ADDRESS_LINE1, profileFullName); 227 assertLogged(ADDRESS_LINE2, profileFullName); 228 assertLogged(CITY, profileFullName); 229 assertLogged(ZIP_CODE, profileFullName); 230 assertLogged(COUNTRY, profileFullName); 231 assertLogged(EMAIL, profileFullName); 232 assertLogged(PHONE_NUMBER, profileFullName); 233 } 234 235 /** 236 * Tests that bringing up an Autofill and clicking on the partially filled first 237 * element will still fill the entire form (including the initiating element itself). 238 */ 239 @MediumTest 240 @Feature({"autofill"}) 241 public void testLoggingInitiatedElementFilled() 242 throws InterruptedException, ExecutionException, TimeoutException { 243 loadAndFillForm(INITIATING_ELEMENT_FILLED, "o"); 244 final String profileFullName = FIRST_NAME + " " + LAST_NAME; 245 final int loggedEntries = 4; 246 assertEquals("Mismatched number of logged entries", 247 loggedEntries, mAutofillLoggedEntries.size()); 248 assertLogged(FIRST_NAME, profileFullName); 249 assertLogged(LAST_NAME, profileFullName); 250 assertLogged(EMAIL, profileFullName); 251 assertLogged(COUNTRY, profileFullName); 252 } 253 254 /** 255 * Tests that bringing up an Autofill and clicking on the empty first element 256 * will fill the all other elements except the previously filled email. 257 */ 258 @MediumTest 259 @Feature({"autofill"}) 260 public void testLoggingAnotherElementFilled() 261 throws InterruptedException, ExecutionException, TimeoutException { 262 loadAndFillForm(ANOTHER_ELEMENT_FILLED, "J"); 263 final String profileFullName = FIRST_NAME + " " + LAST_NAME; 264 final int loggedEntries = 3; 265 assertEquals("Mismatched number of logged entries", 266 loggedEntries, mAutofillLoggedEntries.size()); 267 assertLogged(FIRST_NAME, profileFullName); 268 assertLogged(LAST_NAME, profileFullName); 269 assertLogged(COUNTRY, profileFullName); 270 // Email will not be logged since it already had some data. 271 } 272 273 /** 274 * Tests that selecting a value not present in <option> will not be filled. 275 */ 276 @MediumTest 277 @Feature({"autofill"}) 278 public void testNotLoggingInvalidOption() 279 throws InterruptedException, ExecutionException, TimeoutException { 280 loadAndFillForm(INVALID_OPTION, "o"); 281 final String profileFullName = FIRST_NAME + " " + LAST_NAME; 282 final int loggedEntries = 3; 283 assertEquals("Mismatched number of logged entries", 284 loggedEntries, mAutofillLoggedEntries.size()); 285 assertLogged(FIRST_NAME, profileFullName); 286 assertLogged(LAST_NAME, profileFullName); 287 assertLogged(EMAIL, profileFullName); 288 // Country will not be logged since "US" is not a valid <option>. 289 } 290 291 // Wait and assert helper methods ------------------------------------------------------------- 292 293 private void waitForKeyboardShowRequest(final TestInputMethodManagerWrapper immw, 294 final int count) throws InterruptedException { 295 assertTrue("Keyboard was never requested to be shown.", 296 CriteriaHelper.pollForCriteria(new Criteria() { 297 @Override 298 public boolean isSatisfied() { 299 return immw.getShowSoftInputCounter() == count; 300 } 301 })); 302 } 303 304 private void waitForAnchorViewAdd(final ViewGroup view) throws InterruptedException { 305 assertTrue("Autofill Popup anchor view was never added.", 306 CriteriaHelper.pollForCriteria(new Criteria() { 307 @Override 308 public boolean isSatisfied() { 309 return view.findViewById(R.id.dropdown_popup_window) != null; 310 } 311 })); 312 } 313 314 private void waitForAutofillPopopShow(final AutofillPopup popup) throws InterruptedException { 315 assertTrue("Autofill Popup anchor view was never added.", 316 CriteriaHelper.pollForCriteria(new Criteria() { 317 @Override 318 public boolean isSatisfied() { 319 return popup.isShowing(); 320 } 321 })); 322 } 323 324 private void waitForInputFieldFill(final ContentViewCore viewCore) throws InterruptedException { 325 assertTrue("First name field was never filled.", 326 CriteriaHelper.pollForCriteria(new Criteria() { 327 @Override 328 public boolean isSatisfied() { 329 try { 330 return TextUtils.equals(FIRST_NAME, 331 DOMUtils.getNodeValue(viewCore, "fn")); 332 } catch (InterruptedException e) { 333 return false; 334 } catch (TimeoutException e) { 335 return false; 336 } 337 338 } 339 })); 340 } 341 342 private void assertLogged(String autofilledValue, String profileFullName) { 343 for (AutofillLogger.LogEntry entry : mAutofillLoggedEntries) { 344 if (entry.getAutofilledValue().equals(autofilledValue) && 345 entry.getProfileFullName().equals(profileFullName)) { 346 return; 347 } 348 } 349 fail("Logged entry not found [" + autofilledValue + "," + profileFullName + "]"); 350 } 351 } 352