summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt')
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt1407
1 files changed, 1407 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt
new file mode 100644
index 0000000000..99cc30cd38
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt
@@ -0,0 +1,1407 @@
+/* -*- 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.content.ClipDescription
+import android.net.Uri
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import android.text.InputType
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.ExtractedTextRequest
+import android.view.inputmethod.InputConnection
+import android.view.inputmethod.InputContentInfo
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports
+import org.junit.Assume.assumeThat
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.mozilla.geckoview.GeckoResult
+import org.mozilla.geckoview.GeckoSession
+import org.mozilla.geckoview.GeckoSession.TextInputDelegate
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
+
+@MediumTest
+@RunWith(Parameterized::class)
+class TextInputDelegateTest : BaseSessionTest() {
+ // "parameters" needs to be a static field, so it has to be in a companion object.
+ companion object {
+ @get:Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ val parameters: List<Array<out Any>> = listOf(
+ arrayOf("#input"),
+ arrayOf("#textarea"),
+ arrayOf("#contenteditable"),
+ arrayOf("#designmode"),
+ )
+ }
+
+ @field:Parameter(0)
+ @JvmField
+ var id: String = ""
+
+ private var textContent: String
+ get() = when (id) {
+ "#contenteditable" -> mainSession.evaluateJS("document.querySelector('$id').textContent")
+ "#designmode" -> mainSession.evaluateJS("document.querySelector('$id').contentDocument.body.textContent")
+ else -> mainSession.evaluateJS("document.querySelector('$id').value")
+ } as String
+ set(content) {
+ when (id) {
+ "#contenteditable" -> mainSession.evaluateJS("document.querySelector('$id').textContent = '$content'")
+ "#designmode" -> mainSession.evaluateJS(
+ "document.querySelector('$id').contentDocument.body.textContent = '$content'",
+ )
+ else -> mainSession.evaluateJS("document.querySelector('$id').value = '$content'")
+ }
+ }
+
+ private var selectionOffsets: Pair<Int, Int>
+ get() = when (id) {
+ "#contenteditable" -> mainSession.evaluateJS(
+ """[
+ document.getSelection().anchorOffset,
+ document.getSelection().focusOffset]""",
+ )
+ "#designmode" -> mainSession.evaluateJS(
+ """(function() {
+ var sel = document.querySelector('$id').contentDocument.getSelection();
+ var text = document.querySelector('$id').contentDocument.body.firstChild;
+ return [sel.anchorOffset, sel.focusOffset];
+ })()""",
+ )
+ else -> mainSession.evaluateJS(
+ """(document.querySelector('$id').selectionDirection !== 'backward'
+ ? [ document.querySelector('$id').selectionStart, document.querySelector('$id').selectionEnd ]
+ : [ document.querySelector('$id').selectionEnd, document.querySelector('$id').selectionStart ])""",
+ )
+ }.asJsonArray().let {
+ Pair(it.getInt(0), it.getInt(1))
+ }
+ set(offsets) {
+ var (start, end) = offsets
+ when (id) {
+ "#contenteditable" -> mainSession.evaluateJS(
+ """(function() {
+ let selection = document.getSelection();
+ let text = document.querySelector('$id').firstChild;
+ if (text) {
+ selection.setBaseAndExtent(text, $start, text, $end)
+ } else {
+ selection.collapse(document.querySelector('$id'), 0);
+ }
+ })()""",
+ )
+ "#designmode" -> mainSession.evaluateJS(
+ """(function() {
+ let selection = document.querySelector('$id').contentDocument.getSelection();
+ let text = document.querySelector('$id').contentDocument.body.firstChild;
+ if (text) {
+ selection.setBaseAndExtent(text, $start, text, $end)
+ } else {
+ selection.collapse(document.querySelector('$id').contentDocument.body, 0);
+ }
+ })()""",
+ )
+ else -> mainSession.evaluateJS("document.querySelector('$id').setSelectionRange($start, $end)")
+ }
+ }
+
+ private fun processParentEvents() {
+ sessionRule.requestedLocales
+ }
+
+ private fun processChildEvents() {
+ mainSession.waitForJS("new Promise(r => requestAnimationFrame(r))")
+ }
+
+ private fun setComposingText(ic: InputConnection, text: CharSequence, newCursorPosition: Int) {
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('compositionupdate', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('compositionupdate', r, { once: true }))"
+ },
+ )
+ ic.setComposingText(text, newCursorPosition)
+ promise.value
+ }
+
+ private fun finishComposingText(ic: InputConnection) {
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('compositionend', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('compositionend', r, { once: true }))"
+ },
+ )
+ ic.finishComposingText()
+ promise.value
+ }
+
+ private fun commitText(ic: InputConnection, text: CharSequence, newCursorPosition: Int) {
+ if (text == "") {
+ // No composition event is fired
+ ic.commitText(text, newCursorPosition)
+ return
+ }
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('compositionend', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('compositionend', r, { once: true }))"
+ },
+ )
+ ic.commitText(text, newCursorPosition)
+ promise.value
+ }
+
+ private fun deleteSurroundingText(ic: InputConnection, before: Int, after: Int) {
+ // deleteSurroundingText might fire multiple events.
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('input', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('input', r, { once: true }))"
+ },
+ )
+ ic.deleteSurroundingText(before, after)
+ if (before != 0 || after != 0) {
+ promise.value
+ }
+ // XXX: No way to wait for all events.
+ processChildEvents()
+ }
+
+ private fun setSelection(ic: InputConnection, start: Int, end: Int) {
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('selectionchange', r, { once: true }))"
+ "#contenteditable" -> "new Promise(r => document.addEventListener('selectionchange', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('selectionchange', r, { once: true }))"
+ },
+ )
+ ic.setSelection(start, end)
+ promise.value
+ }
+
+ private fun pressKey(ic: InputConnection, keyCode: Int) {
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('keyup', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('keyup', r, { once: true }))"
+ },
+ )
+ val time = SystemClock.uptimeMillis()
+ val keyEvent = KeyEvent(time, time, KeyEvent.ACTION_DOWN, keyCode, 0)
+ ic.sendKeyEvent(keyEvent)
+ ic.sendKeyEvent(KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP))
+ promise.value
+ }
+
+ private fun syncShadowText(ic: InputConnection) {
+ // Workaround for sync shadow text
+ ic.beginBatchEdit()
+ ic.endBatchEdit()
+ }
+
+ @Test fun restartInput() {
+ // Check that restartInput is called on focus and blur.
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ mainSession.evaluateJS("document.querySelector('$id').focus()")
+ mainSession.waitUntilCalled(object : TextInputDelegate {
+ @AssertCalled(count = 1)
+ override fun restartInput(session: GeckoSession, reason: Int) {
+ assertThat(
+ "Reason should be correct",
+ reason,
+ equalTo(GeckoSession.TextInputDelegate.RESTART_REASON_FOCUS),
+ )
+ }
+ })
+
+ mainSession.evaluateJS("document.querySelector('$id').blur()")
+ mainSession.waitUntilCalled(object : TextInputDelegate {
+ @AssertCalled(count = 1)
+ override fun restartInput(session: GeckoSession, reason: Int) {
+ assertThat(
+ "Reason should be correct",
+ reason,
+ equalTo(GeckoSession.TextInputDelegate.RESTART_REASON_BLUR),
+ )
+ }
+
+ // Also check that showSoftInput/hideSoftInput are not called before a user action.
+ @AssertCalled(count = 0)
+ override fun showSoftInput(session: GeckoSession) {
+ }
+
+ @AssertCalled(count = 0)
+ override fun hideSoftInput(session: GeckoSession) {
+ }
+ })
+ }
+
+ @Test fun restartInput_temporaryFocus() {
+ // Our user action trick doesn't work for design-mode, so we can't test that here.
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+ // Disable for frequent failures Bug 1542525
+ assumeThat(sessionRule.env.isDebugBuild, equalTo(false))
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ // Focus the input once here and once below, but we should only get a
+ // single restartInput or showSoftInput call for the second focus.
+ mainSession.evaluateJS("document.querySelector('$id').focus(); document.querySelector('$id').blur()")
+
+ // Simulate a user action so we're allowed to show/hide the keyboard.
+ mainSession.pressKey(KeyEvent.KEYCODE_CTRL_LEFT)
+ mainSession.evaluateJS("document.querySelector('$id').focus()")
+
+ mainSession.waitUntilCalled(object : TextInputDelegate {
+ @AssertCalled(count = 1, order = [1])
+ override fun restartInput(session: GeckoSession, reason: Int) {
+ assertThat(
+ "Reason should be correct",
+ reason,
+ equalTo(GeckoSession.TextInputDelegate.RESTART_REASON_FOCUS),
+ )
+ }
+
+ @AssertCalled(count = 1, order = [2])
+ override fun showSoftInput(session: GeckoSession) {
+ }
+
+ @AssertCalled(count = 0)
+ override fun hideSoftInput(session: GeckoSession) {
+ }
+ })
+ }
+
+ @Test fun restartInput_temporaryBlur() {
+ // Our user action trick doesn't work for design-mode, so we can't test that here.
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ // Simulate a user action so we're allowed to show/hide the keyboard.
+ mainSession.pressKey(KeyEvent.KEYCODE_CTRL_LEFT)
+ mainSession.evaluateJS("document.querySelector('$id').focus()")
+ mainSession.waitUntilCalled(
+ GeckoSession.TextInputDelegate::class,
+ "restartInput",
+ "showSoftInput",
+ )
+
+ // We should get a pair of restartInput calls for the blur/focus,
+ // but only one showSoftInput call and no hideSoftInput call.
+ mainSession.evaluateJS("document.querySelector('$id').blur(); document.querySelector('$id').focus()")
+
+ mainSession.waitUntilCalled(object : TextInputDelegate {
+ @AssertCalled(count = 2, order = [1])
+ override fun restartInput(session: GeckoSession, reason: Int) {
+ assertThat(
+ "Reason should be correct",
+ reason,
+ equalTo(
+ forEachCall(
+ GeckoSession.TextInputDelegate.RESTART_REASON_BLUR,
+ GeckoSession.TextInputDelegate.RESTART_REASON_FOCUS,
+ ),
+ ),
+ )
+ }
+
+ @AssertCalled(count = 1, order = [2])
+ override fun showSoftInput(session: GeckoSession) {
+ }
+
+ @AssertCalled(count = 0)
+ override fun hideSoftInput(session: GeckoSession) {
+ }
+ })
+ }
+
+ @Test fun showHideSoftInput() {
+ // Our user action trick doesn't work for design-mode, so we can't test that here.
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ // Simulate a user action so we're allowed to show/hide the keyboard.
+ mainSession.pressKey(KeyEvent.KEYCODE_CTRL_LEFT)
+
+ mainSession.evaluateJS("document.querySelector('$id').focus()")
+ mainSession.waitUntilCalled(object : TextInputDelegate {
+ @AssertCalled(count = 1, order = [1])
+ override fun restartInput(session: GeckoSession, reason: Int) {
+ }
+
+ @AssertCalled(count = 1, order = [2])
+ override fun showSoftInput(session: GeckoSession) {
+ }
+
+ @AssertCalled(count = 0)
+ override fun hideSoftInput(session: GeckoSession) {
+ }
+ })
+
+ mainSession.evaluateJS("document.querySelector('$id').blur()")
+ mainSession.waitUntilCalled(object : TextInputDelegate {
+ @AssertCalled(count = 1, order = [1])
+ override fun restartInput(session: GeckoSession, reason: Int) {
+ }
+
+ @AssertCalled(count = 0)
+ override fun showSoftInput(session: GeckoSession) {
+ }
+
+ @AssertCalled(count = 1, order = [2])
+ override fun hideSoftInput(session: GeckoSession) {
+ }
+ })
+ }
+
+ private fun getText(ic: InputConnection) =
+ ic.getExtractedText(ExtractedTextRequest(), 0).text.toString()
+
+ private fun assertText(message: String, actual: String, expected: String) =
+ // In an HTML editor, Gecko may insert an additional element that show up as a
+ // return character at the end. Deal with that here.
+ assertThat(message, actual.trimEnd('\n'), equalTo(expected))
+
+ private fun assertText(
+ message: String,
+ ic: InputConnection,
+ expected: String,
+ checkGecko: Boolean = true,
+ ) {
+ processChildEvents()
+ processParentEvents()
+
+ if (checkGecko) {
+ assertText(message, textContent, expected)
+ }
+ assertText(message, getText(ic), expected)
+ }
+
+ private fun assertSelection(
+ message: String,
+ ic: InputConnection,
+ start: Int,
+ end: Int,
+ checkGecko: Boolean = true,
+ ) {
+ processChildEvents()
+ processParentEvents()
+
+ if (checkGecko) {
+ assertThat(message, selectionOffsets, equalTo(Pair(start, end)))
+ }
+
+ val extracted = ic.getExtractedText(ExtractedTextRequest(), 0)
+ assertThat(message, extracted.selectionStart, equalTo(start))
+ assertThat(message, extracted.selectionEnd, equalTo(end))
+ }
+
+ private fun assertSelectionAt(
+ message: String,
+ ic: InputConnection,
+ value: Int,
+ checkGecko: Boolean = true,
+ ) =
+ assertSelection(message, ic, value, value, checkGecko)
+
+ private fun assertTextAndSelection(
+ message: String,
+ ic: InputConnection,
+ expected: String,
+ start: Int,
+ end: Int,
+ checkGecko: Boolean = true,
+ ) {
+ processChildEvents()
+ processParentEvents()
+
+ if (checkGecko) {
+ assertText(message, textContent, expected)
+ assertThat(message, selectionOffsets, equalTo(Pair(start, end)))
+ }
+
+ val extracted = ic.getExtractedText(ExtractedTextRequest(), 0)
+ assertText(message, extracted.text.toString(), expected)
+ assertThat(message, extracted.selectionStart, equalTo(start))
+ assertThat(message, extracted.selectionEnd, equalTo(end))
+ }
+
+ private fun assertTextAndSelectionAt(
+ message: String,
+ ic: InputConnection,
+ expected: String,
+ value: Int,
+ checkGecko: Boolean = true,
+ ) =
+ assertTextAndSelection(message, ic, expected, value, value, checkGecko)
+
+ private fun setupContent(content: String) {
+ sessionRule.setPrefsUntilTestEnd(
+ mapOf(
+ "dom.select_events.textcontrols.enabled" to true,
+ ),
+ )
+
+ mainSession.textInput.view = View(InstrumentationRegistry.getInstrumentation().targetContext)
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ textContent = content
+ mainSession.evaluateJS("document.querySelector('$id').focus()")
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+ }
+
+ // Test setSelection
+ @Ignore
+ // Disable for frequent timeout for selection event.
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_setSelection() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ // TODO:
+ // onselectionchange won't be fired if caret is last. But commitText
+ // can set text and selection well (Bug 1360388).
+ commitText(ic, "foo", 1) // Selection at end of new text
+ assertTextAndSelectionAt("Can commit text", ic, "foo", 3)
+
+ setSelection(ic, 0, 3)
+ assertSelection("Can set selection to range", ic, 0, 3)
+ // No selection change event is fired
+ ic.setSelection(-3, 6)
+ // Test both forms of assert
+ assertTextAndSelection(
+ "Can handle invalid range",
+ ic,
+ "foo",
+ 0,
+ 3,
+ )
+ setSelection(ic, 3, 3)
+ assertSelectionAt("Can collapse selection", ic, 3)
+ // No selection change event is fired
+ ic.setSelection(4, 4)
+ assertTextAndSelectionAt("Can handle invalid cursor", ic, "foo", 3)
+ }
+
+ // Test commitText
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_commitText() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ commitText(ic, "foo", 1) // Selection at end of new text
+ assertTextAndSelectionAt("Can commit empty text", ic, "foo", 3)
+
+ commitText(ic, "", 10) // Selection past end of new text
+ assertTextAndSelectionAt("Can commit empty text", ic, "foo", 3)
+ commitText(ic, "bar", 1) // Selection at end of new text
+ assertTextAndSelectionAt(
+ "Can commit text (select after)",
+ ic,
+ "foobar",
+ 6,
+ )
+ commitText(ic, "foo", -1) // Selection at start of new text
+ assertTextAndSelectionAt(
+ "Can commit text (select before)",
+ ic,
+ "foobarfoo",
+ 5, /* checkGecko */
+ false,
+ )
+ }
+
+ // Test deleteSurroundingText
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_deleteSurroundingText() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+
+ commitText(ic, "foobarfoo", 1)
+ assertTextAndSelectionAt("Set initial text and selection", ic, "foobarfoo", 9)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ assertSelection("Can set selection to range", ic, 5, 5)
+
+ deleteSurroundingText(ic, 1, 0)
+ assertTextAndSelectionAt(
+ "Can delete text before",
+ ic,
+ "foobrfoo",
+ 4,
+ )
+ deleteSurroundingText(ic, 1, 1)
+ assertTextAndSelectionAt(
+ "Can delete text before/after",
+ ic,
+ "foofoo",
+ 3,
+ )
+ deleteSurroundingText(ic, 0, 10)
+ assertTextAndSelectionAt("Can delete text after", ic, "foo", 3)
+ deleteSurroundingText(ic, 0, 0)
+ assertTextAndSelectionAt("Can delete empty text", ic, "foo", 3)
+ }
+
+ // Test setComposingText
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_setComposingText() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ commitText(ic, "foo", 1) // Selection at end of new text
+ assertTextAndSelectionAt("Can commit text", ic, "foo", 3)
+
+ setComposingText(ic, "foo", 1)
+ assertTextAndSelectionAt("Can start composition", ic, "foofoo", 6)
+ setComposingText(ic, "", 1)
+ assertTextAndSelectionAt("Can set empty composition", ic, "foo", 3)
+ setComposingText(ic, "bar", 1)
+ assertTextAndSelectionAt("Can update composition", ic, "foobar", 6)
+
+ // Test finishComposingText
+ finishComposingText(ic)
+ assertTextAndSelectionAt("Can finish composition", ic, "foobar", 6)
+ }
+
+ // Test setComposingRegion
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_setComposingRegion() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ commitText(ic, "foobar", 1) // Selection at end of new text
+ assertTextAndSelectionAt("Can commit text", ic, "foobar", 6)
+
+ ic.setComposingRegion(0, 3)
+ assertTextAndSelectionAt("Can set composing region", ic, "foobar", 6)
+
+ setComposingText(ic, "far", 1)
+ assertTextAndSelectionAt(
+ "Can set composing region text",
+ ic,
+ "farbar",
+ 3,
+ )
+
+ ic.setComposingRegion(1, 4)
+ assertTextAndSelectionAt(
+ "Can set existing composing region",
+ ic,
+ "farbar",
+ 3,
+ )
+
+ setComposingText(ic, "rab", 3)
+ assertTextAndSelectionAt(
+ "Can set new composing region text",
+ ic,
+ "frabar",
+ 6, /* checkGecko */
+ false,
+ )
+
+ finishComposingText(ic)
+ }
+
+ // Test getTextBefore/AfterCursor
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_getTextBeforeAfterCursor() {
+ setupContent("foobar")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "foobar")
+
+ setSelection(ic, 3, 3)
+ assertSelection("Can set selection to range", ic, 3, 3)
+
+ // Test getTextBeforeCursor
+ assertThat(
+ "Can retrieve text before cursor",
+ "foo",
+ equalTo(ic.getTextBeforeCursor(3, 0)),
+ )
+
+ // Test getTextAfterCursor
+ assertThat(
+ "Can retrieve text after cursor",
+ "bar",
+ equalTo(ic.getTextAfterCursor(3, 0)),
+ )
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_selectionByArrowKey() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Set initial text", ic, "")
+
+ commitText(ic, "foo", 1) // Selection at end of new text
+ assertTextAndSelectionAt("Commit foo text", ic, "foo", 3)
+
+ // backward selection test
+ var time = SystemClock.uptimeMillis()
+ var shiftKey = KeyEvent(
+ time,
+ time,
+ KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SHIFT_LEFT,
+ 0,
+ )
+ ic.sendKeyEvent(shiftKey)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP))
+ // No way to get notification for selection on Java side. So sync shadow text
+ syncShadowText(ic)
+ assertSelection("Set backward select using key event", ic, 3, 0)
+
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ // No way to get notification for selection on Java side. So sync shadow text
+ syncShadowText(ic)
+ assertSelectionAt("Reset selection using key event", ic, 0)
+
+ // forward selection test
+ time = SystemClock.uptimeMillis()
+ shiftKey = KeyEvent(
+ time,
+ time,
+ KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SHIFT_LEFT,
+ 0,
+ )
+ ic.sendKeyEvent(shiftKey)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP))
+ // No way to get notification for selection on Java side. So sync shadow text
+ syncShadowText(ic)
+ assertSelection("Set forward select using key event", ic, 0, 3)
+ }
+
+ // Test sendKeyEvent
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_sendKeyEvent() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ commitText(ic, "frabar", 1) // Selection at end of new text
+ assertTextAndSelectionAt("Can commit text", ic, "frabar", 6)
+
+ val time = SystemClock.uptimeMillis()
+ val shiftKey = KeyEvent(
+ time,
+ time,
+ KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SHIFT_LEFT,
+ 0,
+ )
+
+ // Wait for selection change
+ var promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('selectionchange', r, { once: true }))"
+ "#contenteditable" -> "new Promise(r => document.addEventListener('selectionchange', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('selectionchange', r, { once: true }))"
+ },
+ )
+
+ ic.sendKeyEvent(shiftKey)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP))
+ promise.value
+
+ // TODO(m_kato)
+ // Since geckoview-junit doesn't attach View, there is no way to wait for correct selection data.
+ // So Sync shadow text to avoid failures.
+ syncShadowText(ic)
+ assertTextAndSelection(
+ "Can select using key event",
+ ic,
+ "frabar",
+ 6,
+ 5,
+ )
+
+ promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('input', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('input', r, { once: true }))"
+ },
+ )
+
+ pressKey(ic, KeyEvent.KEYCODE_T)
+ promise.value
+ assertText("Can type using event", ic, "frabat")
+ }
+
+ // Test for Multiple setComposingText with same string length.
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_multiple_setComposingText() {
+ setupContent("")
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+
+ // Don't wait composition event for this test.
+ ic.setComposingText("aaa", 1)
+ ic.setComposingText("aaa", 1)
+ ic.setComposingText("aab", 1)
+
+ finishComposingText(ic)
+ assertTextAndSelectionAt(
+ "Multiple setComposingText don't commit composition string",
+ ic,
+ "aab",
+ 3,
+ )
+ }
+
+ // Test for setting large text on text box.
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_largeText() {
+ val content = (1..102400).map {
+ ('a'..'z').random()
+ }.joinToString("")
+ setupContent(content)
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set large initial text", ic, content, /* checkGecko */ false)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N_MR1)
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_commitContent() {
+ if (id == "#input" || id == "#textarea") {
+ assertThat(
+ "This test is only for contenteditable or designmode",
+ true,
+ equalTo(true),
+ )
+ return
+ }
+
+ setupContent("")
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Set initial text", ic, "")
+
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> """
+ new Promise((resolve, reject) => document.querySelector('$id').contentDocument.addEventListener('input', e => {
+ if (e.inputType == 'insertFromPaste') {
+ resolve();
+ } else {
+ reject();
+ }
+ }, { once: true }))
+ """.trimIndent()
+ else -> """
+ new Promise((resolve, reject) => document.querySelector('$id').addEventListener('input', e => {
+ if (e.inputType == 'insertFromPaste') {
+ resolve();
+ } else {
+ reject();
+ }
+ }, { once: true }))
+ """.trimIndent()
+ },
+ )
+
+ // InputContentInfo requires content:// uri, so we have to set test data to custom content provider.
+ TestContentProvider.setTestData(this.getTestBytes("/assets/www/images/test.gif"), "image/gif")
+ val info = InputContentInfo(Uri.parse("content://org.mozilla.geckoview.test.provider/gif"), ClipDescription("test", arrayOf("image/gif")))
+ ic.commitContent(info, 0, null)
+ promise.value
+ assertThat("Input event is fired by inserting image", true, equalTo(true))
+ }
+
+ // Bug 1133802, duplication when setting the same composing text more than once.
+ @Ignore
+ // Disable for frequent failures.
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1133802() {
+ // TODO:
+ // Disable this test for frequent failures. We consider another way to
+ // wait/ignore event handling.
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ setComposingText(ic, "foo", 1)
+ assertTextAndSelectionAt("Can set the composing text", ic, "foo", 3)
+ // Setting same text doesn't fire compositionupdate
+ ic.setComposingText("foo", 1)
+ assertTextAndSelectionAt(
+ "Can set the same composing text",
+ ic,
+ "foo",
+ 3,
+ )
+ setComposingText(ic, "bar", 1)
+ assertTextAndSelectionAt(
+ "Can set different composing text",
+ ic,
+ "bar",
+ 3,
+ )
+ // Setting same text doesn't fire compositionupdate
+ ic.setComposingText("bar", 1)
+ assertTextAndSelectionAt(
+ "Can set the same composing text",
+ ic,
+ "bar",
+ 3,
+ )
+ // Setting same text doesn't fire compositionupdate
+ ic.setComposingText("bar", 1)
+ assertTextAndSelectionAt(
+ "Can set the same composing text again",
+ ic,
+ "bar",
+ 3,
+ )
+ finishComposingText(ic)
+ assertTextAndSelectionAt("Can finish composing text", ic, "bar", 3)
+ }
+
+ // Bug 1209465, cannot enter ideographic space character by itself (U+3000).
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1209465() {
+ // The ideographic space char may trigger font fallback; we don't want that to be async,
+ // as the resulting deferred reflow may confuse a following test.
+ sessionRule.setPrefsUntilTestEnd(mapOf("gfx.font_rendering.fallback.async" to false))
+
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ commitText(ic, "\u3000", 1)
+ assertTextAndSelectionAt(
+ "Can commit ideographic space",
+ ic,
+ "\u3000",
+ 1,
+ )
+ }
+
+ // Bug 1275371 - shift+backspace should not forward delete on Android.
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1275371() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ ic.beginBatchEdit()
+ commitText(ic, "foo", 1)
+ setSelection(ic, 1, 1)
+ ic.endBatchEdit()
+ assertTextAndSelectionAt("Can commit text", ic, "foo", 1)
+
+ val time = SystemClock.uptimeMillis()
+ val shiftKey = KeyEvent(
+ time,
+ time,
+ KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SHIFT_LEFT,
+ 0,
+ )
+ ic.sendKeyEvent(shiftKey)
+
+ // Wait for input change
+ val promise = mainSession.evaluatePromiseJS(
+ when (id) {
+ "#designmode" -> "new Promise(r => document.querySelector('$id').contentDocument.addEventListener('input', r, { once: true }))"
+ else -> "new Promise(r => document.querySelector('$id').addEventListener('input', r, { once: true }))"
+ },
+ )
+
+ pressKey(ic, KeyEvent.KEYCODE_DEL)
+ promise.value
+ assertText("Can backspace with shift+backspace", ic, "oo")
+
+ pressKey(ic, KeyEvent.KEYCODE_DEL)
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP))
+ assertTextAndSelectionAt(
+ "Cannot forward delete with shift+backspace",
+ ic,
+ "oo",
+ 0,
+ )
+ }
+
+ // Bug 1490391 - Committing then setting composition can result in duplicates.
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1490391() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Can set initial text", ic, "")
+
+ commitText(ic, "far", 1)
+ setComposingText(ic, "bar", 1)
+ assertTextAndSelectionAt(
+ "Can commit then set composition",
+ ic,
+ "farbar",
+ 6,
+ )
+ setComposingText(ic, "baz", 1)
+ assertTextAndSelectionAt(
+ "Composition still exists after setting",
+ ic,
+ "farbaz",
+ 6,
+ )
+
+ finishComposingText(ic)
+
+ // TODO:
+ // Call ic.deleteSurroundingText(6, 0) and check result.
+ // Actually, no way to wait deleteSurroudingText since this may fire
+ // multiple events.
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun sendDummyKeyboardEvent() {
+ // unnecessary for designmode
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+ assertText("Set initial text", ic, "")
+
+ commitText(ic, "foo", 1)
+ assertTextAndSelectionAt("commit text and selection", ic, "foo", 3)
+
+ // Dispatching keydown, input and keyup
+ val promise =
+ mainSession.evaluatePromiseJS(
+ """
+ new Promise(r => window.addEventListener('keydown', () => {
+ window.addEventListener('input',() => {
+ window.addEventListener('keyup', r, { once: true }) },
+ { once: true }) },
+ { once: true}))""",
+ )
+ ic.beginBatchEdit()
+ ic.setSelection(0, 3)
+ ic.setComposingText("", 1)
+ ic.endBatchEdit()
+ promise.value
+ assertText("empty text", ic, "")
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun editorInfo_default() {
+ mainSession.textInput.view = View(InstrumentationRegistry.getInstrumentation().targetContext)
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ textContent = ""
+ mainSession.evaluateJS("document.querySelector('$id').focus()")
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+
+ val editorInfo = EditorInfo()
+ mainSession.textInput.onCreateInputConnection(editorInfo)
+ assertThat(
+ "Default EditorInfo.inputType",
+ editorInfo.inputType,
+ equalTo(
+ when (id) {
+ "#input" ->
+ InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_FLAG_AUTO_CORRECT or
+ InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE
+ else ->
+ InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
+ InputType.TYPE_TEXT_FLAG_AUTO_CORRECT or
+ InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE
+ },
+ ),
+ )
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun editorInfo_defaultByInputType() {
+ assumeThat("type attribute is input element only", id, equalTo("#input"))
+ // Disable this with WebRender due to unexpected abort by mozilla::gl::GLContext::fTexSubImage2D
+ // (Bug 1706688, Bug 1710060 and etc)
+ assumeThat(sessionRule.env.isWebrender and sessionRule.env.isDebugBuild, equalTo(false))
+
+ mainSession.textInput.view = View(InstrumentationRegistry.getInstrumentation().targetContext)
+ mainSession.loadTestPath(FORMS5_HTML_PATH)
+ mainSession.waitForPageStop()
+
+ for (inputType in listOf("#email1", "#pass1", "#search1", "#tel1", "#url1")) {
+ mainSession.evaluateJS("document.querySelector('$inputType').focus()")
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+
+ // IC will be updated asynchronously, so spin event loop
+ processChildEvents()
+ processParentEvents()
+
+ val editorInfo = EditorInfo()
+ val ic = mainSession.textInput.onCreateInputConnection(editorInfo)!!
+ assertThat("InputConnection is created correctly", ic, notNullValue())
+
+ // Even if we get IC, new EditorInfo isn't updated yet.
+ // We post and wait for empty job to IC thread to flush all IC's job.
+ val result = object : GeckoResult<Boolean>() {
+ init {
+ val icHandler = mainSession.textInput.getHandler(Handler(Looper.getMainLooper()))
+ icHandler.post({
+ complete(true)
+ })
+ }
+ }
+ sessionRule.waitForResult(result)
+ mainSession.textInput.onCreateInputConnection(editorInfo)
+
+ assertThat(
+ "EditorInfo.inputType of $inputType",
+ editorInfo.inputType,
+ equalTo(
+ when (inputType) {
+ "#email1" ->
+ InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ "#pass1" ->
+ InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_VARIATION_PASSWORD
+ "#search1" ->
+ InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_FLAG_AUTO_CORRECT or
+ InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE or
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
+ "#tel1" -> InputType.TYPE_CLASS_PHONE
+ "#url1" ->
+ InputType.TYPE_CLASS_TEXT or
+ InputType.TYPE_TEXT_VARIATION_URI
+ else -> 0
+ },
+ ),
+ )
+ }
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun editorInfo_enterKeyHint() {
+ // no way to set enterkeyhint on designmode.
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+
+ sessionRule.setPrefsUntilTestEnd(mapOf("dom.forms.enterkeyhint" to true))
+
+ mainSession.textInput.view = View(InstrumentationRegistry.getInstrumentation().targetContext)
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ textContent = ""
+ val values = listOf("enter", "done", "go", "previous", "next", "search", "send")
+ for (enterkeyhint in values) {
+ mainSession.evaluateJS(
+ """
+ document.querySelector('$id').enterKeyHint = '$enterkeyhint';
+ document.querySelector('$id').focus()""",
+ )
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+
+ val editorInfo = EditorInfo()
+ mainSession.textInput.onCreateInputConnection(editorInfo)
+ assertThat(
+ "EditorInfo.imeOptions by $enterkeyhint",
+ editorInfo.imeOptions and EditorInfo.IME_MASK_ACTION,
+ equalTo(
+ when (enterkeyhint) {
+ "done" -> EditorInfo.IME_ACTION_DONE
+ "go" -> EditorInfo.IME_ACTION_GO
+ "next" -> EditorInfo.IME_ACTION_NEXT
+ "previous" -> EditorInfo.IME_ACTION_PREVIOUS
+ "search" -> EditorInfo.IME_ACTION_SEARCH
+ "send" -> EditorInfo.IME_ACTION_SEND
+ else -> EditorInfo.IME_ACTION_NONE
+ },
+ ),
+ )
+
+ mainSession.evaluateJS("document.querySelector('$id').blur()")
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+ }
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun editorInfo_autocapitalize() {
+ // no way to set autocapitalize on designmode.
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+
+ sessionRule.setPrefsUntilTestEnd(mapOf("dom.forms.autocapitalize" to true))
+
+ mainSession.textInput.view = View(InstrumentationRegistry.getInstrumentation().targetContext)
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ textContent = ""
+ val values = listOf("characters", "none", "sentences", "words", "off", "on")
+ for (autocapitalize in values) {
+ mainSession.evaluateJS(
+ """
+ document.querySelector('$id').autocapitalize = '$autocapitalize';
+ document.querySelector('$id').focus()""",
+ )
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+
+ val editorInfo = EditorInfo()
+ mainSession.textInput.onCreateInputConnection(editorInfo)
+ assertThat(
+ "EditorInfo.inputType by $autocapitalize",
+ editorInfo.inputType and 0x00007000,
+ equalTo(
+ when (autocapitalize) {
+ "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
+ "on" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
+ "sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
+ "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
+ else -> 0
+ },
+ ),
+ )
+
+ mainSession.evaluateJS("document.querySelector('$id').blur()")
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+ }
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun bug1613804_finishComposingText() {
+ mainSession.textInput.view = View(InstrumentationRegistry.getInstrumentation().targetContext)
+
+ mainSession.loadTestPath(INPUTS_PATH)
+ mainSession.waitForPageStop()
+
+ textContent = ""
+ mainSession.evaluateJS("document.querySelector('$id').focus()")
+ mainSession.waitUntilCalled(GeckoSession.TextInputDelegate::class, "restartInput")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+
+ ic.beginBatchEdit()
+ ic.setComposingText("abc", 1)
+ ic.endBatchEdit()
+
+ // finishComposingText has to dispatch compositionend event.
+ finishComposingText(ic)
+
+ assertText("commit abc", ic, "abc")
+ }
+
+ // Bug 1593683 - Cursor is jumping when using the arrow keys in input field on GBoard
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1593683() {
+ setupContent("")
+
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+
+ setComposingText(ic, "foo", 1)
+ assertTextAndSelectionAt("Can set the composing text", ic, "foo", 3)
+ // Arrow key should keep composition then move caret
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ pressKey(ic, KeyEvent.KEYCODE_DPAD_LEFT)
+ assertSelection("IME caret is moved to top", ic, 0, 0, /* checkGecko */ false)
+
+ setComposingText(ic, "bar", 1)
+ finishComposingText(ic)
+ assertText("commit abc", ic, "bar")
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1633621() {
+ // no way on designmode.
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+
+ setupContent("")
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+
+ mainSession.evaluateJS(
+ """
+ document.querySelector('$id').addEventListener('input', () => {
+ document.querySelector('$id').blur();
+ document.querySelector('$id').focus();
+ })
+ """,
+ )
+
+ setComposingText(ic, "b", 1)
+ assertTextAndSelectionAt(
+ "Don't change caret position after calling blur and focus",
+ ic,
+ "b",
+ 1,
+ )
+
+ setComposingText(ic, "a", 1)
+ assertTextAndSelectionAt(
+ "Can set composition string after calling blur and focus",
+ ic,
+ "ba",
+ 2,
+ )
+
+ pressKey(ic, KeyEvent.KEYCODE_R)
+ assertText(
+ "Can set input string by keypress after calling blur and focus",
+ ic,
+ "bar",
+ )
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1650705() {
+ // no way on designmode.
+ assumeThat("Not in designmode", id, not(equalTo("#designmode")))
+
+ setupContent("")
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+
+ commitText(ic, "foo", 1)
+ ic.setSelection(0, 3)
+
+ mainSession.evaluateJS(
+ """
+ input_event_count = 0;
+ document.querySelector('$id').addEventListener('input', () => {
+ input_event_count++;
+ })
+ """,
+ )
+
+ setComposingText(ic, "barbaz", 1)
+
+ val count = mainSession.evaluateJS("input_event_count") as Double
+ assertThat("input event is once", count, equalTo(1.0))
+
+ finishComposingText(ic)
+ }
+
+ @WithDisplay(width = 512, height = 512)
+ // Child process updates require having a display.
+ @Test
+ fun inputConnection_bug1767556() {
+ setupContent("")
+ val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!!
+
+ // Emulate GBoard's InputConnection API calls
+ ic.beginBatchEdit()
+ ic.setComposingText("fooba", 1)
+ ic.endBatchEdit()
+ ic.setComposingText("fooba", 1)
+ processChildEvents()
+
+ ic.beginBatchEdit()
+ ic.setComposingText("foobaz", 1)
+ ic.endBatchEdit()
+ ic.setComposingText("foobaz", 1)
+ processChildEvents()
+
+ ic.beginBatchEdit()
+ ic.setComposingText("foobaz1", 1)
+ ic.endBatchEdit()
+ ic.setComposingText("foobaz1", 1)
+ processChildEvents()
+
+ finishComposingText(ic)
+ assertText("commit foobaz1", ic, "foobaz1")
+ }
+}