summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutofillDelegateTest.kt
diff options
context:
space:
mode:
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.kt715
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))
+ }
+ })
+ }
+}