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
This commit is contained in:
Jim Chen 2018-08-23 17:03:35 -04:00
parent ae15c6ad5c
commit 28a1a1d5dd

View File

@ -4,6 +4,8 @@
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
@ -16,7 +18,13 @@ 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
@ -184,4 +192,227 @@ class ContentDelegateTest : BaseSessionTest() {
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))
}
}