From 39e817b7f525adced77955489ba16361dba37d58 Mon Sep 17 00:00:00 2001 From: Mounil Kanakhara Date: Sun, 22 Mar 2026 16:25:37 +0530 Subject: [PATCH 1/3] fix: hide invalidInfoBar when transitioning to IDLE state Signed-off-by: Mounil Kanakhara --- .../be/scri/helpers/ui/KeyboardUIManager.kt | 2 + .../scri/helpers/ui/KeyboardUIManagerTest.kt | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/app/src/main/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/main/java/be/scri/helpers/ui/KeyboardUIManager.kt index 2edc82c17..3b8679688 100644 --- a/app/src/main/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/main/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -221,6 +221,8 @@ class KeyboardUIManager( binding.ivInfo.visibility = View.GONE binding.conjugateGridContainer.visibility = View.GONE binding.keyboardView.visibility = View.VISIBLE + binding.invalidInfoBar.visibility = View.GONE + currentPage = 0 binding.scribeKeyOptions.foreground = AppCompatResources.getDrawable(context, R.drawable.ic_scribe_icon_vector) diff --git a/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt b/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt index 04d1cf85a..ea839617e 100644 --- a/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt @@ -280,6 +280,86 @@ class KeyboardUIManagerTest { assertEquals("🚀", emojiBtnTablet3.text) } + @Test + fun `invalidInfoBar is hidden and keyboard restored when returning to IDLE after info panel shown from INVALID state`() { + // Step 1: Transition to INVALID state + uiManager.updateUI( + currentState = ScribeState.INVALID, + language = "English", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = null, + conjugateLabels = null, + selectedConjugationSubCategory = null, + currentVerbForConjugation = null, + ) + assertEquals("Toolbar bar should be visible in INVALID state", View.VISIBLE, toolbarBar.visibility) + assertEquals("Info icon should be visible in INVALID state", View.VISIBLE, ivInfo.visibility) + + // Step 2: User taps ⓘ — info panel opens + ivInfo.performClick() + assertEquals("invalidInfoBar should be visible after tapping info icon", View.VISIBLE, invalidInfoBar.visibility) + assertEquals("keyboardView should be GONE while info panel is open", View.GONE, keyboardView.visibility) + + // Step 3: User taps X (scribeKeyClose) — listener.onCloseClicked() fires, IME calls moveToIdleState() -> updateUI(IDLE) + scribeKeyClose.performClick() + verify { listener.onCloseClicked() } + + // Simulate what the IME does after onCloseClicked(): transition UI to IDLE + uiManager.updateUI( + currentState = ScribeState.IDLE, + language = "English", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = null, + conjugateLabels = null, + selectedConjugationSubCategory = null, + currentVerbForConjugation = null, + ) + + // Step 4: Assert – no orphaned UI elements remain + assertEquals("invalidInfoBar must be GONE after returning to IDLE", View.GONE, invalidInfoBar.visibility) + assertEquals("keyboardView must be VISIBLE after returning to IDLE", View.VISIBLE, keyboardView.visibility) + assertEquals("commandOptionsBar must be VISIBLE in IDLE state", View.VISIBLE, commandOptionsBar.visibility) + } + + @Test + fun `invalidInfoBar is hidden and keyboard restored when returning to IDLE after info panel shown from ALREADY_PLURAL state`() { + // Step 1: Transition to ALREADY_PLURAL state + uiManager.updateUI( + currentState = ScribeState.ALREADY_PLURAL, + language = "English", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = null, + conjugateLabels = null, + selectedConjugationSubCategory = null, + currentVerbForConjugation = null, + ) + assertEquals("Info icon should be visible in ALREADY_PLURAL state", View.VISIBLE, ivInfo.visibility) + + // Step 2: User taps ⓘ — info panel opens + ivInfo.performClick() + assertEquals("invalidInfoBar should be visible after tapping info icon", View.VISIBLE, invalidInfoBar.visibility) + assertEquals("keyboardView should be GONE while info panel is open", View.GONE, keyboardView.visibility) + + // Step 3: User taps X — simulate IME returning to IDLE + uiManager.updateUI( + currentState = ScribeState.IDLE, + language = "English", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = null, + conjugateLabels = null, + selectedConjugationSubCategory = null, + currentVerbForConjugation = null, + ) + + // Step 4: Assert – no orphaned UI elements remain + assertEquals("invalidInfoBar must be GONE after returning to IDLE from ALREADY_PLURAL", View.GONE, invalidInfoBar.visibility) + assertEquals("keyboardView must be VISIBLE after returning to IDLE from ALREADY_PLURAL", View.VISIBLE, keyboardView.visibility) + } + @Test fun `disableAutoSuggest resets buttons to commands`() { uiManager.updateUI(ScribeState.IDLE, "English", false, null, null, null, null, null) From ab94dd180bc67cf4ff2fd1dcf8f914ef344df181 Mon Sep 17 00:00:00 2001 From: Mounil Kanakhara Date: Mon, 23 Mar 2026 03:36:44 +0530 Subject: [PATCH 2/3] test: fix KeyboardUIManagerTest setup for Robolectric compatibility Signed-off-by: Mounil Kanakhara --- app/build.gradle.kts | 3 + .../scri/helpers/ui/KeyboardUIManagerTest.kt | 156 +++--------------- 2 files changed, 25 insertions(+), 134 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 788c10d05..5c73feda3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -281,10 +281,13 @@ dependencies { // ========================== testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junit5Version") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:$junit5Version") testImplementation("io.mockk:mockk:$mockkVersion") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") testImplementation("junit:junit:4.13.2") testImplementation("org.robolectric:robolectric:4.13") + testImplementation("androidx.test:core-ktx:1.6.1") + testImplementation("androidx.test.ext:junit-ktx:1.2.1") testImplementation("androidx.compose.ui:ui-test-junit4:1.8.3") testImplementation("org.jetbrains.kotlin:kotlin-test:2.0.20") testImplementation("org.mockito:mockito-core:5.12.0") diff --git a/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt b/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt index ea839617e..43fb54e21 100644 --- a/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt @@ -3,19 +3,12 @@ package be.scri.helpers.ui import android.content.Context +import android.view.LayoutInflater import android.view.View -import android.widget.Button -import android.widget.EditText -import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.TextView -import androidx.appcompat.widget.AppCompatImageView -import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.core.app.ApplicationProvider import be.scri.databinding.InputMethodViewBinding import be.scri.helpers.ui.KeyboardUIManager.KeyboardUIListener import be.scri.services.GeneralKeyboardIME.ScribeState -import be.scri.views.KeyboardView import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -35,140 +28,35 @@ class KeyboardUIManagerTest { private lateinit var listener: KeyboardUIListener private lateinit var uiManager: KeyboardUIManager - // Real Views (Robolectric handles these) - private lateinit var keyboardView: KeyboardView - private lateinit var commandOptionsBar: ConstraintLayout - private lateinit var toolbarBar: ConstraintLayout - private lateinit var translateBtn: Button - private lateinit var conjugateBtn: Button - private lateinit var pluralBtn: Button - private lateinit var translateBtnLeft: Button - private lateinit var translateBtnRight: Button - private lateinit var scribeKeyOptions: Button - private lateinit var separator2: View - private lateinit var separator3: View - private lateinit var commandBarEditText: EditText - private lateinit var promptText: TextView - private lateinit var ivInfo: AppCompatImageView - private lateinit var scribeKeyToolbar: Button - private lateinit var separator1: View - private lateinit var conjugateGridContainer: FrameLayout - private lateinit var conjugateGrid: LinearLayout - private lateinit var commandBarLayout: LinearLayout - - // Emoji Buttons - private lateinit var emojiBtnPhone1: Button - private lateinit var emojiBtnPhone2: Button - private lateinit var emojiSpacePhone: TextView - private lateinit var emojiBtnTablet1: Button - private lateinit var emojiBtnTablet2: Button - private lateinit var emojiBtnTablet3: Button - private lateinit var emojiSpaceTablet1: TextView - private lateinit var emojiSpaceTablet2: TextView - private lateinit var separator4: View - private lateinit var separator5: View - private lateinit var separator6: View - - // Invalid Info Views - private lateinit var invalidInfoBar: ConstraintLayout - private lateinit var invalidText: TextView - private lateinit var scribeKeyClose: Button - private lateinit var buttonLeft: Button - private lateinit var buttonRight: Button - private lateinit var middleTextview: TextView - private lateinit var pageIndicators: LinearLayout + // Convenience references resolved from the inflated binding + private val keyboardView get() = binding.keyboardView + private val commandOptionsBar get() = binding.commandOptionsBar + private val toolbarBar get() = binding.toolbarBar + private val invalidInfoBar get() = binding.invalidInfoBar + private val ivInfo get() = binding.ivInfo + private val scribeKeyClose get() = binding.scribeKeyClose + private val translateBtn get() = binding.translateBtn + private val translateBtnLeft get() = binding.translateBtnLeft + private val conjugateGridContainer get() = binding.conjugateGridContainer + private val commandBarEditText get() = binding.commandBar + private val emojiBtnPhone1 get() = binding.emojiBtnPhone1 + private val emojiBtnPhone2 get() = binding.emojiBtnPhone2 + private val emojiBtnTablet1 get() = binding.emojiBtnTablet1 + private val emojiBtnTablet2 get() = binding.emojiBtnTablet2 + private val emojiBtnTablet3 get() = binding.emojiBtnTablet3 @Before fun setUp() { context = ApplicationProvider.getApplicationContext() - // Initialize Real Views - keyboardView = KeyboardView(context, null) - commandOptionsBar = ConstraintLayout(context) - toolbarBar = ConstraintLayout(context) - translateBtn = Button(context) - conjugateBtn = Button(context) - pluralBtn = Button(context) - translateBtnLeft = Button(context) - translateBtnRight = Button(context) - scribeKeyOptions = Button(context) - separator2 = View(context) - separator3 = View(context) - commandBarEditText = EditText(context) - promptText = TextView(context) - ivInfo = AppCompatImageView(context) - scribeKeyToolbar = Button(context) - separator1 = View(context) - conjugateGridContainer = FrameLayout(context) - conjugateGrid = LinearLayout(context) - commandBarLayout = LinearLayout(context) - - emojiBtnPhone1 = Button(context) - emojiBtnPhone2 = Button(context) - emojiSpacePhone = TextView(context) - emojiBtnTablet1 = Button(context) - emojiBtnTablet2 = Button(context) - emojiBtnTablet3 = Button(context) - emojiSpaceTablet1 = TextView(context) - emojiSpaceTablet2 = TextView(context) - separator4 = View(context) - separator5 = View(context) - separator6 = View(context) - - invalidInfoBar = ConstraintLayout(context) - invalidText = TextView(context) - scribeKeyClose = Button(context) - buttonLeft = Button(context) - buttonRight = Button(context) - middleTextview = TextView(context) - pageIndicators = LinearLayout(context) - - // Mock Binding to return our Real Views - binding = mockk(relaxed = true) - every { binding.root } returns ConstraintLayout(context) - every { binding.keyboardView } returns keyboardView - every { binding.commandOptionsBar } returns commandOptionsBar - every { binding.toolbarBar } returns toolbarBar - every { binding.translateBtn } returns translateBtn - every { binding.conjugateBtn } returns conjugateBtn - every { binding.pluralBtn } returns pluralBtn - every { binding.translateBtnLeft } returns translateBtnLeft - every { binding.translateBtnRight } returns translateBtnRight - every { binding.scribeKeyOptions } returns scribeKeyOptions - every { binding.separator2 } returns separator2 - every { binding.separator3 } returns separator3 - every { binding.commandBar } returns commandBarEditText - every { binding.promptText } returns promptText - every { binding.ivInfo } returns ivInfo - every { binding.scribeKeyToolbar } returns scribeKeyToolbar - every { binding.separator1 } returns separator1 - every { binding.conjugateGridContainer } returns conjugateGridContainer - every { binding.conjugateGrid } returns conjugateGrid - every { binding.commandBarLayout } returns commandBarLayout - - every { binding.emojiBtnPhone1 } returns emojiBtnPhone1 - every { binding.emojiBtnPhone2 } returns emojiBtnPhone2 - every { binding.emojiSpacePhone } returns emojiSpacePhone - every { binding.emojiBtnTablet1 } returns emojiBtnTablet1 - every { binding.emojiBtnTablet2 } returns emojiBtnTablet2 - every { binding.emojiBtnTablet3 } returns emojiBtnTablet3 - every { binding.emojiSpaceTablet1 } returns emojiSpaceTablet1 - every { binding.emojiSpaceTablet2 } returns emojiSpaceTablet2 - every { binding.separator4 } returns separator4 - every { binding.separator5 } returns separator5 - every { binding.separator6 } returns separator6 - - every { binding.invalidInfoBar } returns invalidInfoBar - every { binding.invalidText } returns invalidText - every { binding.scribeKeyClose } returns scribeKeyClose - every { binding.buttonLeft } returns buttonLeft - every { binding.buttonRight } returns buttonRight - every { binding.middleTextview } returns middleTextview - every { binding.pageIndicators } returns pageIndicators + // Inflate the real layout — Robolectric handles Android view inflation natively. + // This avoids MockK's inability to stub generated ViewBinding field accessors + // in Robolectric's sandboxed classloader. + binding = InputMethodViewBinding.inflate(LayoutInflater.from(context)) // Mock Listener listener = mockk(relaxed = true) - every { listener.getKeyboardLayoutXML() } returns 0 + every { listener.getKeyboardLayoutXML() } returns be.scri.R.xml.keys_letters_english every { listener.onKeyboardActionListener() } returns mockk() // Init Manager From 65d3195380b829cad5747795bf5c2e555a06e95b Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Mon, 23 Mar 2026 16:50:34 +0100 Subject: [PATCH 3/3] Minor comment fixes --- .../scri/helpers/ui/KeyboardUIManagerTest.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt b/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt index 43fb54e21..c5818c0a7 100644 --- a/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt +++ b/app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt @@ -28,7 +28,7 @@ class KeyboardUIManagerTest { private lateinit var listener: KeyboardUIListener private lateinit var uiManager: KeyboardUIManager - // Convenience references resolved from the inflated binding + // Convenience references resolved from the inflated binding. private val keyboardView get() = binding.keyboardView private val commandOptionsBar get() = binding.commandOptionsBar private val toolbarBar get() = binding.toolbarBar @@ -170,7 +170,7 @@ class KeyboardUIManagerTest { @Test fun `invalidInfoBar is hidden and keyboard restored when returning to IDLE after info panel shown from INVALID state`() { - // Step 1: Transition to INVALID state + // Transition to INVALID state. uiManager.updateUI( currentState = ScribeState.INVALID, language = "English", @@ -184,16 +184,16 @@ class KeyboardUIManagerTest { assertEquals("Toolbar bar should be visible in INVALID state", View.VISIBLE, toolbarBar.visibility) assertEquals("Info icon should be visible in INVALID state", View.VISIBLE, ivInfo.visibility) - // Step 2: User taps ⓘ — info panel opens + // User taps ⓘ to open the info panel. ivInfo.performClick() assertEquals("invalidInfoBar should be visible after tapping info icon", View.VISIBLE, invalidInfoBar.visibility) assertEquals("keyboardView should be GONE while info panel is open", View.GONE, keyboardView.visibility) - // Step 3: User taps X (scribeKeyClose) — listener.onCloseClicked() fires, IME calls moveToIdleState() -> updateUI(IDLE) + // User taps X (scribeKeyClose) to simulate IME returning to IDLE. scribeKeyClose.performClick() verify { listener.onCloseClicked() } - // Simulate what the IME does after onCloseClicked(): transition UI to IDLE + // Simulate what the IME does after onCloseClicked(): Transition UI to IDLE. uiManager.updateUI( currentState = ScribeState.IDLE, language = "English", @@ -205,7 +205,7 @@ class KeyboardUIManagerTest { currentVerbForConjugation = null, ) - // Step 4: Assert – no orphaned UI elements remain + // Assert no orphaned UI elements remain. assertEquals("invalidInfoBar must be GONE after returning to IDLE", View.GONE, invalidInfoBar.visibility) assertEquals("keyboardView must be VISIBLE after returning to IDLE", View.VISIBLE, keyboardView.visibility) assertEquals("commandOptionsBar must be VISIBLE in IDLE state", View.VISIBLE, commandOptionsBar.visibility) @@ -213,7 +213,7 @@ class KeyboardUIManagerTest { @Test fun `invalidInfoBar is hidden and keyboard restored when returning to IDLE after info panel shown from ALREADY_PLURAL state`() { - // Step 1: Transition to ALREADY_PLURAL state + // Transition to ALREADY_PLURAL state. uiManager.updateUI( currentState = ScribeState.ALREADY_PLURAL, language = "English", @@ -226,12 +226,12 @@ class KeyboardUIManagerTest { ) assertEquals("Info icon should be visible in ALREADY_PLURAL state", View.VISIBLE, ivInfo.visibility) - // Step 2: User taps ⓘ — info panel opens + // User taps ⓘ to open the info panel. ivInfo.performClick() assertEquals("invalidInfoBar should be visible after tapping info icon", View.VISIBLE, invalidInfoBar.visibility) assertEquals("keyboardView should be GONE while info panel is open", View.GONE, keyboardView.visibility) - // Step 3: User taps X — simulate IME returning to IDLE + // User taps X (scribeKeyClose) to simulate IME returning to IDLE. uiManager.updateUI( currentState = ScribeState.IDLE, language = "English", @@ -243,7 +243,7 @@ class KeyboardUIManagerTest { currentVerbForConjugation = null, ) - // Step 4: Assert – no orphaned UI elements remain + // Assert that no orphaned UI elements remain. assertEquals("invalidInfoBar must be GONE after returning to IDLE from ALREADY_PLURAL", View.GONE, invalidInfoBar.visibility) assertEquals("keyboardView must be VISIBLE after returning to IDLE from ALREADY_PLURAL", View.VISIBLE, keyboardView.visibility) }