-
Notifications
You must be signed in to change notification settings - Fork 123
Non-null assertion cleanup PART 1 #706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
67c210b
a2b17f8
038e3ee
8ed3aba
a5b3da4
c82789c
ab7953f
1ba26c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,21 +3,24 @@ package be.scri.helpers | |
|
|
||
| import android.content.Context | ||
|
|
||
| private const val PREFS_NAME = "recent_emojis" | ||
| private const val KEY_RECENT = "recent_emoji_list" | ||
| const val PREFS_NAME = "recent_emojis" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider making |
||
| const val KEY_RECENT = "recent_emoji_list" | ||
| private const val MAX_RECENT = 30 | ||
|
|
||
| fun recordRecentEmoji( | ||
| context: Context, | ||
| emoji: String, | ||
| ) { | ||
| val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) | ||
|
|
||
| val recentEmojis = prefs.getString(KEY_RECENT, "") ?: "" | ||
|
|
||
| val current = | ||
| prefs | ||
| .getString(KEY_RECENT, "")!! | ||
| recentEmojis | ||
| .split(",") | ||
| .filter { it.isNotBlank() } | ||
| .toMutableList() | ||
|
|
||
| current.remove(emoji) | ||
| current.add(0, emoji) | ||
| while (current.size > MAX_RECENT) current.removeAt(current.lastIndex) | ||
|
|
@@ -26,5 +29,6 @@ fun recordRecentEmoji( | |
|
|
||
| fun getRecentEmojis(context: Context): List<String> { | ||
| val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) | ||
| return prefs.getString(KEY_RECENT, "")!!.split(",").filter { it.isNotBlank() } | ||
| val recentEmojisPrefs = prefs.getString(KEY_RECENT, "") ?: "" | ||
| return recentEmojisPrefs.split(",").filter { it.isNotBlank() } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,16 +20,18 @@ fun parseRawEmojiSpecsFile( | |||||
| path: String, | ||||||
| ): MutableList<EmojiData> { | ||||||
| val emojis = mutableListOf<EmojiData>() | ||||||
| var emojiEditorList: MutableList<String>? = null | ||||||
| var emojiEditorList = mutableListOf<String>() | ||||||
| var category: String? = null | ||||||
|
|
||||||
| fun commitEmojiEditorList() { | ||||||
| emojiEditorList?.let { | ||||||
| val base = it.first() | ||||||
| val variants = it.drop(1) | ||||||
| emojis.add(EmojiData(category ?: "none", base, variants)) | ||||||
| if (emojiEditorList.isNotEmpty()) { | ||||||
| emojiEditorList.let { | ||||||
| val base = it.first() | ||||||
| val variants = it.drop(1) | ||||||
| emojis.add(EmojiData(category ?: "none", base, variants)) | ||||||
| } | ||||||
| } | ||||||
| emojiEditorList = null | ||||||
| emojiEditorList = emptyList<String>().toMutableList() | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also, inside |
||||||
| } | ||||||
|
|
||||||
| context.assets.open(path).bufferedReader().useLines { lines -> | ||||||
|
|
@@ -48,8 +50,8 @@ fun parseRawEmojiSpecsFile( | |||||
| val data = line.split(";") | ||||||
| if (data.size == 3) { | ||||||
| val emoji = data[0].trim() | ||||||
| if (emojiEditorList != null) { | ||||||
| emojiEditorList!!.add(emoji) | ||||||
| if (emojiEditorList.isNotEmpty()) { | ||||||
| emojiEditorList.add(emoji) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since
Suggested change
|
||||||
| } else { | ||||||
| emojiEditorList = mutableListOf(emoji) | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| package be.scri.helpers | ||
|
|
||
| import android.content.Context | ||
| import android.content.res.AssetManager | ||
| import be.scri.R | ||
| import io.mockk.every | ||
| import io.mockk.mockk | ||
| import org.junit.jupiter.api.Assertions.assertEquals | ||
| import org.junit.jupiter.api.Test | ||
| import java.io.ByteArrayInputStream | ||
|
|
||
| class EmojiHelperTest { | ||
| @Test | ||
| fun parseRawEmojiSpecsFile_validFile_parsesCorrectly() { | ||
| val context = mockk<Context>() | ||
| val assetManager = mockk<AssetManager>() | ||
| val specContent = | ||
| """ | ||
| # Comment line | ||
| [smileys_emotion] | ||
| 😀;; | ||
| 😃;; | ||
|
|
||
| [people_body] | ||
| 👋;; | ||
| 👋🏻;; | ||
| 👋🏼;; | ||
| """.trimIndent() | ||
| val inputStream = ByteArrayInputStream(specContent.toByteArray()) | ||
|
|
||
| every { context.assets } returns assetManager | ||
| every { assetManager.open("emoji_spec.txt") } returns inputStream | ||
|
|
||
| val result = parseRawEmojiSpecsFile(context, "emoji_spec.txt") | ||
|
|
||
| assertEquals(3, result.size) | ||
|
|
||
| assertEquals("smileys_emotion", result[0].category) | ||
| assertEquals("😀", result[0].emoji) | ||
| assertEquals(0, result[0].variants.size) | ||
|
|
||
| assertEquals("smileys_emotion", result[1].category) | ||
| assertEquals("😃", result[1].emoji) | ||
| assertEquals(0, result[1].variants.size) | ||
|
|
||
| assertEquals("people_body", result[2].category) | ||
| assertEquals("👋", result[2].emoji) | ||
| assertEquals(listOf("👋🏻", "👋🏼"), result[2].variants) | ||
| } | ||
|
|
||
| @Test | ||
| fun parseRawEmojiSpecsFile_emptyAndInvalidLines_ignoresThem() { | ||
| val context = mockk<Context>() | ||
| val assetManager = mockk<AssetManager>() | ||
| val specContent = | ||
| """ | ||
| [cat1] | ||
|
|
||
| # comment | ||
| emoji1;; | ||
|
|
||
| invalid_line | ||
| emoji2;; | ||
| """.trimIndent() | ||
| val inputStream = ByteArrayInputStream(specContent.toByteArray()) | ||
|
|
||
| every { context.assets } returns assetManager | ||
| every { assetManager.open("emoji_spec.txt") } returns inputStream | ||
|
|
||
| val result = parseRawEmojiSpecsFile(context, "emoji_spec.txt") | ||
|
|
||
| assertEquals(2, result.size) | ||
| assertEquals("emoji1", result[0].emoji) | ||
| assertEquals("emoji2", result[1].emoji) | ||
| } | ||
|
|
||
| @Test | ||
| fun parseRawEmojiSpecsFile_noCategory_defaultsToNone() { | ||
| val context = mockk<Context>() | ||
| val assetManager = mockk<AssetManager>() | ||
| val specContent = | ||
| """ | ||
| emoji1;; | ||
| """.trimIndent() | ||
| val inputStream = ByteArrayInputStream(specContent.toByteArray()) | ||
|
|
||
| every { context.assets } returns assetManager | ||
| every { assetManager.open("emoji_spec.txt") } returns inputStream | ||
|
|
||
| val result = parseRawEmojiSpecsFile(context, "emoji_spec.txt") | ||
|
|
||
| assertEquals(1, result.size) | ||
| assertEquals("none", result[0].category) | ||
| assertEquals("emoji1", result[0].emoji) | ||
| } | ||
|
|
||
| @Test | ||
| fun getCategoryIconRes_knownCategories_returnsCorrectIcon() { | ||
| assertEquals(R.drawable.ic_emoji_smileys, getCategoryIconRes("smileys_emotion")) | ||
| assertEquals(R.drawable.ic_emoji_people, getCategoryIconRes("people_body")) | ||
| assertEquals(R.drawable.ic_emoji_animals, getCategoryIconRes("animals_nature")) | ||
| assertEquals(R.drawable.ic_emoji_food, getCategoryIconRes("food_drink")) | ||
| assertEquals(R.drawable.ic_emoji_travel, getCategoryIconRes("travel_places")) | ||
| assertEquals(R.drawable.ic_emoji_activities, getCategoryIconRes("activities")) | ||
| assertEquals(R.drawable.ic_emoji_objects, getCategoryIconRes("objects")) | ||
| assertEquals(R.drawable.ic_emoji_symbols, getCategoryIconRes("symbols")) | ||
| assertEquals(R.drawable.ic_emoji_flags, getCategoryIconRes("flags")) | ||
| assertEquals(R.drawable.counter_clockwise_icon, getCategoryIconRes("recently_used")) | ||
| } | ||
|
|
||
| @Test | ||
| fun getCategoryIconRes_unknownCategory_returnsDefaultIcon() { | ||
| assertEquals(R.drawable.ic_emoji_vector, getCategoryIconRes("unknown_category")) | ||
| } | ||
|
|
||
| @Test | ||
| fun parseRawEmojiSpecsFile_firstLineIsVariant_treatsAsBase() { | ||
| val context = mockk<Context>() | ||
| val assetManager = mockk<AssetManager>() | ||
| val specContent = "\tvariant1;;\n" | ||
| val inputStream = ByteArrayInputStream(specContent.toByteArray()) | ||
|
|
||
| every { context.assets } returns assetManager | ||
| every { assetManager.open("emoji_spec.txt") } returns inputStream | ||
|
|
||
| val result = parseRawEmojiSpecsFile(context, "emoji_spec.txt") | ||
|
|
||
| assertEquals(1, result.size) | ||
| assertEquals("variant1", result[0].emoji) | ||
| assertEquals(0, result[0].variants.size) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| package be.scri.helpers | ||
|
|
||
| import android.os.Handler | ||
| import android.os.Looper | ||
| import be.scri.models.ScribeState | ||
| import io.mockk.MockKAnnotations | ||
| import io.mockk.every | ||
| import io.mockk.impl.annotations.MockK | ||
| import io.mockk.mockk | ||
| import io.mockk.slot | ||
| import io.mockk.verify | ||
| import org.junit.jupiter.api.BeforeEach | ||
| import org.junit.jupiter.api.Test | ||
|
|
||
| class AutocompletionHandlerTest { | ||
| private lateinit var looper: Looper | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| private lateinit var handler: Handler | ||
|
|
||
| @MockK(relaxed = true) | ||
| private lateinit var ime: KeyboardIMEContext | ||
| private lateinit var autocompletionHandler: AutocompletionHandler | ||
|
|
||
| @BeforeEach | ||
| fun setUp() { | ||
| MockKAnnotations.init(this) | ||
| looper = mockk<Looper>(relaxed = true) | ||
| handler = mockk<Handler>(relaxed = true) | ||
| autocompletionHandler = AutocompletionHandler(ime, handler) | ||
| } | ||
|
|
||
| @Test | ||
| fun buildCompletions_completionsMatchTypedWord_emptyResponse() { | ||
| val typedWord = "word" | ||
| val completions = listOf("word", "word") | ||
|
|
||
| val result = AutocompletionHandler.buildCompletions(typedWord, completions) | ||
|
|
||
| assert(result.isEmpty()) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency with the other test classes in this PR and to avoid depending on the |
||
| } | ||
|
|
||
| @Test | ||
| fun buildCompletions_completionsContainTypedWord_removesIt() { | ||
| val typedWord = "word" | ||
| val completions = listOf("word", "wordy") | ||
|
|
||
| val result = AutocompletionHandler.buildCompletions(typedWord, completions) | ||
|
|
||
| assert(result.size == 1) | ||
| assert(result[0] == "wordy") | ||
| } | ||
|
|
||
| @Test | ||
| fun buildCompletions_completionsContainManyMore_returnsOnlyTwo() { | ||
| val typedWord = "word" | ||
| val completions = List(10) { "word$it" } | ||
|
|
||
| val result = AutocompletionHandler.buildCompletions(typedWord, completions) | ||
|
|
||
| assert(result.size == 2) | ||
| assert(result[0] == "word0") | ||
| assert(result[1] == "word1") | ||
| } | ||
|
|
||
| @Test | ||
| fun processAutocomplete_emptyWord_clearsAutocomplete() { | ||
| every { ime.currentState } returns ScribeState.IDLE | ||
|
|
||
| autocompletionHandler.processAutocomplete("") | ||
|
|
||
| verify { ime.clearAutocomplete() } | ||
| } | ||
|
|
||
| @Test | ||
| fun processAutocomplete_inWrongState_clearsAutocomplete() { | ||
| every { ime.currentState } returns ScribeState.PLURAL | ||
|
|
||
| autocompletionHandler.processAutocomplete("word") | ||
|
|
||
| verify { ime.clearAutocomplete() } | ||
| } | ||
|
|
||
| @Test | ||
| fun processAutocomplete_withWord_updatesTypedWordSuggestion() { | ||
| val runnableSlot = slot<Runnable>() | ||
| every { handler.postDelayed(capture(runnableSlot), any()) } returns true | ||
|
|
||
| every { ime.currentState } returns ScribeState.IDLE | ||
|
|
||
| val currentWord = "word" | ||
| autocompletionHandler.processAutocomplete(currentWord) | ||
|
|
||
| runnableSlot.captured.run() | ||
| verify { ime.updateTypedWordSuggestion(currentWord) } | ||
| } | ||
|
|
||
| @Test | ||
| fun processAutocomplete_withWord_updatesAutocompleteCompletions() { | ||
| val runnableSlot = slot<Runnable>() | ||
| every { handler.postDelayed(capture(runnableSlot), any()) } returns true | ||
|
|
||
| every { ime.currentState } returns ScribeState.IDLE | ||
|
|
||
| val currentWord = "word" | ||
| autocompletionHandler.processAutocomplete(currentWord) | ||
|
|
||
| runnableSlot.captured.run() | ||
|
|
||
| verify { ime.updateAutocompleteCompletions(any()) } | ||
| } | ||
|
|
||
| @Test | ||
| fun processAutocomplete_calledMultipleTimes_removesPreviousCallbacks() { | ||
| every { ime.currentState } returns ScribeState.IDLE | ||
|
|
||
| autocompletionHandler.processAutocomplete("first") | ||
|
|
||
| autocompletionHandler.processAutocomplete("second") | ||
|
|
||
| verify(exactly = 1) { handler.removeCallbacks(any<Runnable>()) } | ||
| } | ||
|
|
||
| @Test | ||
| fun clearAutocomplete_cancelsPendingTaskAndClearsIme() { | ||
| every { ime.currentState } returns ScribeState.IDLE | ||
|
|
||
| autocompletionHandler.processAutocomplete("word") | ||
|
|
||
| autocompletionHandler.clearAutocomplete() | ||
|
|
||
| verify { handler.removeCallbacks(any<Runnable>()) } | ||
| verify { ime.clearAutocomplete() } | ||
| } | ||
|
|
||
| @Test | ||
| fun processAutocomplete_stateChangesBeforeRunnableExecutes_abortsExecution() { | ||
| val runnableSlot = slot<Runnable>() | ||
| every { handler.postDelayed(capture(runnableSlot), any()) } returns true | ||
|
|
||
| every { ime.currentState } returns ScribeState.IDLE | ||
| autocompletionHandler.processAutocomplete("word") | ||
|
|
||
| every { ime.currentState } returns ScribeState.PLURAL | ||
|
|
||
| runnableSlot.captured.run() | ||
|
|
||
| verify(exactly = 0) { ime.getAutocompletions(any(), any(), any()) } | ||
| verify(exactly = 0) { ime.updateAutocompleteCompletions(any()) } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Can omit the curly braces (
$junit5Version) to match the surrounding dependency lines.