diff options
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt')
-rw-r--r-- | mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt | 715 |
1 files changed, 715 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt new file mode 100644 index 0000000000..f1adc7bf1e --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt @@ -0,0 +1,715 @@ +/* -*- 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.graphics.Rect +import android.util.SparseArray +import android.view.KeyEvent +import android.view.View +import androidx.test.filters.MediumTest +import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports +import org.junit.Assume.assumeThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mozilla.geckoview.Autofill +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoSession.TextInputDelegate +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.* // ktlint-disable no-wildcard-imports + +@RunWith(Parameterized::class) +@MediumTest +class AutofillDelegateTest : BaseSessionTest() { + + companion object { + @get:Parameterized.Parameters(name = "{0}") + @JvmStatic + val parameters: List<Array<out Any>> = listOf( + arrayOf("#inProcess"), + arrayOf("#oop"), + ) + } + + @field:Parameterized.Parameter(0) + @JvmField + var iframe: String = "" + + // Whether the iframe is loaded in-process (i.e. with the same origin as the + // outer html page) or out-of-process. + private val pageUrl by lazy { + when (iframe) { + "#inProcess" -> "http://example.org/tests/junit/forms_xorigin.html" + "#oop" -> createTestUrl(FORMS_XORIGIN_HTML_PATH) + else -> throw IllegalStateException() + } + } + + @Test fun autofillCommit() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "signon.rememberSignons" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + mainSession.loadUri(pageUrl) + // Wait for the auto-fill nodes to populate. + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + // We expect to get a call to onSessionStart and many calls to onNodeAdd depending + // on timing. + @AssertCalled(count = 1) + override fun onSessionStart(session: GeckoSession) {} + + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + // Assign node values. + mainSession.evaluateJS("document.querySelector('#user1').value = 'user1x'") + mainSession.evaluateJS("document.querySelector('#pass1').value = 'pass1x'") + mainSession.evaluateJS("document.querySelector('#email1').value = 'e@mail.com'") + mainSession.evaluateJS("document.querySelector('#number1').value = '1'") + + // Submit the session. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(order = [1, 2, 3, 4]) + override fun onNodeUpdate( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + } + + @AssertCalled(order = [5]) + override fun onSessionCommit( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + val autofillSession = mainSession.autofillSession + assertThat( + "Values should match", + countAutofillNodes({ + autofillSession.dataFor(it).value == "user1x" + }), + equalTo(1), + ) + assertThat( + "Values should match", + countAutofillNodes({ + autofillSession.dataFor(it).value == "pass1x" + }), + equalTo(1), + ) + assertThat( + "Values should match", + countAutofillNodes({ + autofillSession.dataFor(it).value == "e@mail.com" + }), + equalTo(1), + ) + assertThat( + "Values should match", + countAutofillNodes({ + autofillSession.dataFor(it).value == "1" + }), + equalTo(1), + ) + } + }) + } + + @Test fun autofillCommitIdValue() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "signon.rememberSignons" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + mainSession.loadTestPath(FORMS_ID_VALUE_HTML_PATH) + // Wait for the auto-fill nodes to populate. + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + @AssertCalled(count = 1) + override fun onSessionStart(session: GeckoSession) {} + + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + // Assign node values. + mainSession.evaluateJS("document.querySelector('#value').value = 'pass1x'") + + // Submit the session. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(order = [1]) + override fun onNodeUpdate( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + } + + @AssertCalled(order = [2]) + override fun onSessionCommit( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat( + "Values should match", + countAutofillNodes({ + mainSession.autofillSession.dataFor(it).value == "pass1x" + }), + equalTo(1), + ) + } + }) + } + + @Test fun autofill() { + // Test parts of the Oreo auto-fill API; there is another autofill test in + // SessionAccessibility for a11y auto-fill support. + mainSession.loadUri(pageUrl) + // Wait for the auto-fill nodes to populate. + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + // We expect many call to onNodeAdd while loading the page + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + val autofills = mapOf( + "#user1" to "bar", + "#user2" to "bar", + "#pass1" to "baz", + "#pass2" to "baz", + "#email1" to "a@b.c", + "#number1" to "24", + "#tel1" to "42", + ) + + // Set up promises to monitor the values changing. + val promises = autofills.map { entry -> + // Repeat each test with both the top document and the iframe document. + mainSession.evaluatePromiseJS( + """ + window.getDataForAllFrames('${entry.key}', '${entry.value}') + """, + ) + } + + val autofillValues = SparseArray<CharSequence>() + + // Perform auto-fill and return number of auto-fills performed. + fun checkAutofillChild(child: Autofill.Node, domain: String) { + // Seal the node info instance so we can perform actions on it. + if (child.children.isNotEmpty()) { + for (c in child.children) { + checkAutofillChild(c!!, child.domain) + } + } + + if (child == mainSession.autofillSession.root) { + return + } + + assertThat( + "Should have HTML tag", + child.tag, + not(isEmptyOrNullString()), + ) + if (domain != "") { + assertThat( + "Web domain should match its parent.", + child.domain, + equalTo(domain), + ) + } + + if (child.inputType == Autofill.InputType.TEXT) { + assertThat("Input should be enabled", child.enabled, equalTo(true)) + assertThat( + "Input should be focusable", + child.focusable, + equalTo(true), + ) + + assertThat("Should have HTML tag", child.tag, equalTo("input")) + assertThat("Should have ID attribute", child.attributes.get("id"), not(isEmptyOrNullString())) + } + + val childId = mainSession.autofillSession.dataFor(child).id + autofillValues.append( + childId, + when (child.inputType) { + Autofill.InputType.NUMBER -> "24" + Autofill.InputType.PHONE -> "42" + Autofill.InputType.TEXT -> when (child.hint) { + Autofill.Hint.PASSWORD -> "baz" + Autofill.Hint.EMAIL_ADDRESS -> "a@b.c" + else -> "bar" + } + else -> "bar" + }, + ) + } + + val nodes = mainSession.autofillSession.root + checkAutofillChild(nodes, "") + + mainSession.autofillSession.autofill(autofillValues) + + // Wait on the promises and check for correct values. + for (values in promises.map { it.value.asJsonArray() }) { + for (i in 0 until values.length()) { + val (key, actual, expected, eventInterface) = values.get(i).asJSList<String>() + + assertThat("Auto-filled value must match ($key)", actual, equalTo(expected)) + assertThat( + "input event should be dispatched with InputEvent interface", + eventInterface, + equalTo("InputEvent"), + ) + } + } + } + + @Test fun autofillUnknownValue() { + // Test parts of the Oreo auto-fill API; there is another autofill test in + // SessionAccessibility for a11y auto-fill support. + mainSession.loadUri(pageUrl) + // Wait for the auto-fill nodes to populate. + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + val autofillValues = SparseArray<CharSequence>() + autofillValues.append(-1, "lobster") + mainSession.autofillSession.autofill(autofillValues) + } + + private fun countAutofillNodes( + cond: (Autofill.Node) -> Boolean = + { it.inputType != Autofill.InputType.NONE }, + root: Autofill.Node? = null, + ): Int { + val node = if (root !== null) root else mainSession.autofillSession.root + return (if (cond(node)) 1 else 0) + + node.children.sumOf { + countAutofillNodes(cond, it) + } + } + + @WithDisplay(width = 100, height = 100) + @Test + fun autofillNavigation() { + // Wait for the accessibility nodes to populate. + mainSession.loadUri(pageUrl) + + sessionRule.waitUntilCalled(object : + Autofill.Delegate, + ShouldContinue, + GeckoSession.ProgressDelegate { + var nodeCount = 0 + + // Continue waiting util we get all 16 nodes + override fun shouldContinue(): Boolean = nodeCount < 16 + + @AssertCalled(count = 1) + override fun onSessionStart(session: GeckoSession) {} + + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("Node should be valid", node, notNullValue()) + nodeCount = countAutofillNodes() + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + assertThat( + "Initial auto-fill count should match", + countAutofillNodes(), + equalTo(16), + ) + + // Now wait for the nodes to clear. + mainSession.loadTestPath(HELLO_HTML_PATH) + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + @AssertCalled(count = 1) + override fun onSessionCancel(session: GeckoSession) {} + + @AssertCalled + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + assertThat( + "Should not have auto-fill fields", + countAutofillNodes(), + equalTo(0), + ) + + mainSession.goBack() + sessionRule.waitUntilCalled(object : + Autofill.Delegate, + GeckoSession.ProgressDelegate, + ShouldContinue { + var nodeCount = 0 + override fun shouldContinue(): Boolean = nodeCount < 16 + + @AssertCalled(count = 1) + override fun onSessionStart(session: GeckoSession) {} + + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("Node should be valid", node, notNullValue()) + nodeCount = countAutofillNodes() + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + assertThat( + "Should have auto-fill fields again", + countAutofillNodes(), + equalTo(16), + ) + + var focused = mainSession.autofillSession.focused + assertThat( + "Should not have focused field", + countAutofillNodes({ it == focused }), + equalTo(0), + ) + + mainSession.evaluateJS("document.querySelector('#pass2').focus()") + + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(count = 1) + override fun onNodeFocus( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("ID should be valid", node, notNullValue()) + } + }) + + focused = mainSession.autofillSession.focused + assertThat( + "Should have one focused field", + countAutofillNodes({ it == focused }), + equalTo(1), + ) + // The focused field, its siblings, its parent, and the root node should + // be visible. + // Hidden elements are ignored. + // TODO: Is this actually correct? Should the whole focused branch be + // visible or just the nodes as described above? + assertThat( + "Should have nine visible nodes", + countAutofillNodes({ node -> mainSession.autofillSession.isVisible(node) }), + equalTo(8), + ) + + mainSession.evaluateJS("document.querySelector('#pass2').blur()") + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(count = 1) + override fun onNodeBlur( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("ID should be valid", node, notNullValue()) + } + }) + + focused = mainSession.autofillSession.focused + assertThat( + "Should not have focused field", + countAutofillNodes({ it == focused }), + equalTo(0), + ) + } + + @WithDisplay(height = 100, width = 100) + @Test + fun autofillUserpass() { + mainSession.loadTestPath(FORMS2_HTML_PATH) + // Wait for the auto-fill nodes to populate. + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + @AssertCalled(count = 1) + override fun onSessionStart(session: GeckoSession) {} + + @AssertCalled(count = 1) + override fun onNodeFocus( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + // Perform auto-fill and return number of auto-fills performed. + fun checkAutofillChild(child: Autofill.Node): Int { + var sum = 0 + // Seal the node info instance so we can perform actions on it. + for (c in child.children) { + sum += checkAutofillChild(c!!) + } + + if (child.hint == Autofill.Hint.NONE) { + return sum + } + + val childId = mainSession.autofillSession.dataFor(child).id + assertThat("ID should be valid", childId, not(equalTo(View.NO_ID))) + assertThat("Should have HTML tag", child.tag, equalTo("input")) + + return sum + 1 + } + + val root = mainSession.autofillSession.root + + // form and iframe have each have 2 nodes with hints. + assertThat( + "autofill hint count", + checkAutofillChild(root), + equalTo(4), + ) + } + + @WithDisplay(width = 100, height = 100) + @Test + fun autofillActiveChange() { + // We should blur the active autofill node if the session is set + // inactive. Likewise, we should focus a node once we return. + mainSession.loadUri(pageUrl) + // Wait for the auto-fill nodes to populate. + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + // 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 = 1) + override fun onSessionStart(session: GeckoSession) {} + + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + mainSession.evaluateJS("document.querySelector('#pass2').focus()") + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(count = 1) + override fun onNodeFocus( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("ID should be valid", node, notNullValue()) + } + }) + + var focused = mainSession.autofillSession.focused + assertThat( + "Should have one focused field", + countAutofillNodes({ it == focused }), + equalTo(1), + ) + + // Make sure we get NODE_BLURRED when inactive + mainSession.setActive(false) + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(count = 1) + override fun onNodeBlur( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("ID should be valid", node, notNullValue()) + } + }) + + // Make sure we get NODE_FOCUSED when active once again + mainSession.setActive(true) + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(count = 1) + override fun onNodeFocus( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("ID should be valid", node, notNullValue()) + } + }) + + focused = mainSession.autofillSession.focused + assertThat( + "Should have one focused field", + countAutofillNodes({ focused == it }), + equalTo(1), + ) + } + + @WithDisplay(width = 100, height = 100) + @Test + fun autofillAutocompleteAttribute() { + mainSession.loadTestPath(FORMS_AUTOCOMPLETE_HTML_PATH) + sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate { + @AssertCalled(count = -1) + override fun onNodeAdd( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) {} + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) {} + }) + + fun checkAutofillChild(child: Autofill.Node): Int { + var sum = 0 + for (c in child.children) { + sum += checkAutofillChild(c!!) + } + if (child.hint == Autofill.Hint.NONE) { + return sum + } + assertThat("Should have HTML tag", child.tag, equalTo("input")) + return sum + 1 + } + + val root = mainSession.autofillSession.root + // Each page has 3 nodes for autofill. + assertThat( + "autofill hint count", + checkAutofillChild(root), + equalTo(6), + ) + } + + @WithDisplay(width = 100, height = 100) + @Test + fun autofillWaitForKeyboard() { + // Wait for the accessibility nodes to populate. + mainSession.loadUri(pageUrl) + mainSession.waitForPageStop() + + mainSession.pressKey(KeyEvent.KEYCODE_CTRL_LEFT) + mainSession.evaluateJS("document.querySelector('#pass2').focus()") + + sessionRule.waitUntilCalled(object : Autofill.Delegate, TextInputDelegate { + @AssertCalled(order = [2]) + override fun onNodeFocus( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("ID should be valid", node, notNullValue()) + } + + @AssertCalled(order = [1]) + override fun showSoftInput(session: GeckoSession) {} + }) + } + + @WithDisplay(width = 300, height = 1000) + @Test + fun autofillIframe() { + // No way to click in x-origin frame. + assumeThat("Not in x-origin", iframe, not(equalTo("#oop"))) + + // Wait for the accessibility nodes to populate. + mainSession.loadUri(pageUrl) + mainSession.waitForPageStop() + + // Get non-iframe position of input element + var screenRect = Rect() + mainSession.evaluateJS("document.querySelector('#pass2').focus()") + + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(count = 1) + override fun onNodeFocus( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + screenRect = node.screenRect + } + }) + + mainSession.evaluateJS("document.querySelector('iframe').contentDocument.querySelector('#pass2').focus()") + + sessionRule.waitUntilCalled(object : Autofill.Delegate { + @AssertCalled(count = 1) + override fun onNodeFocus( + session: GeckoSession, + node: Autofill.Node, + data: Autofill.NodeData, + ) { + assertThat("ID should be valid", node, notNullValue()) + // iframe's input element should consider iframe's offset. 200 is enough offset. + assertThat("position is valid", node.getScreenRect().top, greaterThanOrEqualTo(screenRect.top + 200)) + } + }) + } +} |