From 2da6656fc0d184d708e691a0fb3450f1a1c7d22e Mon Sep 17 00:00:00 2001 From: G Date: Sat, 29 Aug 2026 16:13:23 +0530 Subject: [PATCH 1/2] refactor: standardize conjugate label language checks using ScribeLanguage enum (#426) --- .../be/scri/helpers/ui/KeyboardUIManager.kt | 6 +- .../be/scri/ui/screens/ConjugateViewModel.kt | 5 +- .../scri/ui/screens/ConjugateViewModelTest.kt | 55 ++++++ .../scri/helpers/ui/KeyboardUIManagerTest.kt | 181 ++++++++++++++++++ 4 files changed, 243 insertions(+), 4 deletions(-) diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index 563a1656..7a78353d 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -40,6 +40,7 @@ import be.scri.helpers.english.ENInterfaceVariables.ALREADY_PLURAL_MSG import be.scri.helpers.getCategoryIconRes import be.scri.helpers.getRecentEmojis import be.scri.helpers.parseRawEmojiSpecsFile +import be.scri.models.ScribeLanguage import be.scri.models.ScribeState import be.scri.services.GeneralKeyboardIME import be.scri.views.KeyboardView @@ -418,11 +419,12 @@ class KeyboardUIManager( languageOutput?.get(title)?.toList() ?: listOf("", "", "", "") } + val scribeLanguage = ScribeLanguage.fromDisplayName(language) val layoutResId = when { isSubSelection -> R.layout.conjugate_grid_2x1 - language == "English" && forms.size <= 4 -> R.layout.conjugate_grid_2x2 - language in listOf("Russian", "Swedish") && forms.size <= 4 -> R.layout.conjugate_grid_2x2 + scribeLanguage == ScribeLanguage.ENGLISH && forms.size <= 4 -> R.layout.conjugate_grid_2x2 + scribeLanguage in listOf(ScribeLanguage.RUSSIAN, ScribeLanguage.SWEDISH) && forms.size <= 4 -> R.layout.conjugate_grid_2x2 forms.size > 4 -> R.layout.conjugate_grid_3x2 else -> R.layout.conjugate_grid_2x2 } diff --git a/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt b/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt index 2c27f2a7..c0a92788 100644 --- a/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope import be.scri.helpers.DatabaseFileManager import be.scri.helpers.data.getInfinitiveColumnName import be.scri.helpers.data.tableExists +import be.scri.models.ScribeLanguage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -68,7 +69,7 @@ class ConjugateViewModel( * Returns a list of language aliases that have been downloaded (i.e. conjugate database exists). */ fun getDownloadedLanguages(): List { - val aliases = listOf("EN", "FR", "DE", "IT", "PT", "RU", "ES", "SV") + val aliases = ScribeLanguage.entries.map { it.isoCode } return aliases.filter { alias -> val dbName = "${alias}ConjugateData.sqlite" getApplication().getDatabasePath(dbName).exists() @@ -115,7 +116,7 @@ class ConjugateViewModel( viewModelScope.launch(Dispatchers.IO) { val results = mutableListOf() val fileManager = DatabaseFileManager(getApplication()) - val aliases = listOf("EN", "FR", "DE", "IT", "PT", "RU", "ES", "SV") + val aliases = ScribeLanguage.entries.map { it.isoCode } for (alias in aliases) { val db = fileManager.getConjugateDatabase(alias) ?: continue diff --git a/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt b/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt index ed74c2ca..63f7ecb0 100644 --- a/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt +++ b/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt @@ -8,6 +8,7 @@ import android.content.SharedPreferences import io.mockk.every import io.mockk.mockk import io.mockk.verify +import java.io.File import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -98,4 +99,58 @@ class ConjugateViewModelTest { assertEquals(ConjugateSearchResult("essere", "IT"), list[0]) assertEquals(ConjugateSearchResult("parler", "FR"), list[1]) } + + @Test + fun getDownloadedLanguages_AllDatabasesExist_ReturnsAllEightAliasesInExpectedOrder() { + // Arrange + val expectedAliases = listOf("EN", "FR", "DE", "IT", "PT", "RU", "ES", "SV") + every { application.getDatabasePath(any()) } answers { + val file = mockk() + every { file.exists() } returns true + file + } + val viewModel = ConjugateViewModel(application) + + // Act + val actualLanguages = viewModel.getDownloadedLanguages() + + // Assert + assertEquals(expectedAliases, actualLanguages) + assertEquals(8, actualLanguages.size) + } + + @Test + fun getDownloadedLanguages_PartialDatabasesExist_ReturnsMatchingAliases() { + // Arrange + every { application.getDatabasePath(any()) } answers { + val dbName = firstArg() + val file = mockk() + every { file.exists() } returns (dbName == "ENConjugateData.sqlite" || dbName == "ESConjugateData.sqlite") + file + } + val viewModel = ConjugateViewModel(application) + + // Act + val actualLanguages = viewModel.getDownloadedLanguages() + + // Assert + assertEquals(listOf("EN", "ES"), actualLanguages) + } + + @Test + fun getDownloadedLanguages_NoDatabasesExist_ReturnsEmptyList() { + // Arrange + every { application.getDatabasePath(any()) } answers { + val file = mockk() + every { file.exists() } returns false + file + } + val viewModel = ConjugateViewModel(application) + + // Act + val actualLanguages = viewModel.getDownloadedLanguages() + + // Assert + assertTrue(actualLanguages.isEmpty()) + } } diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt index 4518acc6..fec0bdb3 100644 --- a/app/src/testKeyboards/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt @@ -6,6 +6,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import androidx.test.core.app.ApplicationProvider +import be.scri.R import be.scri.databinding.InputMethodViewBinding import be.scri.helpers.ui.KeyboardUIManager.KeyboardUIListener import be.scri.models.ScribeState @@ -13,6 +14,8 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -261,4 +264,182 @@ class KeyboardUIManagerTest { assertEquals(View.VISIBLE, translateBtn.visibility) assertEquals(View.INVISIBLE, translateBtnLeft.visibility) } + + @Test + fun updateUI_EnglishWithSmallForms_Inflates2x2Grid() { + // Arrange + val mockConjugateOutput = + mapOf( + "Present" to + mapOf( + "Present" to listOf("am", "are", "is", "are"), + ), + ) + + // Act + uiManager.updateUI( + currentState = ScribeState.SELECT_VERB_CONJUNCTION, + language = "English", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = mockConjugateOutput, + conjugateLabels = setOf("Present"), + selectedConjugationSubCategory = null, + currentVerbForConjugation = "be", + ) + + // Assert + assertNotNull("Button 1 should be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_1)) + assertNotNull("Button 4 should be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_4)) + assertNull("Button 5 should not be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_5)) + assertNull("Button 6 should not be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_6)) + } + + @Test + fun updateUI_RussianWithSmallForms_Inflates2x2Grid() { + // Arrange + val mockConjugateOutput = + mapOf( + "Настоящее" to + mapOf( + "Настоящее" to listOf("читаю", "читаешь", "читает"), + ), + ) + + // Act + uiManager.updateUI( + currentState = ScribeState.SELECT_VERB_CONJUNCTION, + language = "Russian", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = mockConjugateOutput, + conjugateLabels = setOf("Настоящее"), + selectedConjugationSubCategory = null, + currentVerbForConjugation = "читать", + ) + + // Assert + assertNotNull("Button 1 should be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_1)) + assertNotNull("Button 4 should be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_4)) + assertNull("Button 5 should not be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_5)) + } + + @Test + fun updateUI_SwedishWithSmallForms_Inflates2x2Grid() { + // Arrange + val mockConjugateOutput = + mapOf( + "Presens" to + mapOf( + "Presens" to listOf("är", "var", "varit"), + ), + ) + + // Act + uiManager.updateUI( + currentState = ScribeState.SELECT_VERB_CONJUNCTION, + language = "Swedish", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = mockConjugateOutput, + conjugateLabels = setOf("Presens"), + selectedConjugationSubCategory = null, + currentVerbForConjugation = "vara", + ) + + // Assert + assertNotNull("Button 1 should be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_1)) + assertNotNull("Button 4 should be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_4)) + assertNull("Button 5 should not be present in 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_5)) + } + + @Test + fun updateUI_GermanWithLargeForms_Inflates3x2Grid() { + // Arrange + val mockConjugateOutput = + mapOf( + "Präsens" to + mapOf( + "Präsens" to listOf("habe", "hast", "hat", "haben", "habt", "haben"), + ), + ) + + // Act + uiManager.updateUI( + currentState = ScribeState.SELECT_VERB_CONJUNCTION, + language = "German", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = mockConjugateOutput, + conjugateLabels = setOf("Präsens"), + selectedConjugationSubCategory = null, + currentVerbForConjugation = "haben", + ) + + // Assert + assertNotNull("Button 1 should be present in 3x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_1)) + assertNotNull("Button 5 should be present in 3x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_5)) + assertNotNull("Button 6 should be present in 3x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_6)) + } + + @Test + fun updateUI_SubSelectionActive_Prioritizes2x1Grid() { + // Arrange + val mockConjugateOutput = + mapOf( + "Present" to + mapOf( + "Singular" to listOf("I am", "You are"), + ), + ) + + // Act + uiManager.updateUI( + currentState = ScribeState.SELECT_VERB_CONJUNCTION, + language = "English", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = mockConjugateOutput, + conjugateLabels = setOf("Present"), + selectedConjugationSubCategory = "Singular", + currentVerbForConjugation = "be", + ) + + // Assert + assertNotNull("Button 1 should be present in 2x1 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_1)) + assertNotNull("Button 2 should be present in 2x1 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_2)) + assertNull("Button 3 should not be present in 2x1 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_3)) + assertNull("Button 4 should not be present in 2x1 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_4)) + assertNull("Button 5 should not be present in 2x1 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_5)) + } + + @Test + fun updateUI_UnknownLanguageString_FallsBackSafelyWithoutCrashing() { + // Arrange + val mockConjugateOutput = + mapOf( + "Present" to + mapOf( + "Present" to listOf("form1", "form2"), + ), + ) + + // Act + uiManager.updateUI( + currentState = ScribeState.SELECT_VERB_CONJUNCTION, + language = "UnknownLanguageXYZ", + emojiAutoSuggestionEnabled = false, + autoSuggestEmojis = null, + conjugateOutput = mockConjugateOutput, + conjugateLabels = setOf("Present"), + selectedConjugationSubCategory = null, + currentVerbForConjugation = "verb", + ) + + // Assert + assertEquals("Toolbar bar should be visible", View.VISIBLE, toolbarBar.visibility) + assertEquals("Conjugate grid container should be visible", View.VISIBLE, conjugateGridContainer.visibility) + assertNotNull("Button 1 should be present in fallback grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_1)) + assertNull("Button 5 should not be present in fallback 2x2 grid", binding.conjugateGrid.findViewById(R.id.conjugate_btn_5)) + } } From d21f8a644d773bcd91fa298f7aee0d051c909ea2 Mon Sep 17 00:00:00 2001 From: G Date: Sun, 30 Aug 2026 21:19:59 +0530 Subject: [PATCH 2/2] refactor: extract duplicated language alias list into shared constant, add edge-case tests (#426) --- .../be/scri/ui/screens/ConjugateViewModel.kt | 13 +-- .../scri/ui/screens/ConjugateViewModelTest.kt | 79 +++++++++++++++++++ 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt b/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt index c0a92788..6ea2d39a 100644 --- a/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt +++ b/app/src/main/java/be/scri/ui/screens/ConjugateViewModel.kt @@ -36,6 +36,10 @@ data class ConjugateSearchResult( class ConjugateViewModel( application: Application, ) : AndroidViewModel(application) { + companion object { + private val SUPPORTED_ALIASES = ScribeLanguage.entries.map { it.isoCode } + } + private val prefs = application.getSharedPreferences("scribe_conjugate_search_prefs", Context.MODE_PRIVATE) private val _searchQuery = MutableStateFlow("") @@ -68,13 +72,11 @@ class ConjugateViewModel( /** * Returns a list of language aliases that have been downloaded (i.e. conjugate database exists). */ - fun getDownloadedLanguages(): List { - val aliases = ScribeLanguage.entries.map { it.isoCode } - return aliases.filter { alias -> + fun getDownloadedLanguages(): List = + SUPPORTED_ALIASES.filter { alias -> val dbName = "${alias}ConjugateData.sqlite" getApplication().getDatabasePath(dbName).exists() } - } /** * Formats the list of downloaded languages into a user-friendly display string. @@ -116,9 +118,8 @@ class ConjugateViewModel( viewModelScope.launch(Dispatchers.IO) { val results = mutableListOf() val fileManager = DatabaseFileManager(getApplication()) - val aliases = ScribeLanguage.entries.map { it.isoCode } - for (alias in aliases) { + for (alias in SUPPORTED_ALIASES) { val db = fileManager.getConjugateDatabase(alias) ?: continue try { diff --git a/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt b/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt index 63f7ecb0..1024fee2 100644 --- a/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt +++ b/app/src/test/kotlin/be/scri/ui/screens/ConjugateViewModelTest.kt @@ -153,4 +153,83 @@ class ConjugateViewModelTest { // Assert assertTrue(actualLanguages.isEmpty()) } + + @Test + fun getDownloadedLanguages_UnrecognizedDatabasesPresent_OnlyReturnsSupportedAliases() { + // Arrange + every { application.getDatabasePath(any()) } answers { + val dbName = firstArg() + val file = mockk() + every { file.exists() } returns ( + dbName == "ENConjugateData.sqlite" || + dbName == "XYZConjugateData.sqlite" || + dbName == "UNKNOWNConjugateData.sqlite" + ) + file + } + val viewModel = ConjugateViewModel(application) + + // Act + val actualLanguages = viewModel.getDownloadedLanguages() + + // Assert + assertEquals(listOf("EN"), actualLanguages) + } + + @Test + fun getDownloadedLanguagesFormatted_FormatsCorrectlyForVariousCounts() { + // Arrange + val viewModel = ConjugateViewModel(application) + + // 0 languages + every { application.getDatabasePath(any()) } answers { + val file = mockk() + every { file.exists() } returns false + file + } + assertEquals("", viewModel.getDownloadedLanguagesFormatted()) + + // 1 language + every { application.getDatabasePath(any()) } answers { + val dbName = firstArg() + val file = mockk() + every { file.exists() } returns (dbName == "ENConjugateData.sqlite") + file + } + assertEquals("English", viewModel.getDownloadedLanguagesFormatted()) + + // 2 languages + every { application.getDatabasePath(any()) } answers { + val dbName = firstArg() + val file = mockk() + every { file.exists() } returns (dbName == "ENConjugateData.sqlite" || dbName == "FRConjugateData.sqlite") + file + } + assertEquals("English and Français", viewModel.getDownloadedLanguagesFormatted()) + + // 3 languages + every { application.getDatabasePath(any()) } answers { + val dbName = firstArg() + val file = mockk() + every { file.exists() } returns ( + dbName == "ENConjugateData.sqlite" || + dbName == "FRConjugateData.sqlite" || + dbName == "DEConjugateData.sqlite" + ) + file + } + assertEquals("English, Français and Deutsch", viewModel.getDownloadedLanguagesFormatted()) + } + + @Test + fun testLoadRecentlyConjugatedWithUnrecognizedLanguageAlias() { + every { sharedPreferences.getString("recently_conjugated_list", null) } returns "parler,XYZ;mangiare,INVALID_ALIAS;speak,EN" + + val testViewModel = ConjugateViewModel(application) + val list = testViewModel.recentlyConjugated.value + assertEquals(3, list.size) + assertEquals(ConjugateSearchResult("parler", "XYZ"), list[0]) + assertEquals(ConjugateSearchResult("mangiare", "INVALID_ALIAS"), list[1]) + assertEquals(ConjugateSearchResult("speak", "EN"), list[2]) + } }