diff --git a/app/src/androidTest/kotlin/be/scri/helpers/KeyboardTest.kt b/app/src/androidTest/kotlin/be/scri/helpers/KeyboardTest.kt index f6613c8f..6a28bdbd 100644 --- a/app/src/androidTest/kotlin/be/scri/helpers/KeyboardTest.kt +++ b/app/src/androidTest/kotlin/be/scri/helpers/KeyboardTest.kt @@ -1,14 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -package be.scri.ui +package be.scri.helpers +import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.widget.Button import androidx.test.ext.junit.runners.AndroidJUnit4 -import be.scri.helpers.KeyHandler -import be.scri.helpers.KeyboardBase -import be.scri.helpers.SuggestionHandler import be.scri.services.GeneralKeyboardIME import be.scri.services.GeneralKeyboardIME.ScribeState +import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -16,6 +15,20 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +private enum class EnterKeyExpectation { + PERFORM_ACTION, + COMMIT_NEWLINE, + DEFERRED_TO_IME, +} + +/** this repreesents a single test case for the parameterized Enter key test. */ +private data class EnterKeyTest( + val imeAction: Int, + val scribeState: ScribeState, + val expectation: EnterKeyExpectation, +) + +// Unit tests for KeyHandler @RunWith(AndroidJUnit4::class) class KeyboardTest { private lateinit var mockIME: GeneralKeyboardIME @@ -35,6 +48,7 @@ class KeyboardTest { every { mockIME.keyboard } returns mockk(relaxed = true) every { mockIME.currentState } returns ScribeState.IDLE every { mockIME.language } returns "German" + every { mockIME.handleKeycodeEnter() } returns Unit keyHandler = KeyHandler(mockIME) suggestionHandler = SuggestionHandler(mockIME) @@ -74,6 +88,53 @@ class KeyboardTest { verify(exactly = 1) { mockInputConnection.deleteSurroundingText(1, 0) } } + @Test + fun testEnterKeyBehavior_Parameterized() { + val testCases = + listOf( + EnterKeyTest(EditorInfo.IME_ACTION_NONE, ScribeState.IDLE, EnterKeyExpectation.COMMIT_NEWLINE), + EnterKeyTest(EditorInfo.IME_ACTION_SEARCH, ScribeState.IDLE, EnterKeyExpectation.PERFORM_ACTION), + EnterKeyTest(EditorInfo.IME_ACTION_SEND, ScribeState.IDLE, EnterKeyExpectation.PERFORM_ACTION), + EnterKeyTest(EditorInfo.IME_ACTION_DONE, ScribeState.IDLE, EnterKeyExpectation.PERFORM_ACTION), + EnterKeyTest(EditorInfo.IME_ACTION_SEND, ScribeState.TRANSLATE, EnterKeyExpectation.DEFERRED_TO_IME), + EnterKeyTest(EditorInfo.IME_ACTION_NONE, ScribeState.CONJUGATE, EnterKeyExpectation.DEFERRED_TO_IME), + EnterKeyTest(EditorInfo.IME_ACTION_SEARCH, ScribeState.PLURAL, EnterKeyExpectation.DEFERRED_TO_IME), + ) + + testCases.forEach { case -> + every { mockIME.currentInputEditorInfo } returns EditorInfo().apply { imeOptions = case.imeAction } + every { mockIME.currentState } returns case.scribeState + + keyHandler.handleKey(KeyboardBase.KEYCODE_ENTER, "en") + + when (case.expectation) { + EnterKeyExpectation.PERFORM_ACTION -> { + verify(exactly = 1) { mockInputConnection.performEditorAction(case.imeAction) } + verify(exactly = 0) { mockInputConnection.commitText(any(), any()) } + verify(exactly = 0) { mockIME.handleKeycodeEnter() } + } + EnterKeyExpectation.COMMIT_NEWLINE -> { + verify(exactly = 1) { mockInputConnection.commitText("\n", 1) } + verify(exactly = 0) { mockInputConnection.performEditorAction(any()) } + verify(exactly = 0) { mockIME.handleKeycodeEnter() } + } + EnterKeyExpectation.DEFERRED_TO_IME -> { + // ensures the custom command logic takes precedence + verify(exactly = 1) { mockIME.handleKeycodeEnter() } + verify(exactly = 0) { mockInputConnection.performEditorAction(any()) } + verify(exactly = 0) { mockInputConnection.commitText(any(), any()) } + } + } + + // cleans up mocks for the next iteration in the loop + clearMocks(mockInputConnection, mockIME) + every { mockIME.currentInputConnection } returns mockInputConnection + // fix: restores the mock using returns Unit + every { mockIME.handleKeycodeEnter() } returns Unit + every { mockIME.keyboard } returns mockk(relaxed = true) + } + } + @Test fun processSuggestions() { every { mockIME.findGenderForLastWord(any(), "in") } returns listOf("Neuter") diff --git a/app/src/main/java/be/scri/helpers/KeyHandler.kt b/app/src/main/java/be/scri/helpers/KeyHandler.kt index 922c99cd..a45e23ef 100644 --- a/app/src/main/java/be/scri/helpers/KeyHandler.kt +++ b/app/src/main/java/be/scri/helpers/KeyHandler.kt @@ -4,6 +4,7 @@ package be.scri.helpers import android.content.Context import android.util.Log +import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import be.scri.services.GeneralKeyboardIME import be.scri.services.GeneralKeyboardIME.ScribeState @@ -235,7 +236,36 @@ class KeyHandler( * editor action) to the main IME class. */ private fun handleEnterKey() { - ime.handleKeycodeEnter() + // step 1: checks if the keyboard is in a command state (not IDLE). + if (ime.currentState != ScribeState.IDLE) { + // reintorducing the call to the state-aware logic for command execution. + ime.handleKeycodeEnter() + return + } + + // step 2: if in IDLE state, proceed with standard IME action/newline logic. + val inputConnection = ime.currentInputConnection + val editorInfo = ime.currentInputEditorInfo + + if (inputConnection != null && editorInfo != null) { + val imeAction = editorInfo.imeOptions and EditorInfo.IME_MASK_ACTION + + when (imeAction) { + EditorInfo.IME_ACTION_GO, + EditorInfo.IME_ACTION_SEARCH, + EditorInfo.IME_ACTION_SEND, + EditorInfo.IME_ACTION_NEXT, + EditorInfo.IME_ACTION_DONE, + -> { + // performs the editor action + inputConnection.performEditorAction(imeAction) + } + else -> { + // default behaviour for inserting a new line + inputConnection.commitText("\n", 1) + } + } + } } /**