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
This commit is contained in:
Jim Chen 2018-08-20 22:28:21 -04:00
parent 7ead74b685
commit da23d1931b
3 changed files with 190 additions and 0 deletions

View File

@ -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>

View File

@ -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<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))
}
}

View File

@ -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"