mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-06-08 15:47:43 +00:00
rename package
introduces weird bugs for some reason
This commit is contained in:
parent
8c40d4f3e8
commit
a91a90bfbd
312 changed files with 1620 additions and 1612 deletions
962
app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt
Normal file
962
app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt
Normal file
|
@ -0,0 +1,962 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.latin
|
||||
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Message
|
||||
import android.text.InputType
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.*
|
||||
import androidx.core.content.edit
|
||||
import helium314.keyboard.ShadowInputMethodManager2
|
||||
import helium314.keyboard.ShadowLocaleManagerCompat
|
||||
import helium314.keyboard.event.Event
|
||||
import helium314.keyboard.keyboard.KeyboardSwitcher
|
||||
import helium314.keyboard.keyboard.MainKeyboardView
|
||||
import helium314.keyboard.latin.ShadowFacilitator2.Companion.lastAddedWord
|
||||
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
|
||||
import helium314.keyboard.latin.common.Constants
|
||||
import helium314.keyboard.latin.common.StringUtils
|
||||
import helium314.keyboard.latin.inputlogic.InputLogic
|
||||
import helium314.keyboard.latin.inputlogic.SpaceState
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.DeviceProtectedUtils
|
||||
import helium314.keyboard.latin.utils.ScriptUtils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.Implementation
|
||||
import org.robolectric.annotation.Implements
|
||||
import org.robolectric.shadows.ShadowLog
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(shadows = [
|
||||
ShadowLocaleManagerCompat::class,
|
||||
ShadowInputMethodManager2::class,
|
||||
ShadowInputMethodService::class,
|
||||
ShadowKeyboardSwitcher::class,
|
||||
ShadowHandler::class,
|
||||
ShadowFacilitator2::class,
|
||||
])
|
||||
class InputLogicTest {
|
||||
private lateinit var latinIME: LatinIME
|
||||
private val settingsValues get() = Settings.getInstance().current
|
||||
private val inputLogic get() = latinIME.mInputLogic
|
||||
private val connection: RichInputConnection get() = inputLogic.mConnection
|
||||
private val composerReader = InputLogic::class.java.getDeclaredField("mWordComposer").apply { isAccessible = true }
|
||||
private val composer get() = composerReader.get(inputLogic) as WordComposer
|
||||
private val spaceStateReader = InputLogic::class.java.getDeclaredField("mSpaceState").apply { isAccessible = true }
|
||||
private val spaceState get() = spaceStateReader.get(inputLogic) as Int
|
||||
private val beforeComposingReader = RichInputConnection::class.java.getDeclaredField("mCommittedTextBeforeComposingText").apply { isAccessible = true }
|
||||
private val connectionTextBeforeComposingText get() = (beforeComposingReader.get(connection) as CharSequence).toString()
|
||||
private val composingReader = RichInputConnection::class.java.getDeclaredField("mComposingText").apply { isAccessible = true }
|
||||
private val connectionComposingText get() = (composingReader.get(connection) as CharSequence).toString()
|
||||
|
||||
@Before fun setUp() {
|
||||
latinIME = Robolectric.setupService(LatinIME::class.java)
|
||||
// start logging only after latinIME is created, avoids showing the stack traces if library is not found
|
||||
ShadowLog.setupLogging()
|
||||
ShadowLog.stream = System.out
|
||||
}
|
||||
|
||||
@Test fun inputCode() {
|
||||
reset()
|
||||
input('c')
|
||||
assertEquals("c", textBeforeCursor)
|
||||
assertEquals("c", getText())
|
||||
assertEquals("", textAfterCursor)
|
||||
assertEquals("c", composingText)
|
||||
latinIME.mHandler.onFinishInput()
|
||||
assertEquals("", composingText)
|
||||
}
|
||||
|
||||
@Test fun delete() {
|
||||
reset()
|
||||
setText("hello there ")
|
||||
functionalKeyPress(Constants.CODE_DELETE)
|
||||
assertEquals("hello there", text)
|
||||
assertEquals("there", composingText)
|
||||
}
|
||||
|
||||
@Test fun deleteInsideWord() {
|
||||
reset()
|
||||
setText("hello you there")
|
||||
setCursorPosition(8) // after o in you
|
||||
functionalKeyPress(Constants.CODE_DELETE)
|
||||
assertEquals("hello yu there", text)
|
||||
// todo: do we really want an empty composing text in this case?
|
||||
// setting whole word composing will delete text behind cursor
|
||||
// setting part before cursor as composing may be bad if user just wants to adjust a letter and result is some autocorrect
|
||||
assertEquals("", composingText)
|
||||
}
|
||||
|
||||
@Test fun insertLetterIntoWord() {
|
||||
reset()
|
||||
setText("hello")
|
||||
setCursorPosition(3) // after first l
|
||||
input('i')
|
||||
assertEquals("helilo", getWordAtCursor())
|
||||
assertEquals("helilo", getText())
|
||||
assertEquals(4, getCursorPosition())
|
||||
assertEquals(4, cursor)
|
||||
assertEquals("", composingText)
|
||||
}
|
||||
|
||||
@Test fun insertLetterIntoWordWithWeirdEditor() {
|
||||
reset()
|
||||
currentInputType = 180225 // should not change much, but just to be sure
|
||||
setText("hello")
|
||||
setCursorPosition(3, weirdTextField = true) // after first l
|
||||
input('i')
|
||||
assertEquals("helilo", getWordAtCursor())
|
||||
assertEquals("helilo", getText())
|
||||
assertEquals(4, getCursorPosition())
|
||||
assertEquals(4, cursor)
|
||||
}
|
||||
|
||||
@Test fun insertLetterIntoOneOfSeveralWords() {
|
||||
reset()
|
||||
setText("hello my friend")
|
||||
setCursorPosition(7) // between m and y
|
||||
input('a')
|
||||
assertEquals("may", getWordAtCursor())
|
||||
assertEquals("hello may friend", getText())
|
||||
assertEquals(8, getCursorPosition())
|
||||
assertEquals(8, cursor)
|
||||
}
|
||||
|
||||
// todo: make it work, but it might not be that simple because adding is done in combiner
|
||||
// https://github.com/Helium314/openboard/issues/214
|
||||
@Test fun insertLetterIntoWordHangul() {
|
||||
reset()
|
||||
currentScript = ScriptUtils.SCRIPT_HANGUL
|
||||
chainInput("ㅛㅎㄹㅎㅕㅛ")
|
||||
setCursorPosition(3)
|
||||
input('ㄲ') // fails, as expected from the hangul issue when processing the event in onCodeInput
|
||||
assertEquals("ㅛㅎㄹㄲ혀ㅛ", getWordAtCursor())
|
||||
assertEquals("ㅛㅎㄹㄲ혀ㅛ", getText())
|
||||
assertEquals("ㅛㅎㄹㄲ혀ㅛ", textBeforeCursor + textAfterCursor)
|
||||
assertEquals(4, getCursorPosition())
|
||||
assertEquals(4, cursor)
|
||||
}
|
||||
|
||||
@Test fun separatorUnselectsWord() {
|
||||
reset()
|
||||
setText("hello")
|
||||
assertEquals("hello", composingText)
|
||||
input('.')
|
||||
assertEquals("", composingText)
|
||||
}
|
||||
|
||||
@Test fun autospace() {
|
||||
reset()
|
||||
setText("hello")
|
||||
input('.')
|
||||
input('a')
|
||||
assertEquals("hello.a", textBeforeCursor)
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) }
|
||||
setText("hello")
|
||||
input('.')
|
||||
input('a')
|
||||
assertEquals("hello. a", textBeforeCursor)
|
||||
}
|
||||
|
||||
@Test fun autospaceButWithTextAfter() {
|
||||
reset()
|
||||
setText("hello there")
|
||||
setCursorPosition(5) // after hello
|
||||
input('.')
|
||||
input('a')
|
||||
assertEquals("hello.a", textBeforeCursor)
|
||||
assertEquals("hello.a there", text)
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) }
|
||||
setText("hello there")
|
||||
setCursorPosition(5) // after hello
|
||||
input('.')
|
||||
input('a')
|
||||
assertEquals("hello. a", textBeforeCursor)
|
||||
assertEquals("hello. a there", text)
|
||||
}
|
||||
|
||||
@Test fun noAutospaceInUrlField() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) }
|
||||
chainInput("example.net")
|
||||
assertEquals("example. net", text)
|
||||
lastAddedWord = ""
|
||||
setText("")
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
chainInput("example.net")
|
||||
assertEquals("", lastAddedWord)
|
||||
assertEquals("example.net", text)
|
||||
assertEquals("example.net", composingText)
|
||||
}
|
||||
|
||||
@Test fun noAutospaceForDetectedUrl() { // "light" version, should work without url detection
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) }
|
||||
chainInput("http://example.net")
|
||||
assertEquals("http://example.net", text)
|
||||
assertEquals("http", lastAddedWord)
|
||||
assertEquals("example.net", composingText)
|
||||
}
|
||||
|
||||
@Test fun noAutospaceForDetectedEmail() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) }
|
||||
chainInput("mail@example.com")
|
||||
assertEquals("mail@example.com", text)
|
||||
assertEquals("mail@example", lastAddedWord) // todo: do we want this? not really nice, but don't want to be too aggressive with URL detection disabled
|
||||
assertEquals("com", composingText) // todo: maybe this should still see the whole address as a single word? or don't be too aggressive?
|
||||
setText("")
|
||||
lastAddedWord = ""
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("mail@example.com")
|
||||
assertEquals("", lastAddedWord)
|
||||
assertEquals("mail@example.com", composingText)
|
||||
}
|
||||
|
||||
@Test fun urlDetectionThings() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("...h")
|
||||
assertEquals("...h", text)
|
||||
assertEquals("h", composingText)
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("bla..")
|
||||
assertEquals("bla..", text)
|
||||
assertEquals("", composingText)
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("bla.c")
|
||||
assertEquals("bla.c", text)
|
||||
assertEquals("bla.c", composingText)
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) }
|
||||
input("bla")
|
||||
input('.')
|
||||
functionalKeyPress(Constants.CODE_SHIFT) // should remove the phantom space (in addition to normal effect)
|
||||
input('c')
|
||||
assertEquals("bla.c", text)
|
||||
assertEquals("bla.c", composingText)
|
||||
}
|
||||
|
||||
@Test fun stripSeparatorsBeforeAddingToHistoryWithURLDetection() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("example.com.")
|
||||
assertEquals("example.com.", composingText)
|
||||
input(' ')
|
||||
assertEquals("example.com", lastAddedWord)
|
||||
}
|
||||
|
||||
@Test fun dontSelectConsecutiveSeparatorsWithURLDetection() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("bla..")
|
||||
assertEquals("", composingText)
|
||||
assertEquals("bla..", text)
|
||||
}
|
||||
|
||||
@Test fun selectDoesSelect() {
|
||||
reset()
|
||||
setText("this is some text")
|
||||
setCursorPosition(3, 8)
|
||||
assertEquals("s is ", text.substring(3, 8))
|
||||
}
|
||||
|
||||
@Test fun noComposingForPasswordFields() {
|
||||
reset()
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)
|
||||
input('a')
|
||||
input('b')
|
||||
assertEquals("", composingText)
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
input('.')
|
||||
input('c')
|
||||
assertEquals("", composingText)
|
||||
}
|
||||
|
||||
@Test fun `don't select whole thing as composing word if URL detection disabled`() {
|
||||
reset()
|
||||
setText("http://example.com")
|
||||
setCursorPosition(13) // between l and e
|
||||
assertEquals("example", composingText)
|
||||
}
|
||||
|
||||
@Test fun `select whole thing except http(s) as composing word if URL detection enabled and selecting`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setText("http://example.com")
|
||||
setCursorPosition(13) // between l and e
|
||||
assertEquals("example.com", composingText)
|
||||
setText("http://bla.com http://example.com ")
|
||||
setCursorPosition(29) // between l and e
|
||||
assertEquals("example.com", composingText)
|
||||
}
|
||||
|
||||
@Test fun `select whole thing except http(s) as composing word if URL detection enabled and typing`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("http://example.com")
|
||||
assertEquals("example.com", composingText)
|
||||
}
|
||||
|
||||
@Test fun `don't add partial URL to history`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setText("http:/") // just so lastAddedWord isn't set to http
|
||||
chainInput("/bla.com")
|
||||
assertEquals("", lastAddedWord)
|
||||
}
|
||||
|
||||
@Test fun urlProperlySelected() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
setText("http://example.com/here")
|
||||
setCursorPosition(18) // after .com
|
||||
functionalKeyPress(Constants.CODE_DELETE)
|
||||
functionalKeyPress(Constants.CODE_DELETE)
|
||||
functionalKeyPress(Constants.CODE_DELETE) // delete com
|
||||
// todo: do we really want no composing text?
|
||||
// probably not... try not to break composing
|
||||
assertEquals("", composingText)
|
||||
chainInput("net")
|
||||
assertEquals("example.net", composingText)
|
||||
}
|
||||
|
||||
@Test fun urlProperlySelectedWhenNotDeletingFullTld() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setText("http://example.com/here")
|
||||
setCursorPosition(18) // after .com
|
||||
functionalKeyPress(Constants.CODE_DELETE)
|
||||
functionalKeyPress(Constants.CODE_DELETE) // delete om
|
||||
// todo: this is a weird difference to deleting the full TLD (see urlProperlySelected)
|
||||
// what do we want here? (probably consistency)
|
||||
assertEquals("example.c/here", composingText)
|
||||
chainInput("z")
|
||||
assertEquals("", composingText) // todo: this is a weird difference to deleting the full TLD
|
||||
// assertEquals("example.cz", composingText) // fails, but probably would be better than above
|
||||
}
|
||||
|
||||
@Test fun dontCommitPartialUrlBeforeFirstPeriod() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
// type http://bla. -> bla not selected, but clearly url, also means http://bla is committed which we probably don't want
|
||||
chainInput("http://bla.")
|
||||
assertEquals("bla.", composingText)
|
||||
}
|
||||
|
||||
@Test fun `intermediate commits in text field without protocol`() {
|
||||
reset()
|
||||
chainInput("bla.")
|
||||
assertEquals("bla", lastAddedWord)
|
||||
chainInput("com/")
|
||||
assertEquals("com", lastAddedWord)
|
||||
chainInput("img.jpg")
|
||||
assertEquals("img", lastAddedWord)
|
||||
assertEquals("jpg", composingText)
|
||||
}
|
||||
|
||||
@Test fun `intermediate commit in text field without protocol and with URL detection`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("bla.com/img.jpg")
|
||||
assertEquals("bla", lastAddedWord)
|
||||
assertEquals("bla.com/img.jpg", composingText)
|
||||
}
|
||||
|
||||
@Test fun `only protocol commit in text field with protocol and URL detection`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("http://bla.com/img.jpg")
|
||||
assertEquals("http", lastAddedWord)
|
||||
assertEquals("bla.com/img.jpg", composingText)
|
||||
}
|
||||
|
||||
@Test fun `no intermediate commit in URL field with protocol`() {
|
||||
reset()
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
chainInput("http://bla.com/img.jpg")
|
||||
assertEquals("http", lastAddedWord) // todo: somehow avoid?
|
||||
assertEquals("http://bla.com/img.jpg", text)
|
||||
assertEquals("bla.com/img.jpg", composingText)
|
||||
}
|
||||
|
||||
@Test fun `no intermediate commit in URL field with protocol and URL detection`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
chainInput("http://bla.com/img.jpg")
|
||||
assertEquals("http", lastAddedWord) // todo: somehow avoid?
|
||||
assertEquals("http://bla.com/img.jpg", text)
|
||||
assertEquals("bla.com/img.jpg", composingText)
|
||||
}
|
||||
|
||||
@Test fun `no intermediate commit in URL field without protocol`() {
|
||||
reset()
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
chainInput("bla.com/img.jpg")
|
||||
assertEquals("", lastAddedWord)
|
||||
assertEquals("bla.com/img.jpg", text)
|
||||
assertEquals("bla.com/img.jpg", composingText)
|
||||
}
|
||||
|
||||
@Test fun `no intermediate commit in URL field without protocol and with URL detection`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
chainInput("bla.com/img.jpg")
|
||||
assertEquals("", lastAddedWord)
|
||||
assertEquals("bla.com/img.jpg", text)
|
||||
assertEquals("bla.com/img.jpg", composingText)
|
||||
}
|
||||
|
||||
@Test fun `don't accidentally detect some other text fields as URI`() {
|
||||
// see comment in InputLogic.textBeforeCursorMayBeUrlOrSimilar
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) }
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE)
|
||||
chainInput("Hey,why")
|
||||
assertEquals("Hey, why", text)
|
||||
}
|
||||
|
||||
@Test fun `URL detection does not trigger on non-words`() {
|
||||
// first make sure it works without URL detection
|
||||
reset()
|
||||
chainInput("15:50-17")
|
||||
assertEquals("15:50-17", text)
|
||||
assertEquals("", composingText)
|
||||
// then with URL detection
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
chainInput("15:50-17")
|
||||
assertEquals("15:50-17", text)
|
||||
assertEquals("", composingText)
|
||||
}
|
||||
|
||||
@Test fun `autospace after selecting a suggestion`() {
|
||||
reset()
|
||||
pickSuggestion("this")
|
||||
input('b')
|
||||
assertEquals("this b", text)
|
||||
assertEquals("b", composingText)
|
||||
}
|
||||
|
||||
@Test fun `autospace works in URL field when input isn't URL`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
pickSuggestion("this")
|
||||
input('b')
|
||||
assertEquals("this b", text)
|
||||
assertEquals("b", composingText)
|
||||
}
|
||||
|
||||
// https://github.com/Helium314/openboard/issues/215
|
||||
// https://github.com/Helium314/openboard/issues/229
|
||||
@Test fun `autospace works in URL field when input isn't URL, also for multiple suggestions`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
pickSuggestion("this")
|
||||
pickSuggestion("is")
|
||||
assertEquals("this is", text)
|
||||
pickSuggestion("not")
|
||||
assertEquals("this is not", text)
|
||||
input('c')
|
||||
assertEquals("this is not c", text)
|
||||
assertEquals("c", composingText)
|
||||
}
|
||||
|
||||
@Test fun `emoji is added to dictionary`() {
|
||||
// check both text and codepoint input
|
||||
reset()
|
||||
chainInput("hello ")
|
||||
input(0x1F36D)
|
||||
assertEquals(StringUtils.newSingleCodePointString(0x1F36D), lastAddedWord)
|
||||
reset()
|
||||
chainInput("hello ")
|
||||
input("🤗")
|
||||
assertEquals("\uD83E\uDD17", lastAddedWord)
|
||||
|
||||
reset()
|
||||
chainInput("hello ")
|
||||
input("why 🤗 ") // not added because it's not only emoji (input can come from pasting)
|
||||
assertEquals("hello", lastAddedWord)
|
||||
}
|
||||
|
||||
@Test fun `emoji uses phantom space`() {
|
||||
// check both text and codepoint input
|
||||
reset()
|
||||
pickSuggestion("hi")
|
||||
input("🤗")
|
||||
assertEquals("\uD83E\uDD17", lastAddedWord)
|
||||
assertEquals("hi \uD83E\uDD17", text)
|
||||
reset()
|
||||
pickSuggestion("hi")
|
||||
input(0x1F36D)
|
||||
assertEquals(StringUtils.newSingleCodePointString(0x1F36D), lastAddedWord)
|
||||
assertEquals("hi ${StringUtils.newSingleCodePointString(0x1F36D)}", text)
|
||||
}
|
||||
|
||||
// https://github.com/Helium314/openboard/issues/230
|
||||
@Test fun `no autospace after opening quotes`() {
|
||||
reset()
|
||||
chainInput("\"Hi\" \"h")
|
||||
assertEquals("\"Hi\" \"h", text)
|
||||
assertEquals("h", composingText)
|
||||
reset()
|
||||
chainInput("\"Hi\", \"h")
|
||||
assertEquals("\"Hi\", \"h", text)
|
||||
assertEquals("h", composingText)
|
||||
}
|
||||
|
||||
@Test fun `autospace works in URL field when starting with quotes`() {
|
||||
reset()
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
|
||||
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
|
||||
input("\"")
|
||||
pickSuggestion("this")
|
||||
input("i")
|
||||
assertEquals("\"this i", text)
|
||||
}
|
||||
|
||||
// ------- helper functions ---------
|
||||
|
||||
// should be called before every test, so the same state is guaranteed
|
||||
private fun reset() {
|
||||
// reset input connection & facilitator
|
||||
currentScript = ScriptUtils.SCRIPT_LATIN
|
||||
text = ""
|
||||
batchEdit = 0
|
||||
currentInputType = InputType.TYPE_CLASS_TEXT
|
||||
lastAddedWord = ""
|
||||
|
||||
// reset settings
|
||||
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { clear() }
|
||||
|
||||
setText("") // (re)sets selection and composing word
|
||||
}
|
||||
|
||||
private fun chainInput(text: String) = text.forEach { input(it.code) }
|
||||
|
||||
private fun input(char: Char) = input(char.code)
|
||||
|
||||
private fun input(codePoint: Int) {
|
||||
require(codePoint > 0) { "not a codePoint: $codePoint" }
|
||||
val oldBefore = textBeforeCursor
|
||||
val oldAfter = textAfterCursor
|
||||
val insert = StringUtils.newSingleCodePointString(codePoint)
|
||||
val phantomSpaceToInsert = if (spaceState == SpaceState.PHANTOM) " " else ""
|
||||
|
||||
latinIME.onEvent(Event.createEventForCodePointFromUnknownSource(codePoint))
|
||||
handleMessages()
|
||||
|
||||
if (currentScript != ScriptUtils.SCRIPT_HANGUL) { // check fails if hangul combiner merges symbols
|
||||
if (phantomSpaceToInsert.isEmpty())
|
||||
assertEquals(oldBefore + insert, textBeforeCursor)
|
||||
else // in some cases autospace might be suppressed
|
||||
assert(oldBefore + phantomSpaceToInsert + insert == textBeforeCursor || oldBefore + insert == textBeforeCursor)
|
||||
}
|
||||
assertEquals(oldAfter, textAfterCursor)
|
||||
assertEquals(textBeforeCursor + textAfterCursor, getText())
|
||||
checkConnectionConsistency()
|
||||
}
|
||||
|
||||
private fun functionalKeyPress(keyCode: Int) {
|
||||
require(keyCode < 0) { "not a functional key code: $keyCode" }
|
||||
latinIME.onEvent(Event.createSoftwareKeypressEvent(Event.NOT_A_CODE_POINT, keyCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false))
|
||||
handleMessages()
|
||||
checkConnectionConsistency()
|
||||
}
|
||||
|
||||
// almost the same as codePoint input, but calls different latinIME function
|
||||
private fun input(insert: String) {
|
||||
val oldBefore = textBeforeCursor
|
||||
val oldAfter = textAfterCursor
|
||||
val phantomSpaceToInsert = if (spaceState == SpaceState.PHANTOM) " " else ""
|
||||
|
||||
latinIME.onTextInput(insert)
|
||||
handleMessages()
|
||||
|
||||
if (phantomSpaceToInsert.isEmpty())
|
||||
assertEquals(oldBefore + insert, textBeforeCursor)
|
||||
else // in some cases autospace might be suppressed
|
||||
assert(oldBefore + phantomSpaceToInsert + insert == textBeforeCursor || oldBefore + insert == textBeforeCursor)
|
||||
assert(oldBefore + insert == textBeforeCursor || "$oldBefore $insert" == textBeforeCursor)
|
||||
assertEquals(oldAfter, textAfterCursor)
|
||||
assertEquals(textBeforeCursor + textAfterCursor, getText())
|
||||
checkConnectionConsistency()
|
||||
}
|
||||
|
||||
private fun getWordAtCursor() = connection.getWordRangeAtCursor(
|
||||
settingsValues.mSpacingAndPunctuations,
|
||||
currentScript,
|
||||
false
|
||||
).mWord
|
||||
|
||||
private fun setCursorPosition(start: Int, end: Int = start, weirdTextField: Boolean = false) {
|
||||
val ei = EditorInfo()
|
||||
ei.inputType = currentInputType
|
||||
ei.initialSelStart = start
|
||||
ei.initialSelEnd = end
|
||||
// imeOptions should not matter
|
||||
|
||||
// adjust text in inputConnection first, otherwise fixLyingCursorPosition will move cursor
|
||||
// to the end of the text
|
||||
val fullText = textBeforeCursor + selectedText + textAfterCursor
|
||||
assertEquals(fullText, getText())
|
||||
|
||||
// need to update ic before, otherwise when reloading text cache from ic, ric will load wrong text before cursor
|
||||
val oldStart = selectionStart
|
||||
val oldEnd = selectionEnd
|
||||
selectionStart = start
|
||||
selectionEnd = end
|
||||
assertEquals(fullText, textBeforeCursor + selectedText + textAfterCursor)
|
||||
|
||||
latinIME.onUpdateSelection(oldStart, oldEnd, start, end, composingStart, composingEnd)
|
||||
handleMessages()
|
||||
|
||||
if (weirdTextField) {
|
||||
latinIME.mHandler.onStartInput(ei, true) // essentially does nothing
|
||||
latinIME.mHandler.onStartInputView(ei, true) // does the thing
|
||||
handleMessages()
|
||||
}
|
||||
|
||||
assertEquals(fullText, getText())
|
||||
assertEquals(start, selectionStart)
|
||||
assertEquals(end, selectionEnd)
|
||||
checkConnectionConsistency()
|
||||
}
|
||||
|
||||
// assumes we have nothing selected
|
||||
private fun getCursorPosition(): Int {
|
||||
assertEquals(cursor, connection.expectedSelectionStart)
|
||||
assertEquals(cursor, connection.expectedSelectionEnd)
|
||||
return cursor
|
||||
}
|
||||
|
||||
// just sets the text and starts input so connection it set up correctly
|
||||
private fun setText(newText: String) {
|
||||
text = newText
|
||||
selectionStart = newText.length
|
||||
selectionEnd = selectionStart
|
||||
composingStart = -1
|
||||
composingStart = -1
|
||||
|
||||
// we need to start input to notify that something changed
|
||||
// restarting is false, so this is seen as a new text field
|
||||
val ei = EditorInfo()
|
||||
ei.inputType = currentInputType
|
||||
latinIME.mHandler.onStartInput(ei, false)
|
||||
latinIME.mHandler.onStartInputView(ei, false)
|
||||
handleMessages() // this is important so the composing span is set correctly
|
||||
checkConnectionConsistency()
|
||||
}
|
||||
|
||||
// like selecting a suggestion from strip
|
||||
private fun pickSuggestion(suggestion: String) {
|
||||
val info = SuggestedWordInfo(suggestion, "", 0, 0, null, 0, 0)
|
||||
latinIME.pickSuggestionManually(info)
|
||||
checkConnectionConsistency()
|
||||
}
|
||||
|
||||
private fun checkConnectionConsistency() {
|
||||
// RichInputConnection only has composing text up to cursor, but InputConnection has full composing text
|
||||
val expectedConnectionComposingText = if (composingStart == -1 || composingEnd == -1) ""
|
||||
else text.substring(composingStart, min(composingEnd, selectionEnd))
|
||||
assert(composingText.startsWith(expectedConnectionComposingText))
|
||||
// RichInputConnection only returns text up to cursor
|
||||
val textBeforeComposingText = if (composingStart == -1) textBeforeCursor else text.substring(0, composingStart)
|
||||
|
||||
println("consistency: $selectionStart, ${connection.expectedSelectionStart}, $selectionEnd, ${connection.expectedSelectionEnd}, $textBeforeComposingText, " +
|
||||
"$connectionTextBeforeComposingText, $composingText, $connectionComposingText, $textBeforeCursor, ${connection.getTextBeforeCursor(textBeforeCursor.length, 0)}" +
|
||||
", $textAfterCursor, ${connection.getTextAfterCursor(textAfterCursor.length, 0)}")
|
||||
assertEquals(selectionStart, connection.expectedSelectionStart)
|
||||
assertEquals(selectionEnd, connection.expectedSelectionEnd)
|
||||
assertEquals(textBeforeComposingText, connectionTextBeforeComposingText)
|
||||
assertEquals(expectedConnectionComposingText, connectionComposingText)
|
||||
assertEquals(textBeforeCursor, connection.getTextBeforeCursor(textBeforeCursor.length, 0).toString())
|
||||
assertEquals(textAfterCursor, connection.getTextAfterCursor(textAfterCursor.length, 0).toString())
|
||||
}
|
||||
|
||||
private fun getText() =
|
||||
connection.getTextBeforeCursor(100, 0).toString() + (connection.getSelectedText(0) ?: "") + connection.getTextAfterCursor(100, 0)
|
||||
|
||||
private fun setInputType(inputType: Int) {
|
||||
// set text to actually apply input type
|
||||
currentInputType = inputType
|
||||
setText(text)
|
||||
}
|
||||
|
||||
// always need to handle messages for proper simulation
|
||||
private fun handleMessages() {
|
||||
while (messages.isNotEmpty()) {
|
||||
latinIME.mHandler.handleMessage(messages.first())
|
||||
messages.removeFirst()
|
||||
}
|
||||
while (delayedMessages.isNotEmpty()) {
|
||||
val msg = delayedMessages.first()
|
||||
if (msg.what != 2) // MSG_UPDATE_SUGGESTION_STRIP, we want to ignore it because it's irrelevant and has a 500 ms timeout
|
||||
latinIME.mHandler.handleMessage(delayedMessages.first())
|
||||
delayedMessages.removeFirst()
|
||||
// delayed messages may post further messages, handle before next delayed message
|
||||
while (messages.isNotEmpty()) {
|
||||
latinIME.mHandler.handleMessage(messages.first())
|
||||
messages.removeFirst()
|
||||
}
|
||||
}
|
||||
assertEquals(0, messages.size)
|
||||
assertEquals(0, delayedMessages.size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private var currentInputType = InputType.TYPE_CLASS_TEXT
|
||||
private var currentScript = ScriptUtils.SCRIPT_LATIN
|
||||
private val messages = mutableListOf<Message>() // for latinIME / ShadowInputMethodService
|
||||
private val delayedMessages = mutableListOf<Message>() // for latinIME / ShadowInputMethodService
|
||||
// inputconnection stuff
|
||||
private var batchEdit = 0
|
||||
private var text = ""
|
||||
private var selectionStart = 0
|
||||
private var selectionEnd = 0
|
||||
private var composingStart = -1
|
||||
private var composingEnd = -1
|
||||
// convenience for access
|
||||
private val textBeforeCursor get() = text.substring(0, selectionStart)
|
||||
private val textAfterCursor get() = text.substring(selectionEnd)
|
||||
private val selectedText get() = text.substring(selectionStart, selectionEnd)
|
||||
private val cursor get() = if (selectionStart == selectionEnd) selectionStart else -1
|
||||
|
||||
// composingText should return everything, but RichInputConnection.mComposingText only returns up to cursor
|
||||
private val composingText get() = if (composingStart == -1 || composingEnd == -1) ""
|
||||
else text.substring(composingStart, composingEnd)
|
||||
|
||||
// essentially this is the text field we're editing in
|
||||
private val ic = object : InputConnection {
|
||||
// pretty clear (though this may be slow depending on the editor)
|
||||
// bad return value here is likely the cause for that weird bug improved/fixed by fixIncorrectLength
|
||||
override fun getTextBeforeCursor(p0: Int, p1: Int): CharSequence = textBeforeCursor.take(p0)
|
||||
// pretty clear (though this may be slow depending on the editor)
|
||||
override fun getTextAfterCursor(p0: Int, p1: Int): CharSequence = textAfterCursor.take(p0)
|
||||
// pretty clear
|
||||
override fun getSelectedText(p0: Int): CharSequence? = if (selectionStart == selectionEnd) null
|
||||
else text.substring(selectionStart, selectionEnd)
|
||||
// inserts text at cursor (right?), and sets it as composing text
|
||||
// this REPLACES currently composing text (even if at a different position)
|
||||
// moves the cursor: positive means relative to composing text start, negative means relative to start
|
||||
override fun setComposingText(newText: CharSequence, cursor: Int): Boolean {
|
||||
// first remove the composing text if any
|
||||
if (composingStart != -1 && composingEnd != -1)
|
||||
text = text.substring(0, composingStart) + text.substring(composingEnd)
|
||||
else // no composing span active, we should remove selected text
|
||||
if (selectionStart != selectionEnd) {
|
||||
text = textBeforeCursor + textAfterCursor
|
||||
selectionEnd = selectionStart
|
||||
}
|
||||
// then set the new text at old composing start
|
||||
// if no composing start, set it at cursor position
|
||||
val insertStart = if (composingStart == -1) selectionStart else composingStart
|
||||
text = text.substring(0, insertStart) + newText + text.substring(insertStart)
|
||||
composingStart = insertStart
|
||||
composingEnd = insertStart + newText.length
|
||||
// the cursor -1 is not clear in documentation, but
|
||||
// "So a value of 1 will always advance you to the position after the full text being inserted"
|
||||
// means that 1 must be composingEnd
|
||||
selectionStart = if (cursor > 0) composingEnd + cursor - 1
|
||||
else -cursor
|
||||
selectionEnd = selectionStart
|
||||
// todo: this should call InputMethodManager#updateSelection(View, int, int, int, int)
|
||||
// but only after batch edit has ended
|
||||
// this is not used in RichInputMethodManager, but probably ends up in LatinIME.onUpdateSelection
|
||||
// -> DO IT (though it will likely only trigger that belatedSelectionUpdate thing, it might be relevant)
|
||||
return true
|
||||
}
|
||||
override fun setComposingRegion(p0: Int, p1: Int): Boolean {
|
||||
println("setComposingRegion, $p0, $p1")
|
||||
composingStart = p0
|
||||
composingEnd = p1
|
||||
return true // never checked
|
||||
}
|
||||
// sets composing text empty, but doesn't change actual text
|
||||
override fun finishComposingText(): Boolean {
|
||||
composingStart = -1
|
||||
composingEnd = -1
|
||||
return true // always true
|
||||
}
|
||||
// as per documentation: "This behaves like calling setComposingText(text, newCursorPosition) then finishComposingText()"
|
||||
override fun commitText(p0: CharSequence, p1: Int): Boolean {
|
||||
setComposingText(p0, p1)
|
||||
finishComposingText()
|
||||
return true // whether we added the text
|
||||
}
|
||||
// just tells the text field that we add many updated, and that the editor should not
|
||||
// send status updates until batch edit ended (not actually used for this simulation)
|
||||
override fun beginBatchEdit(): Boolean {
|
||||
++batchEdit
|
||||
return true // always true
|
||||
}
|
||||
// end a batch edit, but maybe there are multiple batch edits happening
|
||||
override fun endBatchEdit(): Boolean {
|
||||
if (batchEdit > 0)
|
||||
return --batchEdit == 0
|
||||
return false // returns true if there is still a batch edit ongoing
|
||||
}
|
||||
// should notify about cursor info containing composing text, selection, ...
|
||||
// todo: maybe that could be interesting, implement it?
|
||||
override fun requestCursorUpdates(p0: Int): Boolean {
|
||||
// we call this, but don't have onUpdateCursorAnchorInfo overridden in latinIME, so it does nothing
|
||||
// also currently we don't care about the return value
|
||||
return false
|
||||
}
|
||||
override fun setSelection(p0: Int, p1: Int): Boolean {
|
||||
selectionStart = p0
|
||||
selectionEnd = p1
|
||||
// todo: call InputMethodService.onUpdateSelection(int, int, int, int, int, int), but only after batch edit is done!
|
||||
return true
|
||||
}
|
||||
// delete beforeLength before cursor position, and afterLength after cursor position
|
||||
// chars, not codepoints or glyphs
|
||||
// todo: may delete only one half of a surrogate pair, but this should be avoided by RichInputConnection (maybe throw error)
|
||||
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
|
||||
// delete only before or after selection
|
||||
text = textBeforeCursor.substring(0, textBeforeCursor.length - beforeLength) +
|
||||
text.substring(selectionStart, selectionEnd) +
|
||||
textAfterCursor.substring(afterLength)
|
||||
|
||||
// if parts of the composing span are deleted, shorten the span (set end to shorter)
|
||||
if (selectionStart <= composingStart) {
|
||||
composingStart -= beforeLength // is this correct?
|
||||
composingEnd -= beforeLength
|
||||
} else if (selectionStart <= composingEnd) {
|
||||
composingEnd -= beforeLength // is this correct?
|
||||
}
|
||||
if (selectionEnd <= composingStart) {
|
||||
composingStart -= afterLength
|
||||
composingEnd -= afterLength
|
||||
} else if (selectionEnd <= composingEnd) {
|
||||
composingEnd -= afterLength
|
||||
}
|
||||
// update selection
|
||||
selectionStart -= beforeLength
|
||||
selectionEnd -= beforeLength
|
||||
return true;
|
||||
}
|
||||
override fun sendKeyEvent(p0: KeyEvent): Boolean {
|
||||
if (p0.action != KeyEvent.ACTION_DOWN) return true // only change the text on key down, like RichInputConnection does
|
||||
if (p0.keyCode == KeyEvent.KEYCODE_DEL) {
|
||||
if (selectionEnd == 0) return true // nothing to delete
|
||||
if (selectedText.isEmpty()) {
|
||||
text = text.substring(0, selectionStart - 1) + text.substring(selectionEnd)
|
||||
selectionStart -= 1
|
||||
} else {
|
||||
text = text.substring(0, selectionStart) + text.substring(selectionEnd)
|
||||
}
|
||||
selectionEnd = selectionStart
|
||||
return true
|
||||
}
|
||||
val textToAdd = when (p0.keyCode) {
|
||||
KeyEvent.KEYCODE_ENTER -> "\n"
|
||||
KeyEvent.KEYCODE_DEL -> null
|
||||
KeyEvent.KEYCODE_UNKNOWN -> p0.characters
|
||||
else -> StringUtils.newSingleCodePointString(p0.unicodeChar)
|
||||
}
|
||||
if (textToAdd != null) {
|
||||
text = text.substring(0, selectionStart) + textToAdd + text.substring(selectionEnd)
|
||||
selectionStart += textToAdd.length
|
||||
selectionEnd = selectionStart
|
||||
composingStart = -1
|
||||
composingEnd = -1
|
||||
}
|
||||
return true
|
||||
}
|
||||
// implement only when necessary
|
||||
override fun getCursorCapsMode(p0: Int): Int = TODO("Not yet implemented")
|
||||
override fun getExtractedText(p0: ExtractedTextRequest?, p1: Int): ExtractedText = TODO("Not yet implemented")
|
||||
override fun deleteSurroundingTextInCodePoints(p0: Int, p1: Int): Boolean = TODO("Not yet implemented")
|
||||
override fun commitCompletion(p0: CompletionInfo?): Boolean = TODO("Not yet implemented")
|
||||
override fun commitCorrection(p0: CorrectionInfo?): Boolean = TODO("Not yet implemented")
|
||||
override fun performEditorAction(p0: Int): Boolean = TODO("Not yet implemented")
|
||||
override fun performContextMenuAction(p0: Int): Boolean = TODO("Not yet implemented")
|
||||
override fun clearMetaKeyStates(p0: Int): Boolean = TODO("Not yet implemented")
|
||||
override fun reportFullscreenMode(p0: Boolean): Boolean = TODO("Not yet implemented")
|
||||
override fun performPrivateCommand(p0: String?, p1: Bundle?): Boolean = TODO("Not yet implemented")
|
||||
override fun getHandler(): Handler? = TODO("Not yet implemented")
|
||||
override fun closeConnection() = TODO("Not yet implemented")
|
||||
override fun commitContent(p0: InputContentInfo, p1: Int, p2: Bundle?): Boolean = TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
// Shadows are handled by Robolectric. @Implementation overrides built-in functionality.
|
||||
// This is used for avoiding crashes (LocaleManagerCompat, InputMethodManager, KeyboardSwitcher)
|
||||
// and for simulating system stuff (InputMethodService for controlling the InputConnection, which
|
||||
// more or less is the contents of the text field), and for setting the current script in
|
||||
// KeyboardSwitcher without having to care about InputMethodSubtypes
|
||||
|
||||
// could also extend LatinIME, it's not final anyway
|
||||
@Implements(InputMethodService::class)
|
||||
class ShadowInputMethodService {
|
||||
@Implementation
|
||||
fun getCurrentInputEditorInfo() = EditorInfo().apply {
|
||||
inputType = currentInputType
|
||||
// anything else?
|
||||
}
|
||||
@Implementation
|
||||
fun getCurrentInputConnection() = ic
|
||||
@Implementation
|
||||
fun isInputViewShown() = true // otherwise selection updates will do nothing
|
||||
}
|
||||
|
||||
@Implements(Handler::class)
|
||||
class ShadowHandler {
|
||||
@Implementation
|
||||
fun sendMessage(message: Message) {
|
||||
messages.add(message)
|
||||
}
|
||||
@Implementation
|
||||
fun sendMessageDelayed(message: Message, delay: Long) {
|
||||
delayedMessages.add(message)
|
||||
}
|
||||
}
|
||||
|
||||
@Implements(KeyboardSwitcher::class)
|
||||
class ShadowKeyboardSwitcher {
|
||||
@Implementation
|
||||
// basically only needed for null check
|
||||
fun getMainKeyboardView(): MainKeyboardView = Mockito.mock(MainKeyboardView::class.java)
|
||||
@Implementation
|
||||
// only affects view
|
||||
fun setKeyboard(keyboardId: Int, toggleState: KeyboardSwitcher.KeyboardSwitchState) = Unit
|
||||
@Implementation
|
||||
// only affects view
|
||||
fun setOneHandedModeEnabled(enabled: Boolean) = Unit
|
||||
@Implementation
|
||||
// only affects view
|
||||
fun getCurrentKeyboardScriptId() = currentScript
|
||||
}
|
||||
|
||||
@Implements(DictionaryFacilitatorImpl::class)
|
||||
class ShadowFacilitator2 {
|
||||
@Implementation
|
||||
fun addToUserHistory(suggestion: String, wasAutoCapitalized: Boolean,
|
||||
ngramContext: NgramContext, timeStampInSeconds: Long,
|
||||
blockPotentiallyOffensive: Boolean) {
|
||||
lastAddedWord = suggestion
|
||||
}
|
||||
companion object {
|
||||
var lastAddedWord = ""
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue