Bug 1486571 - 8. Add tests for Oreo auto-fill frontend; r=snorp, a=RyanVM GECKOVIEW_62_RELBRANCH
authorJim Chen <nchen@mozilla.com>
Tue, 28 Aug 2018 13:18:00 -0400
branchGECKOVIEW_62_RELBRANCH
changeset 481095 d2939e628138e89887f1120b5841a94af7f89a9b
parent 481094 0bc8dc4778efd06f54066af0a409d449c0780163
child 481105 19d42cf29b7c342e9dc85f8c4c02ab87a18f189d
push id1765
push userryanvm@gmail.com
push dateWed, 29 Aug 2018 13:38:35 +0000
treeherdermozilla-release@d2939e628138 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, RyanVM
bugs1486571
milestone62.0
Bug 1486571 - 8. Add tests for Oreo auto-fill frontend; r=snorp, a=RyanVM 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,25 +1,33 @@
 /* -*- 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 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
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class ContentDelegateTest : BaseSessionTest() {
@@ -150,9 +158,232 @@ class ContentDelegateTest : BaseSessionT
         assertThat("'name' field should match",
                 mainSession.evaluateJS("$('#name').value").toString(),
                 equalTo("the name"))
 
         assertThat("Scroll position should match",
                 mainSession.evaluateJS("window.scrollY") as Double,
                 closeTo(100.0, .5))
     }
+
+    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))
+    }
 }