diff --git a/mobile/android/geckoview/src/androidTest/assets/www/forms.html b/mobile/android/geckoview/src/androidTest/assets/www/forms.html
new file mode 100644
index 000000000000..4bd790040d78
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/www/forms.html
@@ -0,0 +1,26 @@
+
+
Forms
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
index c5e48f59afc8..1b2234569a25 100644
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
@@ -16,6 +16,8 @@ import android.os.Bundle
import android.support.test.filters.MediumTest
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
+import android.text.InputType
+import android.util.SparseLongArray
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProvider
@@ -23,6 +25,7 @@ import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityRecord
import android.view.View
import android.view.ViewGroup
+import android.widget.EditText
import android.widget.FrameLayout
@@ -66,6 +69,16 @@ class AccessibilityTest : BaseSessionTest() {
}
}
+ // Get a child ID by index.
+ private fun AccessibilityNodeInfo.getChildId(index: Int): Int =
+ getVirtualDescendantId(
+ if (Build.VERSION.SDK_INT >= 21)
+ AccessibilityNodeInfo::class.java.getMethod(
+ "getChildId", Int::class.java).invoke(this, index) as Long
+ else
+ (AccessibilityNodeInfo::class.java.getMethod("getChildNodeIds")
+ .invoke(this) as SparseLongArray).get(index))
+
private interface EventDelegate {
fun onAccessibilityFocused(event: AccessibilityEvent) { }
fun onClicked(event: AccessibilityEvent) { }
@@ -562,4 +575,154 @@ class AccessibilityTest : BaseSessionTest() {
}
})
}
+
+ @WithDevToolsAPI
+ @Test fun autoFill() {
+ // Wait for the accessibility nodes to populate.
+ mainSession.loadTestPath(FORMS_HTML_PATH)
+ sessionRule.waitUntilCalled(object : EventDelegate {
+ // For the root document and the iframe document, each has a form group and
+ // a group for inputs outside of forms, so the total count is 4.
+ @AssertCalled(count = 4)
+ override fun onWinContentChanged(event: AccessibilityEvent) {
+ }
+ })
+
+ val autoFills = mapOf(
+ "#user1" to "bar", "#pass1" to "baz", "#user2" to "bar", "#pass2" to "baz") +
+ if (Build.VERSION.SDK_INT >= 19) mapOf(
+ "#email1" to "a@b.c", "#number1" to "24", "#tel1" to "42")
+ else mapOf(
+ "#email1" to "bar", "#number1" to "", "#tel1" to "bar")
+
+ // Set up promises to monitor the values changing.
+ val promises = autoFills.flatMap { entry ->
+ // Repeat each test with both the top document and the iframe document.
+ arrayOf("document", "$('#iframe').contentDocument").map { doc ->
+ mainSession.evaluateJS("""new Promise(resolve =>
+ $doc.querySelector('${entry.key}').addEventListener(
+ 'input', event => resolve([event.target.value, '${entry.value}']),
+ { once: true }))""").asJSPromise()
+ }
+ }
+
+ // Perform auto-fill and return number of auto-fills performed.
+ fun autoFillChild(id: Int, child: AccessibilityNodeInfo) {
+ // Seal the node info instance so we can perform actions on it.
+ if (child.childCount > 0) {
+ for (i in 0 until child.childCount) {
+ val childId = child.getChildId(i)
+ autoFillChild(childId, provider.createAccessibilityNodeInfo(childId))
+ }
+ }
+
+ if (EditText::class.java.name == child.className) {
+ assertThat("Input should be enabled", child.isEnabled, equalTo(true))
+ assertThat("Input should be focusable", child.isFocusable, equalTo(true))
+ if (Build.VERSION.SDK_INT >= 19) {
+ assertThat("Password type should match", child.isPassword, equalTo(
+ child.inputType == InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD))
+ }
+
+ val args = Bundle(1)
+ val value = if (child.isPassword) "baz" else
+ if (Build.VERSION.SDK_INT < 19) "bar" else
+ when (child.inputType) {
+ InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "a@b.c"
+ InputType.TYPE_CLASS_NUMBER -> "24"
+ InputType.TYPE_CLASS_PHONE -> "42"
+ else -> "bar"
+ }
+
+ val ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE = if (Build.VERSION.SDK_INT >= 21)
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE else
+ "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"
+ val ACTION_SET_TEXT = if (Build.VERSION.SDK_INT >= 21)
+ AccessibilityNodeInfo.ACTION_SET_TEXT else 0x200000
+
+ args.putCharSequence(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
+ assertThat("Can perform auto-fill",
+ provider.performAction(id, ACTION_SET_TEXT, args), equalTo(true))
+ }
+ child.recycle()
+ }
+
+ autoFillChild(View.NO_ID, provider.createAccessibilityNodeInfo(View.NO_ID))
+
+ // Wait on the promises and check for correct values.
+ for ((actual, expected) in promises.map { it.value.asJSList() }) {
+ assertThat("Auto-filled value must match", actual, equalTo(expected))
+ }
+ }
+
+ @Test fun autoFill_navigation() {
+ fun countAutoFillNodes(cond: (AccessibilityNodeInfo) -> Boolean =
+ { it.className == "android.widget.EditText" },
+ id: Int = View.NO_ID): Int {
+ val info = provider.createAccessibilityNodeInfo(id)
+ try {
+ return (if (cond(info)) 1 else 0) + (if (info.childCount > 0)
+ (0 until info.childCount).sumBy {
+ countAutoFillNodes(cond, info.getChildId(it))
+ } else 0)
+ } finally {
+ info.recycle()
+ }
+ }
+
+ // Wait for the accessibility nodes to populate.
+ mainSession.loadTestPath(FORMS_HTML_PATH)
+ sessionRule.waitUntilCalled(object : EventDelegate {
+ @AssertCalled(count = 4)
+ override fun onWinContentChanged(event: AccessibilityEvent) {
+ }
+ })
+ assertThat("Initial auto-fill count should match",
+ countAutoFillNodes(), equalTo(14))
+ assertThat("Password auto-fill count should match",
+ countAutoFillNodes({ it.isPassword }), equalTo(4))
+
+ // Now wait for the nodes to clear.
+ mainSession.loadTestPath(HELLO_HTML_PATH)
+ mainSession.waitForPageStop()
+ assertThat("Should not have auto-fill fields",
+ countAutoFillNodes(), equalTo(0))
+
+ // Now wait for the nodes to reappear.
+ mainSession.goBack()
+ sessionRule.waitUntilCalled(object : EventDelegate {
+ @AssertCalled(count = 4)
+ override fun onWinContentChanged(event: AccessibilityEvent) {
+ }
+ })
+ assertThat("Should have auto-fill fields again",
+ countAutoFillNodes(), equalTo(14))
+ assertThat("Should not have focused field",
+ countAutoFillNodes({ it.isFocused }), equalTo(0))
+
+ mainSession.evaluateJS("$('#pass1').focus()")
+ sessionRule.waitUntilCalled(object : EventDelegate {
+ @AssertCalled
+ override fun onFocused(event: AccessibilityEvent) {
+ }
+ })
+ assertThat("Should have one focused field",
+ countAutoFillNodes({ it.isFocused }), equalTo(1))
+ // The focused field, its siblings, and its parent should be visible.
+ assertThat("Should have at least six visible fields",
+ countAutoFillNodes({ node -> node.isVisibleToUser &&
+ !(Rect().also({ node.getBoundsInScreen(it) }).isEmpty) }),
+ greaterThanOrEqualTo(6))
+
+ mainSession.evaluateJS("$('#pass1').blur()")
+ sessionRule.waitUntilCalled(object : EventDelegate {
+ @AssertCalled
+ override fun onFocused(event: AccessibilityEvent) {
+ }
+ })
+ assertThat("Should not have focused field",
+ countAutoFillNodes({ it.isFocused }), equalTo(0))
+ }
}
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
index 6c51cf78915d..b4171be6fa43 100644
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -28,6 +28,7 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
const val CONTENT_CRASH_URL = "about:crashcontent"
const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
+ const val FORMS_HTML_PATH = "/assets/www/forms.html"
const val HELLO_HTML_PATH = "/assets/www/hello.html"
const val HELLO2_HTML_PATH = "/assets/www/hello2.html"
const val INPUTS_PATH = "/assets/www/inputs.html"