[go: nahoru, domu]

blob: aefb1668b322d1357243c121f6659daf7b2812f6 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Pair;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.autofill.AutofillValue;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwContentsClient.AwWebResourceRequest;
import org.chromium.android_webview.test.AwActivityTestRule.TestDependencyFactory;
import org.chromium.autofill.mojom.SubmissionSource;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.components.autofill.AutofillHintsServiceTestHelper;
import org.chromium.components.autofill.AutofillManagerWrapper;
import org.chromium.components.autofill.AutofillPopup;
import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillProviderTestHelper;
import org.chromium.components.autofill.AutofillProviderUMA;
import org.chromium.components.autofill.TestViewStructure;
import org.chromium.components.autofill_public.ViewType;
import org.chromium.components.embedder_support.util.WebResourceResponseInfo;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.util.TestWebServer;
import java.io.ByteArrayInputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeoutException;
/** Tests for WebView Autofill. */
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@Batch(Batch.PER_CLASS)
public class AwAutofillTest extends AwParameterizedTest {
public static final boolean DEBUG = false;
public static final String TAG = "AutofillTest";
public static final String FILE = "/login.html";
public static final String FILE_URL = "file:///android_asset/autofill.html";
public static final int AUTOFILL_VIEW_ENTERED = 0;
public static final int AUTOFILL_VIEW_EXITED = 1;
public static final int AUTOFILL_VALUE_CHANGED = 2;
public static final int AUTOFILL_COMMIT = 3;
public static final int AUTOFILL_CANCEL = 4;
public static final int AUTOFILL_SESSION_STARTED = 5;
public static final int AUTOFILL_PREDICTIONS_AVAILABLE = 6;
public static final int AUTOFILL_EVENT_MAX = 7;
public static final String[] EVENT = {
"VIEW_ENTERED",
"VIEW_EXITED",
"VALUE_CHANGED",
"COMMIT",
"CANCEL",
"SESSION_STARTED",
"QUERY_DONE"
};
// crbug.com/776230: On Android L, declaring variables of unsupported classes causes an error.
// Wrapped them in a class to avoid it.
private static class TestValues {
public TestViewStructure testViewStructure;
public ArrayList<Pair<Integer, AutofillValue>> changedValues;
}
private class TestAutofillManagerWrapper extends AutofillManagerWrapper {
private boolean mDisabled;
private boolean mQuerySucceed;
public TestAutofillManagerWrapper(Context context) {
super(context);
}
public void setDisabled() {
mDisabled = true;
}
@Override
public boolean isDisabled() {
return mDisabled;
}
@Override
public boolean isAwGCurrentAutofillService() {
return sIsAwGCurrentAutofillService;
}
public boolean isQuerySucceed() {
return mQuerySucceed;
}
@Override
public void notifyVirtualViewEntered(View parent, int childId, Rect absBounds) {
if (DEBUG) Log.i(TAG, "notifyVirtualViewEntered");
mEventQueue.add(AUTOFILL_VIEW_ENTERED);
mCallbackHelper.notifyCalled();
}
@Override
public void notifyVirtualViewExited(View parent, int childId) {
if (DEBUG) Log.i(TAG, "notifyVirtualViewExited");
mEventQueue.add(AUTOFILL_VIEW_EXITED);
mCallbackHelper.notifyCalled();
}
@Override
public void notifyVirtualValueChanged(View parent, int childId, AutofillValue value) {
if (DEBUG) Log.i(TAG, "notifyVirtualValueChanged");
if (mTestValues.changedValues == null) {
mTestValues.changedValues = new ArrayList<Pair<Integer, AutofillValue>>();
}
mTestValues.changedValues.add(new Pair<Integer, AutofillValue>(childId, value));
mEventQueue.add(AUTOFILL_VALUE_CHANGED);
mCallbackHelper.notifyCalled();
}
@Override
public void commit(int submissionSource) {
if (DEBUG) Log.i(TAG, "commit");
mEventQueue.add(AUTOFILL_COMMIT);
mSubmissionSource = submissionSource;
mCallbackHelper.notifyCalled();
}
@Override
public void cancel() {
if (DEBUG) Log.i(TAG, "cancel");
mEventQueue.add(AUTOFILL_CANCEL);
mCallbackHelper.notifyCalled();
}
@Override
public void notifyNewSessionStarted(boolean hasServerPrediction) {
if (DEBUG) Log.i(TAG, "notifyNewSessionStarted");
mEventQueue.add(AUTOFILL_SESSION_STARTED);
mCallbackHelper.notifyCalled();
}
@Override
public void onServerPredictionsAvailable() {
mQuerySucceed = true;
if (DEBUG) Log.i(TAG, "onServerPredictionsAvailable");
mEventQueue.add(AUTOFILL_PREDICTIONS_AVAILABLE);
mCallbackHelper.notifyCalled();
}
}
private static class AwAutofillTestClient extends TestAwContentsClient {
public interface ShouldInterceptRequestImpl {
WebResourceResponseInfo shouldInterceptRequest(AwWebResourceRequest request);
}
private ShouldInterceptRequestImpl mShouldInterceptRequestImpl;
public void setShouldInterceptRequestImpl(ShouldInterceptRequestImpl impl) {
mShouldInterceptRequestImpl = impl;
}
@Override
public WebResourceResponseInfo shouldInterceptRequest(AwWebResourceRequest request) {
WebResourceResponseInfo response = null;
if (mShouldInterceptRequestImpl != null) {
response = mShouldInterceptRequestImpl.shouldInterceptRequest(request);
}
if (response != null) return response;
return super.shouldInterceptRequest(request);
}
}
private static class AwAutofillSessionUMATestHelper {
private static final String DATA =
"""
<html>
<head></head>
<body>
<form action="a.html" name="formname" id="formid">
<label>User Name:</label>
<input type="text" id="text1" name="username"
placeholder="placeholder@placeholder.com"
autocomplete="username name" />
<input type="submit" />
</form>
<form><input type="text" id="text2" /></form>
</body>
</html>""";
private static final int TOTAL_CONTROLS = 1; // text1
private int mCnt;
private AwAutofillTest mTest;
private TestWebServer mWebServer;
public AwAutofillSessionUMATestHelper(AwAutofillTest test, TestWebServer webServer) {
mTest = test;
mWebServer = webServer;
}
public void triggerAutofill() throws Throwable {
final String url = mWebServer.setResponse(FILE, DATA, null);
mTest.loadUrlSync(url);
mTest.executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
mTest.dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
}
public void simulateServerPredictionBeforeTriggeringAutofill(int serverType)
throws Throwable {
final String url = mWebServer.setResponse(FILE, DATA, null);
mTest.loadUrlSync(url);
simulateServerPrediction(serverType);
mTest.executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
mTest.dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
}
public void simulateServerPrediction(int serverType) throws Throwable {
TestThreadUtils.runOnUiThreadBlocking(
() ->
AutofillProviderTestHelper
.simulateMainFrameAutofillServerResponseForTesting(
mTest.mAwContents.getWebContents(),
new String[] {"text1"},
new int[] {serverType}));
}
public void simulateUserSelectSuggestion() throws Throwable {
// Simulate user select suggestion
TestViewStructure viewStructure = mTest.mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(TOTAL_CONTROLS, viewStructure.getChildCount());
TestViewStructure child0 = viewStructure.getChild(0);
// Autofill form and verify filled values.
SparseArray<AutofillValue> values = new SparseArray<AutofillValue>();
values.append(child0.getId(), AutofillValue.forText("example@example.com"));
mCnt = mTest.getCallbackCount();
mTest.clearChangedValues();
mTest.invokeAutofill(values);
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
}
public void simulateUserChangeAutofilledField() throws Throwable {
mTest.executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
mTest.dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
}
public void submitForm() throws Throwable {
mTest.executeJavaScriptAndWaitForResult("document.getElementById('formid').submit();");
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt, new Integer[] {AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT});
}
public void reload() throws Throwable {
mTest.executeJavaScriptAndWaitForResult("location.reload();");
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT
});
}
public void startNewSession() throws Throwable {
// Start a new session by moving focus to another form.
mTest.executeJavaScriptAndWaitForResult("document.getElementById('text2').select();");
mTest.dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt,
new Integer[] {
AUTOFILL_VIEW_EXITED,
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
}
public void simulateUserChangeField() throws Throwable {
mTest.executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
mTest.dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
mCnt +=
mTest.waitForCallbackAndVerifyTypes(
mCnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
}
}
private static boolean sIsAwGCurrentAutofillService;
@Rule public AwActivityTestRule mRule;
private TestWebServer mWebServer;
private EmbeddedTestServer mEmbeddedServer;
private AwTestContainerView mTestContainerView;
private AwAutofillTestClient mContentsClient;
private CallbackHelper mCallbackHelper = new CallbackHelper();
private AwContents mAwContents;
private ConcurrentLinkedQueue<Integer> mEventQueue = new ConcurrentLinkedQueue<>();
private TestValues mTestValues = new TestValues();
private int mSubmissionSource;
private TestAutofillManagerWrapper mTestAutofillManagerWrapper;
private AwAutofillSessionUMATestHelper mUMATestHelper;
private AutofillProvider mAutofillProvider;
public AwAutofillTest(AwSettingsMutation param) {
this.mRule = new AwActivityTestRule(param.getMutation());
}
@Before
public void setUp() throws Exception {
mWebServer = TestWebServer.start();
mEmbeddedServer =
EmbeddedTestServer.createAndStartServer(
InstrumentationRegistry.getInstrumentation().getContext());
doSetUp(/* isAwGCurrentAutofillService= */ true);
}
private void doSetUp(boolean isAwGCurrentAutofillService) throws Exception {
sIsAwGCurrentAutofillService = isAwGCurrentAutofillService;
AutofillProvider.setAutofillManagerWrapperFactoryForTesting(
new AutofillProvider.AutofillManagerWrapperFactoryForTesting() {
@Override
public AutofillManagerWrapper create(Context context) {
mTestAutofillManagerWrapper = new TestAutofillManagerWrapper(context);
return mTestAutofillManagerWrapper;
}
});
mUMATestHelper = new AwAutofillSessionUMATestHelper(this, mWebServer);
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newSingleRecordWatcher(
AutofillProviderUMA.UMA_AUTOFILL_CREATED_BY_ACTIVITY_CONTEXT,
true);
});
mContentsClient = new AwAutofillTestClient();
TestThreadUtils.runOnUiThreadBlocking(
() -> AutofillProviderTestHelper.disableCrowdsourcingForTesting());
mTestContainerView =
mRule.createAwTestContainerViewOnMainSync(
mContentsClient, false, new TestDependencyFactory());
mAwContents = mTestContainerView.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
mAutofillProvider = mAwContents.getAutofillProviderForTesting();
TestThreadUtils.runOnUiThreadBlocking(() -> histograms.assertExpected());
}
private void setUpAwGNotCurrent() throws Exception {
doSetUp(/* isAwGCurrentAutofillService= */ false);
}
@After
public void tearDown() {
mWebServer.shutdown();
mAutofillProvider = null;
}
public String getAbsoluteTestPageUrl(String relativePageUrl) {
return mEmbeddedServer.getURL("/android_webview/test/data/autofill/" + relativePageUrl);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testTouchingFormWithAdjustResize() throws Throwable {
PostTask.runOrPostTask(
TaskTraits.UI_DEFAULT,
() -> {
mRule.getActivity()
.getWindow()
.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
});
internalTestTriggerTest();
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testTouchingFormWithAdjustPan() throws Throwable {
PostTask.runOrPostTask(
TaskTraits.UI_DEFAULT,
() -> {
mRule.getActivity()
.getWindow()
.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
});
internalTestTriggerTest();
}
private void internalTestTriggerTest() throws Throwable {
int cnt = 0;
final String url = getAbsoluteTestPageUrl("form_username.html");
loadUrlSync(url);
DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "text1");
// Note that we currently depend on keyboard app's behavior.
// TODO(changwan): mock out IME interaction.
Assert.assertTrue(DOMUtils.clickNode(mTestContainerView.getWebContents(), "text1"));
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL, AUTOFILL_VIEW_ENTERED, AUTOFILL_SESSION_STARTED
});
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
executeJavaScriptAndWaitForResult("document.getElementById('text1').blur();");
waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VIEW_EXITED});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testBasicAutofill() throws Throwable {
final String url =
loadHTML(
"""
<form action='a.html' name='formname'>
<label>User Name:</label>
<input type='text' id='text1' name='name' maxlength='30'
placeholder='placeholder@placeholder.com'
autocomplete='name given-name'>
<input type='checkbox' id='checkbox1' name='showpassword'>
<select id='select1' name='month'>
<option value='1'>Jan</option>
<option value='2'>Feb</option>
</select><textarea id='textarea1'></textarea>
<div contenteditable id='div1'>hello</div>
<input type='submit'>
<input type='reset' id='reset1'>
<input type='color' id='color1'><input type='file' id='file1'>
<input type='image' id='image1'>
</form>""");
final int totalControls = 4; // text1, checkbox1, select1, textarea1
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(totalControls, viewStructure.getChildCount());
// Verify form filled correctly in ViewStructure.
URL pageURL = new URL(url);
String webDomain =
new URL(pageURL.getProtocol(), pageURL.getHost(), pageURL.getPort(), "/")
.toString();
assertEquals(webDomain, viewStructure.getWebDomain());
// WebView shouldn't set class name.
assertNull(viewStructure.getClassName());
Bundle extras = viewStructure.getExtras();
assertEquals("Android WebView", extras.getCharSequence("VIRTUAL_STRUCTURE_PROVIDER_NAME"));
assertTrue(0 < extras.getCharSequence("VIRTUAL_STRUCTURE_PROVIDER_VERSION").length());
TestViewStructure.TestHtmlInfo htmlInfoForm = viewStructure.getHtmlInfo();
assertEquals("form", htmlInfoForm.getTag());
assertEquals("formname", htmlInfoForm.getAttribute("name"));
// Verify input text control filled correctly in ViewStructure.
TestViewStructure child0 = viewStructure.getChild(0);
assertEquals(View.AUTOFILL_TYPE_TEXT, child0.getAutofillType());
assertEquals("placeholder@placeholder.com", child0.getHint());
assertEquals("name", child0.getAutofillHints()[0]);
assertEquals("given-name", child0.getAutofillHints()[1]);
assertFalse(child0.getDimensRect().isEmpty());
// The field has no scroll, should always be zero.
assertEquals(0, child0.getDimensScrollX());
assertEquals(0, child0.getDimensScrollY());
TestViewStructure.TestHtmlInfo htmlInfo0 = child0.getHtmlInfo();
assertEquals("text", htmlInfo0.getAttribute("type"));
assertEquals("text1", htmlInfo0.getAttribute("id"));
assertEquals("name", htmlInfo0.getAttribute("name"));
assertEquals("User Name:", htmlInfo0.getAttribute("label"));
assertEquals("30", htmlInfo0.getAttribute("maxlength"));
assertEquals("NAME_FIRST", htmlInfo0.getAttribute("ua-autofill-hints"));
// Verify checkbox control filled correctly in ViewStructure.
TestViewStructure child1 = viewStructure.getChild(1);
assertEquals(View.AUTOFILL_TYPE_TOGGLE, child1.getAutofillType());
assertEquals("", child1.getHint());
assertNull(child1.getAutofillHints());
assertFalse(child1.getDimensRect().isEmpty());
// The field has no scroll, should always be zero.
assertEquals(0, child1.getDimensScrollX());
assertEquals(0, child1.getDimensScrollY());
TestViewStructure.TestHtmlInfo htmlInfo1 = child1.getHtmlInfo();
assertEquals("checkbox", htmlInfo1.getAttribute("type"));
assertEquals("checkbox1", htmlInfo1.getAttribute("id"));
assertEquals("showpassword", htmlInfo1.getAttribute("name"));
assertEquals("", htmlInfo1.getAttribute("label"));
assertNull(htmlInfo1.getAttribute("maxlength"));
assertNull(htmlInfo1.getAttribute("ua-autofill-hints"));
// Verify select control filled correctly in ViewStructure.
TestViewStructure child2 = viewStructure.getChild(2);
assertEquals(View.AUTOFILL_TYPE_LIST, child2.getAutofillType());
assertEquals("", child2.getHint());
assertNull(child2.getAutofillHints());
assertFalse(child2.getDimensRect().isEmpty());
// The field has no scroll, should always be zero.
assertEquals(0, child2.getDimensScrollX());
assertEquals(0, child2.getDimensScrollY());
TestViewStructure.TestHtmlInfo htmlInfo2 = child2.getHtmlInfo();
assertEquals("month", htmlInfo2.getAttribute("name"));
assertEquals("select1", htmlInfo2.getAttribute("id"));
CharSequence[] options = child2.getAutofillOptions();
assertEquals("Jan", options[0]);
assertEquals("Feb", options[1]);
// Verify textarea control is filled correctly in ViewStructure.
TestViewStructure child3 = viewStructure.getChild(3);
assertEquals(View.AUTOFILL_TYPE_TEXT, child3.getAutofillType());
assertEquals("", child3.getHint());
assertNull(child3.getAutofillHints());
assertFalse(child3.getDimensRect().isEmpty());
// The field has no scroll, should always be zero.
assertEquals(0, child3.getDimensScrollX());
assertEquals(0, child3.getDimensScrollY());
TestViewStructure.TestHtmlInfo htmlInfo3 = child3.getHtmlInfo();
assertEquals("textarea1", htmlInfo3.getAttribute("name"));
// Autofill form and verify filled values.
SparseArray<AutofillValue> values = new SparseArray<AutofillValue>();
values.append(child0.getId(), AutofillValue.forText("example@example.com"));
values.append(child1.getId(), AutofillValue.forToggle(true));
values.append(child2.getId(), AutofillValue.forList(1));
values.append(child3.getId(), AutofillValue.forText("aaa"));
cnt = getCallbackCount();
clearChangedValues();
invokeAutofill(values);
// Autofilling the select control will move the focus on it, and triggers a value change
// callback, so we get additional AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED and
// AUTOFILL_VALUE_CHANGED events at the end.
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VIEW_EXITED,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_VALUE_CHANGED
});
// Verify form filled by Javascript
String value0 =
executeJavaScriptAndWaitForResult("document.getElementById('text1').value;");
assertEquals("\"example@example.com\"", value0);
String value1 =
executeJavaScriptAndWaitForResult("document.getElementById('checkbox1').value;");
assertEquals("\"on\"", value1);
String value2 =
executeJavaScriptAndWaitForResult("document.getElementById('select1').value;");
assertEquals("\"2\"", value2);
String value3 =
executeJavaScriptAndWaitForResult("document.getElementById('textarea1').value;");
assertEquals("\"aaa\"", value3);
ArrayList<Pair<Integer, AutofillValue>> changedValues = getChangedValues();
assertEquals("example@example.com", changedValues.get(0).second.getTextValue());
assertTrue(changedValues.get(1).second.getToggleValue());
assertEquals(1, changedValues.get(2).second.getListValue());
}
/** Tests that a frame-transcending form is filled correctly. */
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({"enable-features=AutofillAcrossIframes"})
@DisabledTest(message = "https://crbug.com/1401726")
public void testCrossFrameAutofill() throws Throwable {
loadHTML(
"""
<form>
<input autocomplete=cc-name>
<iframe srcdoc='<input autocomplete=cc-number>'></iframe>
<iframe srcdoc='<input autocomplete=cc-exp>'></iframe>
<iframe srcdoc='<input autocomplete=cc-csc>'></iframe>
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult(
"window.frames[0].document.body.firstElementChild.select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VIEW_EXITED,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
// Autofill form and verify filled values.
SparseArray<AutofillValue> values = new SparseArray<AutofillValue>();
values.append(viewStructure.getChild(0).getId(), AutofillValue.forText("Barack Obama"));
values.append(viewStructure.getChild(1).getId(), AutofillValue.forText("4444333322221111"));
values.append(viewStructure.getChild(2).getId(), AutofillValue.forText("12 / 2035"));
values.append(viewStructure.getChild(3).getId(), AutofillValue.forText("123"));
invokeAutofill(values);
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED
});
assertEquals(
"\"Barack Obama\"",
executeJavaScriptAndWaitForResult("document.forms[0].elements[0].value;"));
assertEquals(
"\"4444333322221111\"",
executeJavaScriptAndWaitForResult(
"window.frames[0].document.body.firstElementChild.value;"));
assertEquals(
"\"12 / 2035\"",
executeJavaScriptAndWaitForResult(
"window.frames[1].document.body.firstElementChild.value;"));
assertEquals(
"\"123\"",
executeJavaScriptAndWaitForResult(
"window.frames[2].document.body.firstElementChild.value;"));
}
/**
* This test is verifying that a user interacting with a form after reloading a webpage
* triggers a new autofill session rather than continuing a session that was started before the
* reload. This is necessary to ensure that autofill is properly triggered in this case (see
* crbug.com/1117563 for details).
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testAutofillTriggersAfterReload() throws Throwable {
int cnt = 0;
final String url = getAbsoluteTestPageUrl("form_username.html");
loadUrlSync(url);
DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "text1");
// TODO(changwan): mock out IME interaction.
Assert.assertTrue(DOMUtils.clickNode(mTestContainerView.getWebContents(), "text1"));
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL, AUTOFILL_VIEW_ENTERED, AUTOFILL_SESSION_STARTED
});
// Reload the page and check that the user clicking on the same form field ends the current
// autofill session and starts a new session.
reloadSync();
DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "text1");
// TODO(changwan): mock out IME interaction.
Assert.assertTrue(DOMUtils.clickNode(mTestContainerView.getWebContents(), "text1"));
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED,
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testNotifyVirtualValueChanged() throws Throwable {
final String url = getAbsoluteTestPageUrl("form_username.html");
loadUrlSync(url);
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
// Check if NotifyVirtualValueChanged() called and value is 'a'.
assertEquals(1, values.size());
assertEquals("a", values.get(0).second.getTextValue());
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
// Check if NotifyVirtualValueChanged() called again, first value is 'a',
// second value is 'ab', and both time has the same id.
waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
values = getChangedValues();
assertEquals(2, values.size());
assertEquals("a", values.get(0).second.getTextValue());
assertEquals("ab", values.get(1).second.getTextValue());
assertEquals(values.get(0).first, values.get(1).first);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testJavascriptNotTriggerNotifyVirtualValueChanged() throws Throwable {
final String url = getAbsoluteTestPageUrl("form_username.html");
loadUrlSync(url);
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
// Check if NotifyVirtualValueChanged() called and value is 'a'.
assertEquals(1, values.size());
assertEquals("a", values.get(0).second.getTextValue());
executeJavaScriptAndWaitForResult("document.getElementById('text1').value='c';");
// Check no new event occurs, this is best effort checking, the event here could be leaked
// from previous dispatchDownAndUpKeyEvents().
assertEquals(
"Events in the queue "
+ buildEventList(mEventQueue.toArray(new Integer[mEventQueue.size()])),
cnt,
getCallbackCount());
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
// Check if NotifyVirtualValueChanged() called one more time and value is 'cb', this
// means javascript change didn't trigger the NotifyVirtualValueChanged().
waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
values = getChangedValues();
assertEquals(2, values.size());
assertEquals("a", values.get(0).second.getTextValue());
assertEquals("cb", values.get(1).second.getTextValue());
assertEquals(values.get(0).first, values.get(1).first);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testCommit() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
<input type='password' id='passwordid' name='passwordname'
<input type='submit'>
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
// Fill the password.
executeJavaScriptAndWaitForResult("document.getElementById('passwordid').select();");
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED
});
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
clearChangedValues();
// Submit form.
executeJavaScriptAndWaitForResult("document.getElementById('formid').submit();");
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {AUTOFILL_VALUE_CHANGED, AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(2, values.size());
assertEquals("a", values.get(0).second.getTextValue());
assertEquals("b", values.get(1).second.getTextValue());
assertEquals(SubmissionSource.FORM_SUBMISSION, mSubmissionSource);
}
// Test that `AutofillManager.commit()` is called after form submission even
// if the web page dynamically modified the form after the last user interaction.
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({"enable-features=AndroidAutofillFormSubmissionCheckById"})
public void testCommitWithChangedFormProperties() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
<input type='password' id='passwordid' name='passwordname'
<input type='submit'>
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
// Fill the password.
executeJavaScriptAndWaitForResult("document.getElementById('passwordid').select();");
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED
});
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
clearChangedValues();
// Change the form name.
executeJavaScriptAndWaitForResult("document.getElementById('formid').name = 'othername';");
// The form submission is detected despite the change in form properties.
executeJavaScriptAndWaitForResult("document.getElementById('formid').submit();");
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {AUTOFILL_VALUE_CHANGED, AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(2, values.size());
assertEquals("a", values.get(0).second.getTextValue());
assertEquals("b", values.get(1).second.getTextValue());
assertEquals(SubmissionSource.FORM_SUBMISSION, mSubmissionSource);
}
/**
* Tests that when a multi-frame form is submitted in a subframe, we register the submission of
* the overall form.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({"enable-features=AutofillAcrossIframes"})
public void testCrossFrameCommit() throws Throwable {
// The only reason we use a <form> inside the iframe is that this makes it easiest to
// trigger a form submission in that frame.
// TODO(crbug.com/1385768): Need to set the "id" so GetSimilarFieldIndex() doesn't confuse
// the fields.
loadHTML(
"""
<form>
<input id=name>
<iframe srcdoc='<form action=arbitrary.html method=GET>
<input id=num></form>'></iframe>
<iframe srcdoc='<input id=exp>'></iframe>
<iframe srcdoc='<input id=csc>'></iframe>
</form>""");
int cnt = 0;
// Fill name field.
executeJavaScriptAndWaitForResult("document.forms[0].elements[0].select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
// Fill number field.
executeJavaScriptAndWaitForResult(
"window.frames[0].document.forms[0].elements[0].select();");
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED
});
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
clearChangedValues();
// Fill expiration date field.
executeJavaScriptAndWaitForResult(
"window.frames[1].document.body.firstElementChild.select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_C);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED
});
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
clearChangedValues();
// Fill CVC field.
executeJavaScriptAndWaitForResult(
"window.frames[2].document.body.firstElementChild.select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_D);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED
});
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
clearChangedValues();
// Submit a form in the subframe.
executeJavaScriptAndWaitForResult("window.frames[0].document.forms[0].submit();");
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_COMMIT
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(4, values.size());
assertEquals("a", values.get(0).second.getTextValue());
assertEquals("b", values.get(1).second.getTextValue());
assertEquals("c", values.get(2).second.getTextValue());
assertEquals("d", values.get(3).second.getTextValue());
assertEquals(SubmissionSource.FORM_SUBMISSION, mSubmissionSource);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testLoadFileURL() throws Throwable {
int cnt = 0;
loadUrlSync(FILE_URL);
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
// Cancel called for the first query.
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testMovingToOtherForm() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
<input type='submit'>
</form>
<form action='a.html' name='formname' id='formid2'>
<input type='text' id='text2' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
<input type='submit'>
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
// Move to form2, cancel() should be called again.
executeJavaScriptAndWaitForResult("document.getElementById('text2').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED,
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
}
/** This test is verifying new session starts if frame change. */
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testSwitchFromIFrame() throws Throwable {
// we intentionally load main frame and iframe from the same URL and make both have the
// similar form, so the new session is triggered by frame change
final String data =
"<html><head></head><body><form name='formname' id='formid'>"
+ "<input type='text' id='text1' name='username'"
+ " placeholder='placeholder@placeholder.com' autocomplete='username name'>"
+ "<input type='submit'></form>"
+ "<iframe id='myframe' src='"
+ FILE
+ "'></iframe>"
+ "</body></html>";
final String iframeData =
"""
<html>
<head></head>
<body>
<form name='formname' id='formid'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name' autofocus>
<input type='submit'>
</form>
</body>
</html>""";
final String url = mWebServer.setResponse(FILE, data, null);
mContentsClient.setShouldInterceptRequestImpl(
new AwAutofillTestClient.ShouldInterceptRequestImpl() {
private int mCallCount;
@Override
public WebResourceResponseInfo shouldInterceptRequest(
AwWebResourceRequest request) {
try {
if (url.equals(request.url)) {
// Only intercept the iframe's request.
if (mCallCount == 1) {
final String encoding = "UTF-8";
return new WebResourceResponseInfo(
"text/html",
encoding,
new ByteArrayInputStream(
iframeData.getBytes(encoding)));
}
mCallCount++;
}
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
loadUrlSync(url);
// Trigger the autofill in iframe.
int count = clearEventQueueAndGetCallCount();
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
// Verify autofill session triggered.
count +=
waitForCallbackAndVerifyTypes(
count,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
// Verify focus is in iframe.
assertEquals(
"true",
executeJavaScriptAndWaitForResult(
"document.getElementById('myframe').contentDocument.hasFocus()"));
// Move focus to the main frame form.
clearChangedValues();
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
// The new session starts because cancel() has been called.
waitForCallbackAndVerifyTypes(
count,
new Integer[] {
AUTOFILL_VIEW_EXITED,
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(1, values.size());
assertEquals("a", values.get(0).second.getTextValue());
// Verify focus isn't in iframe now.
assertEquals(
"false",
executeJavaScriptAndWaitForResult(
"document.getElementById('myframe').contentDocument.hasFocus()"));
}
/** This test is verifying new session starts if frame change. */
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testTouchingPasswordFieldTriggerQuery() throws Throwable {
int cnt = 0;
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='password' id='passwordid'
name='passwordname' <input type='submit'>
</form>""");
DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "passwordid");
// Note that we currently depend on keyboard app's behavior.
// TODO(changwan): mock out IME interaction.
Assert.assertTrue(DOMUtils.clickNode(mTestContainerView.getWebContents(), "passwordid"));
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL, AUTOFILL_VIEW_ENTERED, AUTOFILL_SESSION_STARTED
});
}
/**
* This test is verifying that AutofillProvider correctly processes the removal and restoring
* of focus on a form element.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testFocusRemovedAndRestored() throws Throwable {
int cnt = 0;
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
<input type='password' id='passwordid' name='passwordname'>
</form>""");
// Start the session by clicking on the username element.
DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "text1");
// TODO(changwan): mock out IME interaction.
Assert.assertTrue(DOMUtils.clickNode(mTestContainerView.getWebContents(), "text1"));
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL, AUTOFILL_VIEW_ENTERED, AUTOFILL_SESSION_STARTED
});
// Removing focus from this element should cause a notification that the autofill view was
// exited.
executeJavaScriptAndWaitForResult("document.getElementById('text1').blur();");
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VIEW_EXITED});
// Restoring focus on the form element should cause notifications to the autofill framework
// that the autofill view was entered and value changed (AutofillProvider sends the latter
// as a safeguard whenever focus changes to a new form element in the current session; it
// was not sent as part of the first click above because at that point focus didn't change
// to a *new* form element but was still on the element whose focusing had caused the
// autofill session to start).
Assert.assertTrue(DOMUtils.clickNode(mTestContainerView.getWebContents(), "text1"));
waitForCallbackAndVerifyTypes(
cnt, new Integer[] {AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED});
}
/**
* This test is verifying that a navigation occurring while there is a probably-submitted
* form will trigger commit of the current autofill session.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testNavigationAfterProbableSubmitResultsInSessionCommit() throws Throwable {
int cnt = 0;
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
<input type='password' id='passwordid' name='passwordname'>
</form>
>""");
final String success = "<!DOCTYPE html>" + "<html>" + "<body>" + "</body>" + "</html>";
mWebServer.setResponse("/success.html", success, null);
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
executeJavaScriptAndWaitForResult("window.location.href = 'success.html'; ");
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_VALUE_CHANGED,
AUTOFILL_COMMIT
});
assertEquals(SubmissionSource.PROBABLY_FORM_SUBMITTED, mSubmissionSource);
}
/**
* This test is verifying there is no callback if there is no form change between two
* navigations.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testNoSubmissionWithoutFillingForm() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
<input type='password' id='passwordid' name='passwordname'>
</form>""");
final String success = "<!DOCTYPE html>" + "<html>" + "<body>" + "</body>" + "</html>";
mWebServer.setResponse("/success.html", success, null);
executeJavaScriptAndWaitForResult("window.location.href = 'success.html'; ");
// There is no callback. AUTOFILL_CANCEL shouldn't be invoked.
assertEquals(0, getCallbackCount());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@DisableIf.Build(
sdk_is_less_than = Build.VERSION_CODES.P,
message = "This test is disabled on Android O because of https://crbug.com/997362")
public void testSelectControlChangeNotification() throws Throwable {
int cnt = 0;
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'>
<select id='color' autofocus>
<option value='red'>red</option>
<option value='blue' id='blue'>blue</option>
</select>
</form>""");
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
clearChangedValues();
executeJavaScriptAndWaitForResult("document.getElementById('color').focus();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_SPACE);
// Use key B to select 'blue'.
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(1, values.size());
assertTrue(values.get(0).second.isList());
assertEquals(1, values.get(0).second.getListValue());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
@DisableIf.Build(
sdk_is_less_than = Build.VERSION_CODES.P,
message = "This test is disabled on Android O because of https://crbug.com/997362")
public void testSelectControlChangeStartAutofillSession() throws Throwable {
int cnt = 0;
loadHTML(
"""
<form action='a.html' name='formname' id='formid'>
<input type='text' id='text1' name='username'>
<select id='color' autofocus>
<option value='red'>red</option>
<option value='blue' id='blue'>blue</option>
</select>
</form>""");
// Change select control first shall start autofill session.
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_SPACE);
// Use key B to select 'blue'.
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(1, values.size());
assertTrue(values.get(0).second.isList());
assertEquals(1, values.get(0).second.getListValue());
// Verify the autofill session started by select control has dimens filled.
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
assertFalse(viewStructure.getChild(0).getDimensRect().isEmpty());
// The field has no scroll, should always be zero.
assertEquals(0, viewStructure.getChild(0).getDimensScrollX());
assertEquals(0, viewStructure.getChild(0).getDimensScrollY());
assertFalse(viewStructure.getChild(1).getDimensRect().isEmpty());
// The field has no scroll, should always be zero.
assertEquals(0, viewStructure.getChild(1).getDimensScrollX());
assertEquals(0, viewStructure.getChild(1).getDimensScrollY());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUserInitiatedJavascriptSelectControlChangeNotification() throws Throwable {
int cnt = 0;
loadHTML(
"""
<script>
function myFunction() {
document.getElementById('color').value = 'blue';
}
</script>
<form action='a.html' name='formname' id='formid'>
<button onclick='myFunction();' autofocus>button </button>
<select id='color'>
<option value='red'>red</option>
<option value='blue' id='blue'>blue</option>
</select>
</form>""");
// Change select control first shall start autofill session.
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_SPACE);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(1, values.size());
assertTrue(values.get(0).second.isList());
assertEquals(1, values.get(0).second.getListValue());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testJavascriptNotTriggerSelectControlChangeNotification() throws Throwable {
int cnt = 0;
loadHTML(
"""
<script>
function myFunction() {
document.getElementById('color').value = 'blue';
}
</script>
<script defer>
myFunction();
</script>
<form action='a.html' name='formname' id='formid'>
<button onclick='myFunction();' autofocus>button </button>
<select id='color'>
<option value='red'>red</option>
<option value='blue' id='blue'>blue</option>
</select>
</form>""");
// There is no good way to verify no callback occurred, we just simulate user trigger
// the autofill and verify autofill is only triggered once, then this proves javascript
// didn't trigger the autofill, since
// testUserInitiatedJavascriptSelectControlChangeNotification verified user's triggering
// work.
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_SPACE);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
ArrayList<Pair<Integer, AutofillValue>> values = getChangedValues();
assertEquals(1, values.size());
assertTrue(values.get(0).second.isList());
assertEquals(1, values.get(0).second.getListValue());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUaAutofillHints() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<label for=\"frmAddressB\">Address</label>
<input name=\"bill-address\" id=\"frmAddressB\">
<label for=\"frmCityB\">City</label>
<input name=\"bill-city\" id=\"frmCityB\">
<label for=\"frmStateB\">State</label>
<input name=\"bill-state\" id=\"frmStateB\">
<label for=\"frmZipB\">Zip</label>
<input name=\"bill-zip\" id=\"frmZipB\">
<input type='checkbox' id='checkbox1' name='showpassword'>
<label for=\"frmCountryB\">Country</label>
<input name=\"bill-country\" id=\"frmCountryB\">
<input type='submit'>
</form>""");
final int totalControls = 6;
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('frmAddressB').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(totalControls, viewStructure.getChildCount());
TestViewStructure child0 = viewStructure.getChild(0);
TestViewStructure.TestHtmlInfo htmlInfo0 = child0.getHtmlInfo();
assertEquals("ADDRESS_HOME_LINE1", htmlInfo0.getAttribute("ua-autofill-hints"));
TestViewStructure child1 = viewStructure.getChild(1);
TestViewStructure.TestHtmlInfo htmlInfo1 = child1.getHtmlInfo();
assertEquals("ADDRESS_HOME_CITY", htmlInfo1.getAttribute("ua-autofill-hints"));
TestViewStructure child2 = viewStructure.getChild(2);
TestViewStructure.TestHtmlInfo htmlInfo2 = child2.getHtmlInfo();
assertEquals("ADDRESS_HOME_STATE", htmlInfo2.getAttribute("ua-autofill-hints"));
TestViewStructure child3 = viewStructure.getChild(3);
TestViewStructure.TestHtmlInfo htmlInfo3 = child3.getHtmlInfo();
assertEquals("ADDRESS_HOME_ZIP", htmlInfo3.getAttribute("ua-autofill-hints"));
TestViewStructure child4 = viewStructure.getChild(4);
TestViewStructure.TestHtmlInfo htmlInfo4 = child4.getHtmlInfo();
assertNull(htmlInfo4.getAttribute("ua-autofill-hints"));
TestViewStructure child5 = viewStructure.getChild(5);
TestViewStructure.TestHtmlInfo htmlInfo5 = child5.getHtmlInfo();
assertEquals("ADDRESS_HOME_COUNTRY", htmlInfo5.getAttribute("ua-autofill-hints"));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserSelectSuggestionUserChangeFormFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED)
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.FORM_SUBMISSION)
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY,
AutofillProviderUMA.AWG_HAS_SUGGESTION_AUTOFILLED)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.simulateUserSelectSuggestion();
mUMATestHelper.simulateUserChangeField();
mUMATestHelper.submitForm();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserSelectSuggestionUserChangeFormNoFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectNoRecords(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.simulateUserSelectSuggestion();
mUMATestHelper.simulateUserChangeField();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
/** Tests that the metrics of the ongoing session are recorded on AwContents destruction. */
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMASessionMetricsRecordedOnAwContentsDestruction() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectNoRecords(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.simulateUserSelectSuggestion();
mUMATestHelper.simulateUserChangeField();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
mAwContents.destroy();
});
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserSelectNotSuggestionUserChangeFormNoFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectAnyRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUGGESTION_TIME)
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectNoRecords(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.simulateUserChangeField();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserNotSelectSuggestionUserChangeFormFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED)
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.FORM_SUBMISSION)
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY,
AutofillProviderUMA.AWG_HAS_SUGGESTION_NO_AUTOFILL)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.simulateUserChangeField();
mUMATestHelper.submitForm();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMANoSuggestionUserChangeFormNoFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.NO_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectNoRecords(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
mUMATestHelper.simulateUserChangeField();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMANoSuggestionUserChangeFormFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.NO_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED)
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.FORM_SUBMISSION)
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY,
AutofillProviderUMA.AWG_NO_SUGGESTION)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
mUMATestHelper.simulateUserChangeField();
mUMATestHelper.submitForm();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserSelectSuggestionUserNotChangeFormFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED)
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.FORM_SUBMISSION)
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY,
AutofillProviderUMA.AWG_HAS_SUGGESTION_AUTOFILLED)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.simulateUserSelectSuggestion();
mUMATestHelper.submitForm();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserSelectSuggestionUserNotChangeFormNoFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectNoRecords(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.simulateUserSelectSuggestion();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserNotSelectSuggestionUserNotChangeFormNoFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectNoRecords(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAUserNotSelectSuggestionUserNotChangeFormFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED)
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.FORM_SUBMISSION)
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY,
AutofillProviderUMA.AWG_HAS_SUGGESTION_NO_AUTOFILL)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
invokeOnInputUIShown();
mUMATestHelper.submitForm();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMANoSuggestionUserNotChangeFormNoFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.NO_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectNoRecords(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMANoSuggestionUserNotChangeFormFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA
.NO_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED)
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.FORM_SUBMISSION)
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_AWG_SUGGESTION_AVAILABILITY,
AutofillProviderUMA.AWG_NO_SUGGESTION)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
mUMATestHelper.submitForm();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
/**
* Tests that the proper histograms are reocrded when no virtual structure is provided before
* session start.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMANoCallbackFromFramework() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_AUTOFILL_SESSION,
AutofillProviderUMA.NO_STRUCTURE_PROVIDED)
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.build();
});
mUMATestHelper.triggerAutofill();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMANoServerPrediction() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newSingleRecordWatcher(
AutofillProviderUMA.UMA_AUTOFILL_SERVER_PREDICTION_AVAILABILITY,
AutofillProviderUMA.SERVER_PREDICTION_NOT_AVAILABLE);
});
mUMATestHelper.triggerAutofill();
mUMATestHelper.startNewSession();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAServerPredictionArriveBeforeSessionStart() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_SERVER_PREDICTION_AVAILABILITY,
AutofillProviderUMA
.SERVER_PREDICTION_AVAILABLE_ON_SESSION_STARTS)
.expectBooleanRecord(
AutofillProviderUMA
.UMA_AUTOFILL_VALID_SERVER_PREDICTION,
true)
.build();
});
mUMATestHelper.simulateServerPredictionBeforeTriggeringAutofill(/*USERNAME*/ 86);
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAServerPredictionArriveAfterSessionStart() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA
.UMA_AUTOFILL_SERVER_PREDICTION_AVAILABILITY,
AutofillProviderUMA
.SERVER_PREDICTION_AVAILABLE_AFTER_SESSION_STARTS)
.expectBooleanRecord(
AutofillProviderUMA
.UMA_AUTOFILL_VALID_SERVER_PREDICTION,
false)
.build();
});
mUMATestHelper.triggerAutofill();
mUMATestHelper.simulateServerPrediction(/*NO_SERVER_DATA*/ 0);
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAAutofillDisabled() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectBooleanRecord(
AutofillProviderUMA.UMA_AUTOFILL_ENABLED, false)
.build();
});
mTestAutofillManagerWrapper.setDisabled();
mUMATestHelper.triggerAutofill();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAAutofillEnabled() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectNoRecords(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE)
.expectBooleanRecord(
AutofillProviderUMA.UMA_AUTOFILL_ENABLED, true)
.build();
});
mUMATestHelper.triggerAutofill();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
/**
* Tests recording of the `PROBABLY_FORM_SUBMITTED` bucket for the
* "Autofill.WebView.SubmissionSource" histogram. This event is fired on a navigation not
* resulting from a link click (in this case the test uses a reload).
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAFormSubmissionProbablyFormSubmitted() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.PROBABLY_FORM_SUBMITTED)
.build();
});
mUMATestHelper.triggerAutofill();
invokeOnProvideAutoFillVirtualStructure();
mUMATestHelper.reload();
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
/**
* Tests recording of the `FRAME_DETACHED` bucket for the "Autofill.WebView.SubmissionSource"
* histogram. This event is fired when a non-main frame is detached.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAFormSubmissionFrameDetached() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.FRAME_DETACHED)
.build();
});
loadHTML(
"""
<div id='parent'>
<iframe id='frame' srcdoc='<input id="username">'></iframe>
</div>""");
int cnt = 0;
executeJavaScriptAndWaitForResult(
"""
var iframe = document.getElementById('frame');
var frame_doc = iframe.contentDocument;
frame_doc.getElementById('username').select();""");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
executeJavaScriptAndWaitForResult(
"document.getElementById('parent').removeChild(document.getElementById('frame'));");
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT
});
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
/**
* Tests recording of the `SAME_DOCUMENT_NAVIGATION` bucket for the
* "Autofill.WebView.SubmissionSource" histogram. This event is fired when clicking a link that
* jumps through the same document and the tracked element disappears.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAFormSubmissionSameDocumentNavigation() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.SAME_DOCUMENT_NAVIGATION)
.build();
});
loadHTML(
"""
<input id='username'>
<a id='link' href='#destination'></a>
<div id='destination'></div>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('username').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
executeJavaScriptAndWaitForResult(
"""
document.getElementById('link').click();
document.getElementById('username').remove();""");
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT
});
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
/**
* Tests recording of the `XHR_SUCCEEDED` bucket for the "Autofill.WebView.SubmissionSource"
* histogram. This event is fired when a successful XHR request occurs and the tracked element
* disappears.
*/
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testUMAFormSubmissionXHRSucceeded() throws Throwable {
var histograms =
TestThreadUtils.runOnUiThreadBlocking(
() -> {
return HistogramWatcher.newBuilder()
.expectIntRecord(
AutofillProviderUMA.UMA_AUTOFILL_SUBMISSION_SOURCE,
AutofillProviderUMA.XHR_SUCCEEDED)
.build();
});
loadHTML("<input id='username'>");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('username').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
final String xhrUrl = mWebServer.setEmptyResponse(FILE);
executeJavaScriptAndWaitForResult(
String.format(
"""
document.getElementById('username').remove();
const xhr = new XMLHttpRequest();
xhr.open('GET', '%s', true);
xhr.send(null);""",
xhrUrl));
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_VIEW_EXITED, AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT
});
TestThreadUtils.runOnUiThreadBlocking(
() -> {
histograms.assertExpected();
});
}
@Test
@SmallTest
@RequiresRestart("https://crbug.com/1422936")
public void testUmaFunnelMetrics() throws Throwable {
HistogramWatcher.Builder histogramWatcherBuilder = HistogramWatcher.newBuilder();
histogramWatcherBuilder
.expectBooleanRecord("Autofill.WebView.Funnel.ParsedAsType.Address", true)
// Ignore histogram for pages without any forms.
.allowExtraRecords("Autofill.WebView.Funnel.ParsedAsType.Address")
.expectBooleanRecord(
"Autofill.WebView.Funnel.InteractionAfterParsedAsType.Address", true)
.expectBooleanRecord("Autofill.WebView.Funnel.FillAfterInteraction.Address", true)
.expectBooleanRecord("Autofill.WebView.Funnel.SubmissionAfterFill.Address", true)
.expectBooleanRecord("Autofill.WebView.KeyMetrics.FillingCorrectness.Address", true)
.expectBooleanRecord("Autofill.WebView.KeyMetrics.FillingAssistance.Address", true)
.expectBooleanRecord(
"Autofill.WebView.KeyMetrics.FormSubmission.Autofilled.Address", true);
histogramWatcherBuilder
.expectBooleanRecord("Autofill.WebView.Funnel.ParsedAsType.CreditCard", true)
// Ignore histogram for pages without any forms.
.allowExtraRecords("Autofill.WebView.Funnel.ParsedAsType.CreditCard")
.expectBooleanRecord(
"Autofill.WebView.Funnel.InteractionAfterParsedAsType.CreditCard", false);
histogramWatcherBuilder.expectNoRecords(
"Autofill.WebView.Funnel.SubmissionAfterFill.CreditCard");
HistogramWatcher histogramWatcher = histogramWatcherBuilder.build();
final String url = getAbsoluteTestPageUrl("page_address_credit_card_forms.html");
loadUrlSync(url);
executeJavaScriptAndWaitForResult("document.getElementById('address1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
waitForEvents(new Integer[] {AUTOFILL_SESSION_STARTED, AUTOFILL_VALUE_CHANGED});
invokeOnProvideAutoFillVirtualStructure();
int address1Id = mTestValues.testViewStructure.getChild(0).getId();
SparseArray<AutofillValue> autofillValues = new SparseArray<AutofillValue>();
autofillValues.append(address1Id, AutofillValue.forText("Jane Doe"));
invokeAutofill(autofillValues);
executeJavaScriptAndWaitForResult("document.getElementById('addressFormId').submit();");
// All of the metrics are recorded at the same time. Wait for one of the metrics to be
// recorded.
CriteriaHelper.pollUiThread(
() -> {
int numSamples =
RecordHistogram.getHistogramValueCountForTesting(
"Autofill.WebView.Funnel.ParsedAsType.Address",
/* sample= */ 1);
return numSamples > 0;
});
histogramWatcher.assertExpected();
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testPageScrollTriggerViewExitAndEnter() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<input type='text' id='text1' name='username'
placeholder='placeholder@placeholder.com'
autocomplete='username name'>
</form>
<p style='height: 100vh'>Hello</p>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
// Moved view, the position change trigger additional AUTOFILL_VIEW_EXITED and
// AUTOFILL_VIEW_ENTERED.
scrollToBottom();
pollJavascriptResultNotEqualTo("document.body.scrollTop;", "0");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
List<Integer> expectedValues = new ArrayList<>();
// On Android version below P scroll triggers additional
// AUTOFILL_VIEW_ENTERED (@see AutofillProvider#onTextFieldDidScroll).
if (VERSION.SDK_INT < Build.VERSION_CODES.P) {
expectedValues.add(AUTOFILL_VIEW_ENTERED);
}
// Check if NotifyVirtualValueChanged() called again and with extra AUTOFILL_VIEW_EXITED
// and AUTOFILL_VIEW_ENTERED
expectedValues.addAll(
Arrays.asList(AUTOFILL_VIEW_EXITED, AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED));
waitForCallbackAndVerifyTypes(cnt, expectedValues.toArray(new Integer[0]));
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testMismatchedAutofillValueWontCauseCrash() throws Throwable {
final String url = getAbsoluteTestPageUrl("form_username.html");
loadUrlSync(url);
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(1, viewStructure.getChildCount());
TestViewStructure child0 = viewStructure.getChild(0);
// Autofill form and verify filled values.
SparseArray<AutofillValue> values = new SparseArray<AutofillValue>();
// Append wrong autofill value.
values.append(child0.getId(), AutofillValue.forToggle(false));
// If the test fail, the exception shall be thrown in below.
invokeAutofill(values);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testDatalistSentToAutofillService() throws Throwable {
final String url = getAbsoluteTestPageUrl("form_with_datalist.html");
loadUrlSync(url);
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
// Verified the datalist has correctly been sent to AutofillService
TestViewStructure child1 = viewStructure.getChild(1);
assertEquals(2, child1.getAutofillOptions().length);
assertEquals("A1", child1.getAutofillOptions()[0]);
assertEquals("A2", child1.getAutofillOptions()[1]);
// Simulate autofilling the datalist by the AutofillService.
SparseArray<AutofillValue> values = new SparseArray<AutofillValue>();
values.append(child1.getId(), AutofillValue.forText("example@example.com"));
invokeAutofill(values);
cnt +=
waitForCallbackAndVerifyTypes(
cnt, new Integer[] {AUTOFILL_VALUE_CHANGED, AUTOFILL_VALUE_CHANGED});
String value1 =
executeJavaScriptAndWaitForResult("document.getElementById('text2').value;");
assertEquals("\"example@example.com\"", value1);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testNoEventSentToAutofillServiceForFocusedDatalist() throws Throwable {
final String url = getAbsoluteTestPageUrl("form_with_datalist.html");
loadUrlSync(url);
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text2').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
// Verify not notifying AUTOFILL_VIEW_ENTERED and AUTOFILL_VALUE_CHANGED events for the
// datalist.
cnt +=
waitForCallbackAndVerifyTypes(
cnt, new Integer[] {AUTOFILL_CANCEL, AUTOFILL_SESSION_STARTED});
// Verify input accepted.
String value1 =
executeJavaScriptAndWaitForResult("document.getElementById('text2').value;");
assertEquals("\"a\"", value1);
// Move cursor to text1 and enter something.
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
// Verify no AUTOFILL_VIEW_EXITED sent for datalist and autofill service shall get the
// events from the change of text1.
cnt +=
waitForCallbackAndVerifyTypes(
cnt, new Integer[] {AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testDatalistPopup() throws Throwable {
final String url = getAbsoluteTestPageUrl("form_with_datalist.html");
loadUrlSync(url);
executeJavaScriptAndWaitForResult("document.getElementById('text2').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
pollDatalistPopupShown(2);
TouchCommon.singleClickView(
mAutofillProvider.getDatalistPopupForTesting().getListView().getChildAt(1));
// Verify the selection accepted by renderer.
pollJavascriptResult("document.getElementById('text2').value;", "\"A2\"");
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testHideDatalistPopup() throws Throwable {
final String url = getAbsoluteTestPageUrl("form_with_datalist.html");
loadUrlSync(url);
executeJavaScriptAndWaitForResult("document.getElementById('text2').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
pollDatalistPopupShown(2);
TestThreadUtils.runOnUiThreadBlocking(
() -> {
mAwContents.hideAutofillPopup();
});
assertNull(mAutofillProvider.getDatalistPopupForTesting());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testVisibility() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<input type='text' id='text1' name='username'>
<input type='text' name='email' id='text2' style='display: none;' />
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
// Verifies the visibility set correctly.
assertEquals(View.VISIBLE, viewStructure.getChild(0).getVisibility());
assertEquals(View.INVISIBLE, viewStructure.getChild(1).getVisibility());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testServerPredictionArrivesBeforeAutofillStart() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<input type='text' id='text1' name='username'>
<input type='text' name='email' id='text2' autocomplete='email' />
</form>""");
TestThreadUtils.runOnUiThreadBlocking(
() ->
AutofillProviderTestHelper
.simulateMainFramePredictionsAutofillServerResponseForTesting(
mAwContents.getWebContents(),
new String[] {"text1", "text2"},
new int[][] {
{86 /* USERNAME */, 9 /* EMAIL_ADDRESS */,},
{9 /* EMAIL_ADDRESS */,}
}));
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
assertEquals(
"USERNAME",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"USERNAME",
viewStructure.getChild(0).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"USERNAME,EMAIL_ADDRESS",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
assertEquals(
"EMAIL_ADDRESS",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"HTML_TYPE_EMAIL",
viewStructure.getChild(1).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"EMAIL_ADDRESS",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
// Binder will not be set if the prediction already arrives.
IBinder binder = viewStructure.getExtras().getBinder("AUTOFILL_HINTS_SERVICE");
assertNull(binder);
}
/** Tests that server predictions are mapped to the fields of a cross-frame form. */
@Test
@SmallTest
@Feature({"AndroidWebView"})
@CommandLineFlags.Add({"enable-features=AutofillAcrossIframes"})
public void testCrossFrameServerPredictionArrivesBeforeAutofillStart() throws Throwable {
loadHTML(
"""
<form>
<input id=name>
<iframe srcdoc='<form action=arbitrary.html method=GET>
<input id=num autocomplete=cc-number></form>' sandbox></iframe>
<iframe srcdoc='<input id=exp>'></iframe>
<iframe srcdoc='<input id=csc>'></iframe>
</form>""");
TestThreadUtils.runOnUiThreadBlocking(
() ->
AutofillProviderTestHelper
.simulateMainFramePredictionsAutofillServerResponseForTesting(
mAwContents.getWebContents(),
new String[] {"name", "num", "exp", "csc"},
new int[][] {
{51 /* CREDIT_CARD_NAME_FULL */},
{52 /*CREDIT_CARD_NUMBER*/},
{
56 /*CREDIT_CARD_EXP_DATE_2_DIGIT_YEAR*/,
57 /*CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR*/,
},
{59 /*CREDIT_CARD_VERIFICATION_CODE*/}
}));
int cnt = 0;
executeJavaScriptAndWaitForResult("document.forms[0].elements[0].select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(4, viewStructure.getChildCount());
// Name field.
assertEquals(
"CREDIT_CARD_NAME_FULL",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"CREDIT_CARD_NAME_FULL",
viewStructure.getChild(0).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"CREDIT_CARD_NAME_FULL",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
// Number field.
assertEquals(
"CREDIT_CARD_NUMBER",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"HTML_TYPE_CREDIT_CARD_NUMBER",
viewStructure.getChild(1).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"CREDIT_CARD_NUMBER",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
// Expiration date field.
assertEquals(
"CREDIT_CARD_EXP_DATE_2_DIGIT_YEAR",
viewStructure
.getChild(2)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"CREDIT_CARD_EXP_DATE_2_DIGIT_YEAR",
viewStructure.getChild(2).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"CREDIT_CARD_EXP_DATE_2_DIGIT_YEAR,CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR",
viewStructure
.getChild(2)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
// CVC field.
assertEquals(
"CREDIT_CARD_VERIFICATION_CODE",
viewStructure
.getChild(3)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"CREDIT_CARD_VERIFICATION_CODE",
viewStructure.getChild(3).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"CREDIT_CARD_VERIFICATION_CODE",
viewStructure
.getChild(3)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
// Binder is not set if the prediction has already arrived.
IBinder binder = viewStructure.getExtras().getBinder("AUTOFILL_HINTS_SERVICE");
assertNull(binder);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testServerPredictionPrimaryTypeArrivesBeforeAutofillStart() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<input type='text' id='text1' name='username'>
<input type='text' name='email' id='text2' autocomplete='email' />
</form>""");
TestThreadUtils.runOnUiThreadBlocking(
() ->
AutofillProviderTestHelper
.simulateMainFrameAutofillServerResponseForTesting(
mAwContents.getWebContents(),
new String[] {"text1", "text2"},
new int[] {86 /* USERNAME */, 9 /* EMAIL_ADDRESS */}));
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
assertEquals(
"USERNAME",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"USERNAME",
viewStructure.getChild(0).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"USERNAME",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
assertEquals(
"EMAIL_ADDRESS",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"HTML_TYPE_EMAIL",
viewStructure.getChild(1).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertEquals(
"EMAIL_ADDRESS",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
// Binder will not be set if the prediction already arrives.
IBinder binder = viewStructure.getExtras().getBinder("AUTOFILL_HINTS_SERVICE");
assertNull(binder);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testServerPredictionArrivesAfterAutofillStart() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<input type='text' id='text1' name='username'>
<input type='text' name='email' id='text2' autocomplete='email' />
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
assertEquals(
"NO_SERVER_DATA",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"UNKNOWN_TYPE",
viewStructure.getChild(0).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertNull(
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
assertEquals(
"NO_SERVER_DATA",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"HTML_TYPE_EMAIL",
viewStructure.getChild(1).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertNull(
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
IBinder binder = viewStructure.getExtras().getBinder("AUTOFILL_HINTS_SERVICE");
assertNotNull(binder);
AutofillHintsServiceTestHelper autofillHintsServiceTestHelper =
new AutofillHintsServiceTestHelper();
autofillHintsServiceTestHelper.registerViewTypeService(binder);
TestThreadUtils.runOnUiThreadBlocking(
() ->
AutofillProviderTestHelper
.simulateMainFramePredictionsAutofillServerResponseForTesting(
mAwContents.getWebContents(),
new String[] {"text1", "text2"},
new int[][] {
{86 /* USERNAME */, 9 /* EMAIL_ADDRESS */},
{9 /* EMAIL_ADDRESS */}
}));
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_PREDICTIONS_AVAILABLE});
assertTrue(mTestAutofillManagerWrapper.isQuerySucceed());
autofillHintsServiceTestHelper.waitForCallbackInvoked();
List<ViewType> viewTypes = autofillHintsServiceTestHelper.getViewTypes();
assertEquals(2, viewTypes.size());
assertEquals(viewStructure.getChild(0).getAutofillId(), viewTypes.get(0).mAutofillId);
assertEquals("USERNAME", viewTypes.get(0).mServerType);
assertEquals("USERNAME", viewTypes.get(0).mComputedType);
assertArrayEquals(
new String[] {"USERNAME", "EMAIL_ADDRESS"},
viewTypes.get(0).getServerPredictions());
assertEquals(viewStructure.getChild(1).getAutofillId(), viewTypes.get(1).mAutofillId);
assertEquals("EMAIL_ADDRESS", viewTypes.get(1).mServerType);
assertEquals("HTML_TYPE_EMAIL", viewTypes.get(1).mComputedType);
assertArrayEquals(new String[] {"EMAIL_ADDRESS"}, viewTypes.get(1).getServerPredictions());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testServerPredictionPrimaryTypeArrivesAfterAutofillStart() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<input type='text' id='text1' name='username'>
<input type='text' name='email' id='text2' autocomplete='email' />
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
assertEquals(
"NO_SERVER_DATA",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"UNKNOWN_TYPE",
viewStructure.getChild(0).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertNull(
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
assertEquals(
"NO_SERVER_DATA",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"HTML_TYPE_EMAIL",
viewStructure.getChild(1).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertNull(
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
IBinder binder = viewStructure.getExtras().getBinder("AUTOFILL_HINTS_SERVICE");
assertNotNull(binder);
AutofillHintsServiceTestHelper autofillHintsServiceTestHelper =
new AutofillHintsServiceTestHelper();
autofillHintsServiceTestHelper.registerViewTypeService(binder);
TestThreadUtils.runOnUiThreadBlocking(
() ->
AutofillProviderTestHelper
.simulateMainFrameAutofillServerResponseForTesting(
mAwContents.getWebContents(),
new String[] {"text1", "text2"},
new int[] {86 /* USERNAME */, 9 /* EMAIL_ADDRESS */}));
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_PREDICTIONS_AVAILABLE});
assertTrue(mTestAutofillManagerWrapper.isQuerySucceed());
autofillHintsServiceTestHelper.waitForCallbackInvoked();
List<ViewType> viewTypes = autofillHintsServiceTestHelper.getViewTypes();
assertEquals(2, viewTypes.size());
assertEquals(viewStructure.getChild(0).getAutofillId(), viewTypes.get(0).mAutofillId);
assertEquals("USERNAME", viewTypes.get(0).mServerType);
assertEquals("USERNAME", viewTypes.get(0).mComputedType);
assertArrayEquals(new String[] {"USERNAME"}, viewTypes.get(0).getServerPredictions());
assertEquals(viewStructure.getChild(1).getAutofillId(), viewTypes.get(1).mAutofillId);
assertEquals("EMAIL_ADDRESS", viewTypes.get(1).mServerType);
assertEquals("HTML_TYPE_EMAIL", viewTypes.get(1).mComputedType);
assertArrayEquals(new String[] {"EMAIL_ADDRESS"}, viewTypes.get(1).getServerPredictions());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testServerPredictionArrivesBeforeCallbackRegistered() throws Throwable {
loadHTML(
"""
<form action='a.html' name='formname'>
<input type='text' id='text1' name='username'>
<input type='text' name='email' id='text2' autocomplete='email' />
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
assertEquals(
"NO_SERVER_DATA",
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"UNKNOWN_TYPE",
viewStructure.getChild(0).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertNull(
viewStructure
.getChild(0)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
assertEquals(
"NO_SERVER_DATA",
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-autofill-hints"));
assertEquals(
"HTML_TYPE_EMAIL",
viewStructure.getChild(1).getHtmlInfo().getAttribute("computed-autofill-hints"));
assertNull(
viewStructure
.getChild(1)
.getHtmlInfo()
.getAttribute("crowdsourcing-predictions-autofill-hints"));
TestThreadUtils.runOnUiThreadBlocking(
() ->
AutofillProviderTestHelper
.simulateMainFramePredictionsAutofillServerResponseForTesting(
mAwContents.getWebContents(),
new String[] {"text1", "text2"},
new int[][] {
{86 /* USERNAME */, 9 /* EMAIL_ADDRESS */},
{9 /* EMAIL_ADDRESS */}
}));
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_PREDICTIONS_AVAILABLE});
assertTrue(mTestAutofillManagerWrapper.isQuerySucceed());
IBinder binder = viewStructure.getExtras().getBinder("AUTOFILL_HINTS_SERVICE");
assertNotNull(binder);
AutofillHintsServiceTestHelper autofillHintsServiceTestHelper =
new AutofillHintsServiceTestHelper();
autofillHintsServiceTestHelper.registerViewTypeService(binder);
autofillHintsServiceTestHelper.waitForCallbackInvoked();
List<ViewType> viewTypes = autofillHintsServiceTestHelper.getViewTypes();
assertEquals(2, viewTypes.size());
assertEquals(viewStructure.getChild(0).getAutofillId(), viewTypes.get(0).mAutofillId);
assertEquals("USERNAME", viewTypes.get(0).mServerType);
assertEquals("USERNAME", viewTypes.get(0).mComputedType);
assertArrayEquals(
new String[] {"USERNAME", "EMAIL_ADDRESS"},
viewTypes.get(0).getServerPredictions());
assertEquals(viewStructure.getChild(1).getAutofillId(), viewTypes.get(1).mAutofillId);
assertEquals("EMAIL_ADDRESS", viewTypes.get(1).mServerType);
assertEquals("HTML_TYPE_EMAIL", viewTypes.get(1).mComputedType);
assertArrayEquals(new String[] {"EMAIL_ADDRESS"}, viewTypes.get(1).getServerPredictions());
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testFieldAddedBeforeSuggestionSelected() throws Throwable {
// This test verifies that form filling works even in the case that the form has been
// modified (field was added) in the DOM between the decision to fill and executing the
// fill.
loadHTML(
"""
<form action='a.html' name='formname'>
<label>User Name:</label>
<input type='text' id='text1' name='name' />
<label>Password:</label>
<input type='password' id='pwdid' name='pwd' />
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text1').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
// Append a field.
executeJavaScriptAndWaitForResult(
"""
document.getElementById('pwdid').insertAdjacentHTML(
'afterend', '<input type=\"password\" id=\"pwdid2\"/>');""");
// Autofill the original form.
SparseArray<AutofillValue> values = new SparseArray<AutofillValue>();
values.append(
viewStructure.getChild(0).getId(), AutofillValue.forText("example@example.com"));
values.append(viewStructure.getChild(1).getId(), AutofillValue.forText("password"));
cnt = getCallbackCount();
clearChangedValues();
invokeAutofill(values);
waitForCallbackAndVerifyTypes(
cnt, new Integer[] {AUTOFILL_VALUE_CHANGED, AUTOFILL_VALUE_CHANGED});
String value0 =
executeJavaScriptAndWaitForResult("document.getElementById('text1').value;");
assertEquals("\"example@example.com\"", value0);
String value1 =
executeJavaScriptAndWaitForResult("document.getElementById('pwdid').value;");
assertEquals("\"password\"", value1);
String value2 =
executeJavaScriptAndWaitForResult("document.getElementById('pwdid2').value;");
assertEquals("\"\"", value2);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testFirstFieldRemovedBeforeSuggestionSelected() throws Throwable {
// This test verifies that form filling works even if an element of the form that was
// supposed to be filled has been deleted between the time of decision to fill the form and
// executing the fill.
loadHTML(
"""
<form action='a.html' name='formname'>
<label>User Name:</label>
<input type='text' id='text1' name='name' />
<label>Password:</label>
<input type='password' id='pwdid' name='pwd' />
</form>""");
int cnt = 0;
// Focus on the second element, since the first one is about to be removed. Removing the
// element on which the fill was triggered would cancel the filling operation.
executeJavaScriptAndWaitForResult("document.getElementById('pwdid').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
invokeOnProvideAutoFillVirtualStructure();
TestViewStructure viewStructure = mTestValues.testViewStructure;
assertNotNull(viewStructure);
assertEquals(2, viewStructure.getChildCount());
// Remove the first field.
executeJavaScriptAndWaitForResult("document.getElementById('text1').remove()");
// Autofill the original form.
SparseArray<AutofillValue> values = new SparseArray<AutofillValue>();
values.append(
viewStructure.getChild(0).getId(), AutofillValue.forText("example@example.com"));
values.append(viewStructure.getChild(1).getId(), AutofillValue.forText("password"));
cnt = getCallbackCount();
clearChangedValues();
invokeAutofill(values);
waitForCallbackAndVerifyTypes(
cnt, new Integer[] {AUTOFILL_VALUE_CHANGED, AUTOFILL_VALUE_CHANGED});
String value1 =
executeJavaScriptAndWaitForResult("document.getElementById('pwdid').value;");
assertEquals("\"password\"", value1);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testFrameDetachedOnFormSubmission() throws Throwable {
final String subFrame =
"""
<html>
<body>
<script>
function send_post() {
window.parent.postMessage('SubmitComplete', '*');
}
</script>
<form action='inner_frame_address_form.html' id='deleting_form'
onsubmit='send_post(); return false;'>
<input type='text' id='address_field' name='address'
autocomplete='on'>
<input type='submit' id='submit_button'
name='submit_button'>
</form>
</body>
</html>""";
final String subFrameURL =
mWebServer.setResponse("/inner_frame_address_form.html", subFrame, null);
assertTrue(Uri.parse(subFrameURL).getPath().equals("/inner_frame_address_form.html"));
loadHTML(
"""
<script>
function receiveMessage(event) {
var address_iframe = document.getElementById('address_iframe');
address_iframe.parentNode.removeChild(address_iframe);
setTimeout(delayedUpload, 0);
}
window.addEventListener('message', receiveMessage, false);
</script>
<iframe src='inner_frame_address_form.html' id='address_iframe'
name='address_iframe'>
</iframe>""");
int cnt = 0;
pollJavascriptResult(
"""
var iframe = document.getElementById('address_iframe');
var frame_doc = iframe.contentDocument;
frame_doc.getElementById('address_field').focus();
frame_doc.activeElement.id;""",
"\"address_field\"");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
executeJavaScriptAndWaitForResult(
"""
var iframe = document.getElementById('address_iframe');
var frame_doc = iframe.contentDocument;
frame_doc.getElementById('submit_button').click();""");
waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT});
assertEquals(SubmissionSource.FORM_SUBMISSION, mSubmissionSource);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testFrameDetachedOnFormlessSubmission() throws Throwable {
final String subFrame =
"""
<html>
<body>
<script>
function send_post() {
window.parent.postMessage('SubmitComplete', '*');
}
</script>
<input type='text' id='address_field' name='address' autocomplete='on'>
<input type='button' id='submit_button' name='submit_button' onclick='send_post()'>
</body>
</html>""";
final String subFrameURL =
mWebServer.setResponse("/inner_frame_address_formless.html", subFrame, null);
assertTrue(Uri.parse(subFrameURL).getPath().equals("/inner_frame_address_formless.html"));
loadHTML(
"""
<script>
function receiveMessage(event) {
var address_iframe = document.getElementById('address_iframe');
address_iframe.parentNode.removeChild(address_iframe);
}
window.addEventListener('message', receiveMessage, false);
</script>
<iframe src='inner_frame_address_formless.html' id='address_iframe' name='address_iframe'>
</iframe>""");
int cnt = 0;
pollJavascriptResult(
"""
var iframe = document.getElementById('address_iframe');
var frame_doc = iframe.contentDocument;
frame_doc.getElementById('address_field').focus();
frame_doc.activeElement.id;""",
"\"address_field\"");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
executeJavaScriptAndWaitForResult(
"""
var iframe = document.getElementById('address_iframe');
var frame_doc = iframe.contentDocument;
frame_doc.getElementById('submit_button').click();""");
// The additional AUTOFILL_VIEW_EXITED event caused by 'click' of the button.
waitForCallbackAndVerifyTypes(
cnt, new Integer[] {AUTOFILL_VIEW_EXITED, AUTOFILL_VALUE_CHANGED, AUTOFILL_COMMIT});
assertEquals(SubmissionSource.FRAME_DETACHED, mSubmissionSource);
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testLabelChange() throws Throwable {
loadHTML(
"""
<form action='a.html'>
<label id='label_id'> Address </label>
<input type='text' id='address' name='address' autocomplete='on' />
<p id='p_id'>Address 1</p>
<input type='text' name='address1' autocomplete='on' />
<input type='submit' id='submit_button' name='submit_button' />
</form>""");
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('address').focus();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
// Verify label change shall trigger new session.
executeJavaScriptAndWaitForResult(
"document.getElementById('label_id').innerHTML='address change';");
executeJavaScriptAndWaitForResult("document.getElementById('address').focus();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
cnt +=
waitForCallbackAndVerifyTypes(
cnt,
new Integer[] {
AUTOFILL_CANCEL,
AUTOFILL_VIEW_EXITED,
AUTOFILL_VIEW_ENTERED,
AUTOFILL_SESSION_STARTED,
AUTOFILL_VALUE_CHANGED
});
// Verify inferred label change won't trigger new session.
executeJavaScriptAndWaitForResult(
"document.getElementById('p_id').innerHTML='address change';");
executeJavaScriptAndWaitForResult("document.getElementById('address').focus();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_B);
cnt += waitForCallbackAndVerifyTypes(cnt, new Integer[] {AUTOFILL_VALUE_CHANGED});
}
private void pollJavascriptResult(String script, String expectedResult) throws Throwable {
AwActivityTestRule.pollInstrumentationThread(
() -> {
try {
return expectedResult.equals(executeJavaScriptAndWaitForResult(script));
} catch (Throwable e) {
return false;
}
});
}
private void pollJavascriptResultNotEqualTo(String script, String result) throws Throwable {
AwActivityTestRule.pollInstrumentationThread(
() -> {
try {
return !result.equals(executeJavaScriptAndWaitForResult(script));
} catch (Throwable e) {
return false;
}
});
}
private void pollDatalistPopupShown(int expectedTotalChildren) {
AwActivityTestRule.pollInstrumentationThread(
() -> {
AutofillPopup popup = mAutofillProvider.getDatalistPopupForTesting();
boolean isShown =
popup != null
&& popup.getListView() != null
&& popup.getListView().getChildCount() == expectedTotalChildren;
for (int i = 0; i < expectedTotalChildren && isShown; i++) {
isShown =
popup.getListView().getChildAt(i).getWidth() > 0
&& popup.getListView().getChildAt(i).isAttachedToWindow();
}
return isShown;
});
}
private void scrollToBottom() {
TestThreadUtils.runOnUiThreadBlocking(
() -> {
mTestContainerView.scrollTo(0, mTestContainerView.getHeight());
});
}
private void loadUrlSync(String url) throws Exception {
CallbackHelper done = mContentsClient.getOnPageCommitVisibleHelper();
int callCount = done.getCallCount();
mRule.loadUrlSync(
mTestContainerView.getAwContents(), mContentsClient.getOnPageFinishedHelper(), url);
done.waitForCallback(callCount);
}
private void reloadSync() throws Exception {
CallbackHelper done = mContentsClient.getOnPageCommitVisibleHelper();
int callCount = done.getCallCount();
mRule.reloadSync(
mTestContainerView.getAwContents(), mContentsClient.getOnPageFinishedHelper());
done.waitForCallback(callCount);
}
private String executeJavaScriptAndWaitForResult(String code) throws Throwable {
return mRule.executeJavaScriptAndWaitForResult(
mTestContainerView.getAwContents(), mContentsClient, code);
}
private ArrayList<Pair<Integer, AutofillValue>> getChangedValues() {
return mTestValues.changedValues;
}
private void clearChangedValues() {
if (mTestValues.changedValues != null) mTestValues.changedValues.clear();
}
private void invokeOnProvideAutoFillVirtualStructure() {
TestThreadUtils.runOnUiThreadBlocking(
() -> {
mTestValues.testViewStructure = new TestViewStructure();
mAwContents.onProvideAutoFillVirtualStructure(mTestValues.testViewStructure, 1);
});
}
private void invokeAutofill(SparseArray<AutofillValue> values) {
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.autofill(values));
}
private void invokeOnInputUIShown() {
TestThreadUtils.runOnUiThreadBlocking(
() -> mTestAutofillManagerWrapper.notifyInputUIChange());
}
private int getCallbackCount() {
return mCallbackHelper.getCallCount();
}
private int clearEventQueueAndGetCallCount() {
mEventQueue.clear();
return mCallbackHelper.getCallCount();
}
/**
* Wait for expected callbacks to be called, and verify the types.
*
* @param currentCallCount The current call count to start from.
* @param expectedEventArray The callback types that need to be verified.
* @return The number of new callbacks since currentCallCount. This should be same as the length
* of expectedEventArray.
* @throws TimeoutException
*/
private int waitForCallbackAndVerifyTypes(int currentCallCount, Integer[] expectedEventArray)
throws TimeoutException {
Integer[] adjustedEventArray;
// Didn't call cancel after Android P.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ArrayList<Integer> adjusted = new ArrayList<Integer>();
for (Integer event : expectedEventArray) {
if (event != AUTOFILL_CANCEL) adjusted.add(event);
}
adjustedEventArray = new Integer[adjusted.size()];
adjusted.toArray(adjustedEventArray);
} else {
adjustedEventArray = expectedEventArray;
}
try {
// Check against the call count to avoid missing out a callback in between waits, while
// exposing it so that the test can control where the call count starts.
mCallbackHelper.waitForCallback(currentCallCount, adjustedEventArray.length);
Object[] objectArray = mEventQueue.toArray();
mEventQueue.clear();
Integer[] resultArray = Arrays.copyOf(objectArray, objectArray.length, Integer[].class);
Assert.assertArrayEquals(
"Expect: "
+ buildEventList(adjustedEventArray)
+ " Result: "
+ buildEventList(resultArray),
adjustedEventArray,
resultArray);
return adjustedEventArray.length;
} catch (TimeoutException e) {
Object[] objectArray = mEventQueue.toArray();
Integer[] resultArray = Arrays.copyOf(objectArray, objectArray.length, Integer[].class);
Assert.assertArrayEquals(
"Expect:"
+ buildEventList(adjustedEventArray)
+ " Result:"
+ buildEventList(resultArray),
adjustedEventArray,
resultArray);
throw e;
}
}
/**
* Consumes all observed events from {@link mEventQueue} until the
* {@code expectedEvents} have been observed (in proper order). Calls
* {@code mCallbackHelper.waitForNext();} in case the {@link mEventQueue}
* runs out of events. Unexpected events are just ignored.
*
* @param expectedEvents the events that need to happen.
* @return Whether the {@code expectedEvents} were observed.
* @throws TimeoutException
*/
private boolean waitForEvents(Integer[] expectedEvents) throws TimeoutException {
// Chosen arbitrarily.
final int maxCallsToWaitFor = 20;
int numCallsToWaitFor = 0;
LinkedList<Integer> expectedEventsQueue =
new LinkedList<Integer>(Arrays.asList(expectedEvents));
while (!expectedEventsQueue.isEmpty() && numCallsToWaitFor < maxCallsToWaitFor) {
if (mEventQueue.isEmpty()) {
// Wait for new events.
++numCallsToWaitFor;
mCallbackHelper.waitForNext();
continue;
}
int nextExpectedEvent = expectedEventsQueue.peek();
// Actually consumes the event.
int nextObservedEvent = mEventQueue.poll();
if (nextExpectedEvent == nextObservedEvent) {
expectedEventsQueue.poll();
}
}
return expectedEventsQueue.isEmpty();
}
private static String buildEventList(Integer[] eventArray) {
Assert.assertEquals(EVENT.length, AUTOFILL_EVENT_MAX);
List<String> result = new ArrayList<String>(eventArray.length);
for (Integer event : eventArray) result.add(EVENT[event]);
return TextUtils.join(",", result);
}
private void dispatchDownAndUpKeyEvents(final int code) throws Throwable {
long eventTime = SystemClock.uptimeMillis();
dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, code, 0));
dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, code, 0));
}
private boolean dispatchKeyEvent(final KeyEvent event) throws Throwable {
return TestThreadUtils.runOnUiThreadBlocking(
new Callable<Boolean>() {
@Override
public Boolean call() {
return mTestContainerView.dispatchKeyEvent(event);
}
});
}
/**
* Loads an HTML snippet which will be used by the test to execute JS commands on. This snipped
* is loaded on the test web server.
*
* @param htmlBody The body of the HTML snippet to be loaded.
* @return The url where the loaded HTML can be found on the test web server.
* @throws Exception
*/
private String loadHTML(String htmlBody) throws Exception {
final String data =
String.format(
"""
<html>
<head></head>
<body>
%s
</body>
</html>
""",
htmlBody);
final String url = mWebServer.setResponse(FILE, data, null);
loadUrlSync(url);
return url;
}
}