Bug 1330257 - 6. Add tests for auto-fill accessibility frontend; r=eeejay
☠☠ backed out by 3aca6b42ca4e ☠ ☠
authorJim Chen <nchen@mozilla.com>
Mon, 20 Aug 2018 22:28:21 -0400
changeset 487625 62e53fa35d5bb177f50b7e5ee3fd23a0b282a2b8
parent 487624 7c589fc8621bef8c56954284e2a3dd9d3c96eb3d
child 487626 53ef69afca6959ca87e5c4cfb97955371d729fab
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseeejay
bugs1330257
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1330257 - 6. Add tests for auto-fill accessibility frontend; r=eeejay Add some tests to AccessibilityTest to make sure we can perform auto-fill through the accessibility API. Differential Revision: https://phabricator.services.mozilla.com/D3255
mobile/android/geckoview/src/androidTest/assets/www/forms.html
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/www/forms.html
@@ -0,0 +1,26 @@
+<html>
+    <head><title>Forms</title></head>
+    <body>
+        <form>
+            <input type="text" id="user1" value="foo">
+            <input type="password" id="pass1", value="foo">
+            <input type="email" id="email1", value="@">
+            <input type="number" id="number1", value="0">
+            <input type="tel" id="tel1", value="0">
+        </form>
+        <input type="Text" id="user2" value="foo">
+        <input type="PassWord" id="pass2" maxlength="8" value="foo">
+        <input type="button" id="button1" value="foo"/>
+        <input type="checkbox" id="checkbox1"/>
+        <input type="hidden" id="hidden1" value="foo"/>
+
+        <iframe id="iframe"></iframe>
+    </body>
+    <script>
+        addEventListener("load", function(e) {
+            if (window.parent === window) {
+                document.getElementById("iframe").contentWindow.location.href = window.location.href;
+            }
+        });
+    </script>
+</html>
--- 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
@@ -11,23 +11,26 @@ import org.mozilla.geckoview.test.rule.G
 import android.graphics.Rect
 
 import android.os.Build
 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
 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
 
 import org.hamcrest.Matchers.*
 import org.junit.Test
 import org.junit.Before
 import org.junit.After
 import org.junit.runner.RunWith
@@ -61,16 +64,26 @@ class AccessibilityTest : BaseSessionTes
             val getSourceIdMethod =
                 AccessibilityRecord::class.java.getMethod("getSourceNodeId")
             return getVirtualDescendantId(getSourceIdMethod.invoke(event) as Long)
         } catch (ex: Exception) {
             return 0
         }
     }
 
+    // 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) { }
         fun onFocused(event: AccessibilityEvent) { }
         fun onSelected(event: AccessibilityEvent) { }
         fun onScrolled(event: AccessibilityEvent) { }
         fun onTextSelectionChanged(event: AccessibilityEvent) { }
         fun onTextChanged(event: AccessibilityEvent) { }
@@ -557,9 +570,159 @@ class AccessibilityTest : BaseSessionTes
 
             @AssertCalled(count = 1, order = [3])
             override fun onWinContentChanged(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
             }
         })
     }
+
+    @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<String>() }) {
+            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))
+    }
 }
--- 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
@@ -23,16 +23,17 @@ import kotlin.reflect.KClass
  * Common base class for tests using GeckoSessionTestRule,
  * providing the test rule and other utilities.
  */
 open class BaseSessionTest(noErrorCollector: Boolean = false) {
     companion object {
         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"
         const val INVALID_URI = "http://www.test.invalid/"
         const val LOREM_IPSUM_HTML_PATH = "/assets/www/loremIpsum.html"
         const val NEW_SESSION_HTML_PATH = "/assets/www/newSession.html"
         const val NEW_SESSION_CHILD_HTML_PATH = "/assets/www/newSession_child.html"
         const val SAVE_STATE_PATH = "/assets/www/saveState.html"