Bug 1330257 - 8. Add tests for Oreo auto-fill frontend; r=snorp
authorJim Chen <nchen@mozilla.com>
Thu, 23 Aug 2018 17:03:35 -0400
changeset 490951 3758580bd8895d0a0df7762fbc38b2b746ecef5d
parent 490950 ef90e2fdfa1a4cd0eb71c32020335465c7f2244d
child 490952 d81eb0bdb46afb881262bf2567d9604fe57bd762
push id1815
push userffxbld-merge
push dateMon, 15 Oct 2018 10:40:45 +0000
treeherdermozilla-release@18d4c09e9378 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
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 - 8. Add tests for Oreo auto-fill frontend; r=snorp Add some tests for the Oreo auto-fill frontend, similar to the tests for the a11y auto-fill frontend. However, because these tests depend on the ViewStructure class, they require SDK 23+ to run. Differential Revision: https://phabricator.services.mozilla.com/D3810
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -1,27 +1,35 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
+import android.app.assist.AssistStructure
+import android.os.Build
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
 import org.mozilla.geckoview.test.util.UiThreadUtils
 
 import android.os.Looper
 import android.support.test.filters.MediumTest
+import android.support.test.filters.SdkSuppress
 import android.support.test.runner.AndroidJUnit4
+import android.text.InputType
+import android.util.SparseArray
+import android.view.View
+import android.view.ViewStructure
+import android.widget.EditText
 import org.hamcrest.Matchers.*
 import org.junit.Assume.assumeThat
 import org.junit.Test
 import org.junit.runner.RunWith
 
 import kotlin.concurrent.thread
 
 @RunWith(AndroidJUnit4::class)
@@ -179,9 +187,232 @@ class ContentDelegateTest : BaseSessionT
             Looper.loop()
         }
 
         worker.join(sessionRule.timeoutMillis)
         if (worker.isAlive) {
             throw UiThreadUtils.TimeoutException("Timed out")
         }
     }
+
+    val ViewNode by lazy {
+        AssistStructure.ViewNode::class.java.getDeclaredConstructor().apply { isAccessible = true }
+    }
+
+    val ViewNodeBuilder by lazy {
+        Class.forName("android.app.assist.AssistStructure\$ViewNodeBuilder")
+                .getDeclaredConstructor(AssistStructure::class.java,
+                                        AssistStructure.ViewNode::class.java,
+                                        Boolean::class.javaPrimitiveType)
+                .apply { isAccessible = true }
+    }
+
+    // TextInputDelegateTest is parameterized, so we put this test under ContentDelegateTest.
+    @SdkSuppress(minSdkVersion = 23)
+    @WithDevToolsAPI
+    @Test fun autofill() {
+        // Test parts of the Oreo auto-fill API; there is another autofill test in
+        // SessionAccessibility for a11y auto-fill support.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        // Wait for the auto-fill nodes to populate.
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            // 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 notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+            }
+        })
+
+        val autoFills = mapOf(
+                "#user1" to "bar", "#user2" to "bar") +
+                if (Build.VERSION.SDK_INT >= 26) mapOf(
+                        "#pass1" to "baz", "#pass2" to "baz", "#email1" to "a@b.c",
+                        "#number1" to "24", "#tel1" to "42")
+                else mapOf(
+                        "#pass1" to "bar", "#pass2" to "bar", "#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()
+            }
+        }
+
+        val rootNode = ViewNode.newInstance()
+        val rootStructure = ViewNodeBuilder.newInstance(AssistStructure(), rootNode,
+                /* async */ false) as ViewStructure
+        val autoFillValues = SparseArray<CharSequence>()
+
+        // Perform auto-fill and return number of auto-fills performed.
+        fun checkAutoFillChild(child: AssistStructure.ViewNode) {
+            // Seal the node info instance so we can perform actions on it.
+            if (child.childCount > 0) {
+                for (i in 0 until child.childCount) {
+                    checkAutoFillChild(child.getChildAt(i))
+                }
+            }
+
+            if (child === rootNode) {
+                return
+            }
+
+            assertThat("ID should be valid", child.id, not(equalTo(View.NO_ID)))
+
+            if (Build.VERSION.SDK_INT >= 26) {
+                assertThat("Should have HTML tag",
+                           child.htmlInfo.tag, not(isEmptyOrNullString()))
+                assertThat("Web domain should match",
+                           child.webDomain, equalTo("android"))
+            }
+
+            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))
+                assertThat("Input should be visible",
+                           child.visibility, equalTo(View.VISIBLE))
+
+                if (Build.VERSION.SDK_INT < 26) {
+                    autoFillValues.append(child.id, "bar")
+                    return
+                }
+
+                val htmlInfo = child.htmlInfo
+                assertThat("Should have HTML tag", htmlInfo.tag, equalTo("input"))
+                assertThat("Should have ID attribute",
+                           htmlInfo.attributes.map { it.first }, hasItem("id"))
+
+                assertThat("Autofill type should match",
+                           child.autofillType, equalTo(View.AUTOFILL_TYPE_TEXT))
+
+                assertThat("Autofill hints should match", child.autofillHints, equalTo(
+                        when (child.inputType) {
+                            InputType.TYPE_CLASS_TEXT or
+                                    InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ->
+                                arrayOf(View.AUTOFILL_HINT_PASSWORD)
+                            InputType.TYPE_CLASS_TEXT or
+                                    InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ->
+                                arrayOf(View.AUTOFILL_HINT_EMAIL_ADDRESS)
+                            InputType.TYPE_CLASS_PHONE -> arrayOf(View.AUTOFILL_HINT_PHONE)
+                            else -> null
+                        }))
+
+                autoFillValues.append(child.id, when (child.inputType) {
+                    InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "baz"
+                    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"
+                })
+            }
+        }
+
+        mainSession.textInput.onProvideAutofillVirtualStructure(rootStructure, 0)
+        checkAutoFillChild(rootNode)
+        mainSession.textInput.autofill(autoFillValues)
+
+        // 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))
+        }
+    }
+
+    // TextInputDelegateTest is parameterized, so we put this test under ContentDelegateTest.
+    @SdkSuppress(minSdkVersion = 23)
+    @WithDevToolsAPI
+    @WithDisplay(width = 100, height = 100)
+    @Test fun autoFill_navigation() {
+
+        fun countAutoFillNodes(cond: (AssistStructure.ViewNode) -> Boolean =
+                                       { it.className == "android.widget.EditText" },
+                               root: AssistStructure.ViewNode? = null): Int {
+            val node = if (root !== null) root else ViewNode.newInstance().also {
+                // Fill the nodes first.
+                val structure = ViewNodeBuilder.newInstance(
+                        AssistStructure(), it, /* async */ false) as ViewStructure
+                mainSession.textInput.onProvideAutofillVirtualStructure(structure, 0)
+            }
+            return (if (cond(node)) 1 else 0) +
+                    (if (node.childCount > 0) (0 until node.childCount).sumBy {
+                        countAutoFillNodes(cond, node.getChildAt(it)) } else 0)
+        }
+
+        // Wait for the accessibility nodes to populate.
+        mainSession.loadTestPath(FORMS_HTML_PATH)
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 4)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED,
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Initial auto-fill count should match",
+                   countAutoFillNodes(), equalTo(14))
+
+        // Now wait for the nodes to clear.
+        mainSession.loadTestPath(HELLO_HTML_PATH)
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 1)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be canceling auto-fill",
+                           notification,
+                           equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_CANCELED))
+                assertThat("ID should be valid", virtualId, equalTo(View.NO_ID))
+            }
+        })
+        assertThat("Should not have auto-fill fields",
+                   countAutoFillNodes(), equalTo(0))
+
+        // Now wait for the nodes to reappear.
+        mainSession.waitForPageStop()
+        mainSession.goBack()
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 4)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED,
+                        GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        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 : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 1)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be entering auto-fill view",
+                           notification,
+                           equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        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.width > 0 && node.height > 0 }),
+                   greaterThanOrEqualTo(6))
+
+        mainSession.evaluateJS("$('#pass1').blur()")
+        sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
+            @AssertCalled(count = 1)
+            override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
+                assertThat("Should be exiting auto-fill view",
+                           notification,
+                           equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
+                assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
+            }
+        })
+        assertThat("Should not have focused field",
+                   countAutoFillNodes({ it.isFocused }), equalTo(0))
+    }
 }