From 57bd271d5189069df6506667b36c7f3e2f5267d3 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 11:59:07 +0200 Subject: [PATCH 01/21] feat: add auto scroll switch composable --- .../ui/setting/SettingSwitchItem.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt new file mode 100644 index 0000000..b4cf952 --- /dev/null +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt @@ -0,0 +1,30 @@ +package org.kabiri.android.usbterminal.ui.setting + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.kabiri.android.usbterminal.R + +@Composable +internal fun SettingAutoScrollItem( + enabled: Boolean, + onToggle: (Boolean) -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = stringResource(id = R.string.settings_label_auto_scroll)) + Switch(checked = enabled, onCheckedChange = onToggle) + } +} From 664b0cda0bc37b4ecccd569d0fc83b66bb292c2d Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 12:21:55 +0200 Subject: [PATCH 02/21] tests: add auto scroll setting to getFakeUserSettingRepository --- .../usbterminal/common/FrequentMocks.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt index 141f6b8..40a2fea 100644 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt @@ -7,9 +7,14 @@ import org.kabiri.android.usbterminal.model.UserSettingPreferences internal fun getFakeUserSettingRepository( onSetBaudRate: () -> Unit = {}, - fakeUserSetting: UserSettingPreferences = UserSettingPreferences(baudRate = 123) -): IUserSettingRepository { - return object: IUserSettingRepository { + onSetAutoScroll: () -> Unit = {}, + fakeUserSetting: UserSettingPreferences = + UserSettingPreferences( + baudRate = 123, + autoScroll = true, + ), +): IUserSettingRepository = + object : IUserSettingRepository { override val preferenceFlow: Flow get() = flowOf(fakeUserSetting) @@ -17,11 +22,11 @@ internal fun getFakeUserSettingRepository( onSetBaudRate() } - override suspend fun clear() {} - - override suspend fun fetchInitialPreferences(): UserSettingPreferences { - return fakeUserSetting + override suspend fun setAutoScroll(enabled: Boolean) { + onSetAutoScroll() } + override suspend fun clear() {} + + override suspend fun fetchInitialPreferences(): UserSettingPreferences = fakeUserSetting } -} \ No newline at end of file From 5c7e9bff1e9903b1a44a68fafd943d46ac6a5ae8 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 12:22:31 +0200 Subject: [PATCH 03/21] res: add auto-scroll terminal output label to settings --- app/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3477914..2b64ea5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Baud Rate Saved Enter a Number + Auto-scroll terminal output Go Forward From 4f164cf90fd33e6238a2989cbf56801507002b07 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 12:43:18 +0200 Subject: [PATCH 04/21] feat: add auto scroll preference to UserSettingRepository; move tests from instrumented directory to unit tests and adjust them to use the produce file by TemporaryFolder() rule --- .../UserSettingRepositoryAndroidTest.kt | 91 -------------- .../data/repository/UserSettingRepository.kt | 78 +++++++----- .../repository/UserSettingRepositoryTest.kt | 112 ++++++++++++++++++ 3 files changed, 159 insertions(+), 122 deletions(-) delete mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt create mode 100644 app/src/test/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryTest.kt diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt deleted file mode 100644 index 9057fbe..0000000 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.kabiri.android.usbterminal.data.repository - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStoreFile -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.kabiri.android.usbterminal.model.UserSettingPreferences - -private const val TEST_DATA_STORE_NAME = "test_data_store" -@RunWith(AndroidJUnit4::class) -internal class UserSettingRepositoryAndroidTest { - - private val testCoroutineDispatcher: TestDispatcher = StandardTestDispatcher() - private val testCoroutineScope = TestScope(testCoroutineDispatcher + Job()) - private val testContext = ApplicationProvider.getApplicationContext() - - private val testDataStore: DataStore = - PreferenceDataStoreFactory.create( - scope = testCoroutineScope, - produceFile = { testContext.preferencesDataStoreFile(TEST_DATA_STORE_NAME) } - ) - - private val repository: UserSettingRepository = UserSettingRepository(testDataStore) - - @Test - fun testFetchInitialPreferences() { - - // arrange - val expected = UserSettingPreferences() - var actual: UserSettingPreferences? = null - - // act - testCoroutineScope.runTest { - actual = repository.fetchInitialPreferences() - } - - // assert - assertThat(actual).isEqualTo(expected) - } - - @Test - fun testWriteBaudRate() { - - // arrange - val customBaudRate = 123 - val expected = UserSettingPreferences( - baudRate = customBaudRate - ) - var actual: UserSettingPreferences? = null - - // act - testCoroutineScope.runTest { - repository.setBaudRate(customBaudRate) - actual = repository.preferenceFlow.first() - } - - // assert - assertThat(actual).isEqualTo(expected) - } - - @Test - fun testClearResetsValuesToDefaults() { - - // arrange - val customBaudRate = 123 - val expected = UserSettingPreferences() - var actual: UserSettingPreferences? = null - - // act - testCoroutineScope.runTest { - repository.setBaudRate(customBaudRate) - repository.clear() - actual = repository.preferenceFlow.first() - } - - // assert - assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt b/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt index 680e0bb..d1e6f01 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt @@ -3,6 +3,7 @@ package org.kabiri.android.usbterminal.data.repository import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.stringPreferencesKey @@ -20,52 +21,67 @@ internal const val USER_SETTING_PREFERENCES_NAME = "user_setting_repository" private object PreferencesKeys { val BAUD_RATE = stringPreferencesKey("baud_rate") + val AUTO_SCROLL = booleanPreferencesKey("auto_scroll") } internal interface IUserSettingRepository { val preferenceFlow: Flow + suspend fun setBaudRate(baudRate: Int) + + suspend fun setAutoScroll(enabled: Boolean) + suspend fun clear() + suspend fun fetchInitialPreferences(): UserSettingPreferences } internal class UserSettingRepository -@Inject constructor( - private val dataStore: DataStore -): IUserSettingRepository { - override val preferenceFlow: Flow = dataStore.data - .catch { exception -> - // dataStore.data throws an IOException when an error is encountered when reading data - if (exception is IOException) { - Log.e(TAG, "Error reading preferences.", exception) - emit(emptyPreferences()) - } else { - throw exception + @Inject + constructor( + private val dataStore: DataStore, + ) : IUserSettingRepository { + override val preferenceFlow: Flow = + dataStore.data + .catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + Log.e(TAG, "Error reading preferences.", exception) + emit(emptyPreferences()) + } else { + throw exception + } + }.map { preferences -> + mapPreferences(preferences) + } + + override suspend fun setBaudRate(baudRate: Int) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.BAUD_RATE] = baudRate.toString() + } + } + + override suspend fun setAutoScroll(enabled: Boolean) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.AUTO_SCROLL] = enabled } - }.map { preferences -> - mapPreferences(preferences) } - override suspend fun setBaudRate(baudRate: Int) { - dataStore.edit { preferences -> - preferences[PreferencesKeys.BAUD_RATE] = baudRate.toString() + override suspend fun clear() { + dataStore.edit { preferences -> + preferences[PreferencesKeys.BAUD_RATE] = defaultBaudRate.toString() + preferences[PreferencesKeys.AUTO_SCROLL] = true + } } - } - override suspend fun clear() { - dataStore.edit { preferences -> - preferences[PreferencesKeys.BAUD_RATE] = defaultBaudRate.toString() + private fun mapPreferences(preferences: Preferences): UserSettingPreferences { + // Get the user settings from preferences + // and convert it to a [UserSettingPreferences] object + return UserSettingPreferences( + baudRate = preferences[PreferencesKeys.BAUD_RATE]?.toIntOrNull() ?: defaultBaudRate, + autoScroll = preferences[PreferencesKeys.AUTO_SCROLL] ?: true, + ) } - } - private fun mapPreferences(preferences: Preferences): UserSettingPreferences { - // Get the user settings from preferences - // and convert it to a [UserSettingPreferences] object - return UserSettingPreferences( - baudRate = preferences[PreferencesKeys.BAUD_RATE]?.toIntOrNull() ?: defaultBaudRate - ) + override suspend fun fetchInitialPreferences(): UserSettingPreferences = mapPreferences(dataStore.data.first().toPreferences()) } - - override suspend fun fetchInitialPreferences(): UserSettingPreferences = - mapPreferences(dataStore.data.first().toPreferences()) -} \ No newline at end of file diff --git a/app/src/test/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryTest.kt new file mode 100644 index 0000000..210c1bc --- /dev/null +++ b/app/src/test/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryTest.kt @@ -0,0 +1,112 @@ +package org.kabiri.android.usbterminal.data.repository + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.kabiri.android.usbterminal.model.UserSettingPreferences + +private const val TEST_DATA_STORE_NAME = "test_data_store.preferences_pb" + +@OptIn(ExperimentalCoroutinesApi::class) +internal class UserSettingRepositoryTest { + private val dispatcher: CoroutineDispatcher = UnconfinedTestDispatcher() + + @get:Rule + val tempDir = TemporaryFolder() + + private lateinit var testDataStore: DataStore + + private lateinit var repository: UserSettingRepository + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + tempDir.create() + testDataStore = + PreferenceDataStoreFactory.create( + scope = CoroutineScope(dispatcher), + produceFile = { tempDir.newFile(TEST_DATA_STORE_NAME) }, + ) + repository = UserSettingRepository(testDataStore) + } + + @After + fun cleanUp() { + tempDir.delete() + } + + @Test + fun testFetchInitialPreferences() = + runTest { + // arrange + val expected = UserSettingPreferences() + + // act + val actual = repository.fetchInitialPreferences() + + // assert + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testWriteBaudRate() = + runTest { + // arrange + val customBaudRate = 123 + val expected = + UserSettingPreferences( + baudRate = customBaudRate, + ) + + // act + repository.setBaudRate(customBaudRate) + + // assert + val actual = repository.preferenceFlow.first() + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testSetAutoScrollUpdatesPreferences() = + runTest { + // arrange + val expected = false + + // act + repository.setAutoScroll(false) + + // assert + val actual = repository.preferenceFlow.first().autoScroll + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testClearResetsValuesToDefaults() = + runTest { + // arrange + val customBaudRate = 123 + val expected = UserSettingPreferences() + + // act + repository.setBaudRate(customBaudRate) + repository.clear() + + // assert + val actual = repository.preferenceFlow.first() + assertThat(actual).isEqualTo(expected) + } +} From be838c1939c48bc17525092b7ff2eae7218ca928 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 12:58:00 +0200 Subject: [PATCH 05/21] feat: implement GetAutoScrollUseCase and its test --- .../domain/GetAutoScrollUseCase.kt | 21 +++++++++ .../domain/GetAutoScrollUseCaseTest.kt | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt create mode 100644 app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt diff --git a/app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt b/app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt new file mode 100644 index 0000000..65e4ff6 --- /dev/null +++ b/app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt @@ -0,0 +1,21 @@ +package org.kabiri.android.usbterminal.domain + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository +import javax.inject.Inject + +internal interface IGetAutoScrollUseCase { + operator fun invoke(): Flow +} + +internal class GetAutoScrollUseCase + @Inject + constructor( + private val userSettingRepository: IUserSettingRepository, + ) : IGetAutoScrollUseCase { + override fun invoke(): Flow { + val userSettingFlow = userSettingRepository.preferenceFlow + return userSettingFlow.map { it.autoScroll } + } + } diff --git a/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt new file mode 100644 index 0000000..04baaaf --- /dev/null +++ b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt @@ -0,0 +1,45 @@ +package org.kabiri.android.usbterminal.domain + +import com.google.common.truth.Truth.assertThat +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository +import org.kabiri.android.usbterminal.model.UserSettingPreferences + +internal class GetAutoScrollUseCaseTest { + private lateinit var sut: GetAutoScrollUseCase + + private val mockUserSettingRepository: IUserSettingRepository = mockk() + + @Before + internal fun setup() { + sut = + GetAutoScrollUseCase( + userSettingRepository = mockUserSettingRepository, + ) + } + + @Test + fun testGetAutoScrollUseCaseReturns() = + runTest { + // arrange + val expected = false + mockUserSettingRepository.apply { + coEvery { preferenceFlow } returns + flowOf( + UserSettingPreferences(autoScroll = expected), + ) + } + + // act + val actual = sut().first() + + // assert + assertThat(actual).isEqualTo(expected) + } +} From e61835218d63e27cd1c066bb05d06f165f340198 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:02:52 +0200 Subject: [PATCH 06/21] feat: implement SetAutoScrollUseCase and its test --- .../domain/SetAutoScrollUseCase.kt | 24 ++++++++ .../domain/SetAutoScrollUseCaseTest.kt | 59 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 app/src/main/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCase.kt create mode 100644 app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt diff --git a/app/src/main/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCase.kt b/app/src/main/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCase.kt new file mode 100644 index 0000000..cab8a82 --- /dev/null +++ b/app/src/main/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCase.kt @@ -0,0 +1,24 @@ +package org.kabiri.android.usbterminal.domain + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository +import javax.inject.Inject + +internal fun interface ISetAutoScrollUseCase { + operator fun invoke(enabled: Boolean) +} + +internal class SetAutoScrollUseCase + @Inject + constructor( + private val userSettingRepository: IUserSettingRepository, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO), + ) : ISetAutoScrollUseCase { + override fun invoke(enabled: Boolean) { + scope.launch { + userSettingRepository.setAutoScroll(enabled) + } + } + } diff --git a/app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt new file mode 100644 index 0000000..d0e764a --- /dev/null +++ b/app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt @@ -0,0 +1,59 @@ +package org.kabiri.android.usbterminal.domain + +import com.google.common.truth.Truth.assertThat +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository +import org.kabiri.android.usbterminal.model.UserSettingPreferences + +internal class SetAutoScrollUseCaseTest { + private lateinit var sut: SetAutoScrollUseCase + + private val mockUserSettingRepository: IUserSettingRepository = mockk() + + @Before + internal fun setup() { + sut = + SetAutoScrollUseCase( + userSettingRepository = mockUserSettingRepository, + ) + } + + @Test + fun `test setAutoScrollUseCase calls setAutoScroll with true on repository`() = + runTest { + // arrange + val expected = true + mockUserSettingRepository.apply { + coEvery { setAutoScroll(expected) } returns Unit + } + + // act + sut(expected) + + // assert + coVerify(exactly = 1) { mockUserSettingRepository.setAutoScroll(expected) } + } + + @Test + fun `test setAutoScrollUseCase calls setAutoScroll with false on repository`() = + runTest { + // arrange + val expected = false + mockUserSettingRepository.apply { + coEvery { setAutoScroll(expected) } returns Unit + } + + // act + sut(expected) + + // assert + coVerify(exactly = 1) { mockUserSettingRepository.setAutoScroll(expected) } + } +} From 6bd22b2c24dc317c4bfd4695ad4c8b88ba3f7f43 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:04:42 +0200 Subject: [PATCH 07/21] tests: enhance GetAutoScrollUseCaseTest with additional assertions for true/false cases --- .../domain/GetAutoScrollUseCaseTest.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt index 04baaaf..61d2ba6 100644 --- a/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt +++ b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt @@ -25,7 +25,26 @@ internal class GetAutoScrollUseCaseTest { } @Test - fun testGetAutoScrollUseCaseReturns() = + fun `test getAutoScrollUseCase returns true`() = + runTest { + // arrange + val expected = true + mockUserSettingRepository.apply { + coEvery { preferenceFlow } returns + flowOf( + UserSettingPreferences(autoScroll = expected), + ) + } + + // act + val actual = sut().first() + + // assert + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test getAutoScrollUseCase returns false`() = runTest { // arrange val expected = false From 18c9e9949f897bf3ff878d9d870977609e6a6967 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:07:15 +0200 Subject: [PATCH 08/21] feat: add providers for GetAutoScrollUseCase and SetAutoScrollUseCase in AppModule --- .../android/usbterminal/di/AppModule.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/di/AppModule.kt b/app/src/main/java/org/kabiri/android/usbterminal/di/AppModule.kt index 766af90..4833d1c 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/di/AppModule.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/di/AppModule.kt @@ -23,11 +23,15 @@ import org.kabiri.android.usbterminal.data.repository.USER_SETTING_PREFERENCES_N import org.kabiri.android.usbterminal.data.repository.UsbRepository import org.kabiri.android.usbterminal.data.repository.UserSettingRepository import org.kabiri.android.usbterminal.domain.ArduinoUseCase +import org.kabiri.android.usbterminal.domain.GetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.GetCustomBaudRateUseCase import org.kabiri.android.usbterminal.domain.IArduinoUseCase +import org.kabiri.android.usbterminal.domain.IGetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.IGetCustomBaudRateUseCase +import org.kabiri.android.usbterminal.domain.ISetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.ISetCustomBaudRateUseCase import org.kabiri.android.usbterminal.domain.IUsbUseCase +import org.kabiri.android.usbterminal.domain.SetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.SetCustomBaudRateUseCase import org.kabiri.android.usbterminal.domain.UsbUseCase import org.kabiri.android.usbterminal.util.IResourceProvider @@ -131,4 +135,18 @@ internal class AppModule { ): ISetCustomBaudRateUseCase { return SetCustomBaudRateUseCase(userSettingRepository = userSettingRepository) } -} \ No newline at end of file + + @Provides + fun provideGetAutoScrollUseCase( + userSettingRepository: IUserSettingRepository, + ): IGetAutoScrollUseCase { + return GetAutoScrollUseCase(userSettingRepository = userSettingRepository) + } + + @Provides + fun provideSetAutoScrollUseCase( + userSettingRepository: IUserSettingRepository, + ): ISetAutoScrollUseCase { + return SetAutoScrollUseCase(userSettingRepository = userSettingRepository) + } +} From 6bb57000738fd3906e8a3990f2a6ebf83d35f824 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:08:04 +0200 Subject: [PATCH 09/21] feat: add autoScroll preference to UserSettingPreferences data class --- .../android/usbterminal/model/UserSettingPreferences.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt b/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt index 75637ea..f4f8722 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt @@ -3,5 +3,6 @@ package org.kabiri.android.usbterminal.model internal const val defaultBaudRate: Int = 9600 data class UserSettingPreferences( - val baudRate: Int = defaultBaudRate // Arduino default -) \ No newline at end of file + val baudRate: Int = defaultBaudRate, // Arduino default + val autoScroll: Boolean = true, +) From 795acffcdcb4582fad805ce68312300ae208290d Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:10:59 +0200 Subject: [PATCH 10/21] test: add cleanup method --- .../android/usbterminal/domain/GetAutoScrollUseCaseTest.kt | 7 +++++++ .../android/usbterminal/domain/SetAutoScrollUseCaseTest.kt | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt index 61d2ba6..89b9909 100644 --- a/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt +++ b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCaseTest.kt @@ -1,11 +1,13 @@ package org.kabiri.android.usbterminal.domain import com.google.common.truth.Truth.assertThat +import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository @@ -24,6 +26,11 @@ internal class GetAutoScrollUseCaseTest { ) } + @After + internal fun cleanUp() { + clearAllMocks() + } + @Test fun `test getAutoScrollUseCase returns true`() = runTest { diff --git a/app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt index d0e764a..afcc431 100644 --- a/app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt +++ b/app/src/test/java/org/kabiri/android/usbterminal/domain/SetAutoScrollUseCaseTest.kt @@ -1,12 +1,14 @@ package org.kabiri.android.usbterminal.domain import com.google.common.truth.Truth.assertThat +import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository @@ -25,6 +27,11 @@ internal class SetAutoScrollUseCaseTest { ) } + @After + internal fun cleanUp() { + clearAllMocks() + } + @Test fun `test setAutoScrollUseCase calls setAutoScroll with true on repository`() = runTest { From 398efae86891cb35f9d3b5da9bf1fffe2a4b4e28 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:29:26 +0200 Subject: [PATCH 11/21] feat: add autoScroll functionality to SettingViewModel and add tests for it --- .../ui/setting/SettingViewModel.kt | 29 +++-- .../viewmodel/SettingViewModelTest.kt | 105 ++++++++++++++++++ 2 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt index 937b09b..9f82812 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt @@ -3,7 +3,9 @@ package org.kabiri.android.usbterminal.ui.setting import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow +import org.kabiri.android.usbterminal.domain.IGetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.IGetCustomBaudRateUseCase +import org.kabiri.android.usbterminal.domain.ISetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.ISetCustomBaudRateUseCase import org.kabiri.android.usbterminal.model.defaultBaudRate import javax.inject.Inject @@ -13,17 +15,24 @@ import javax.inject.Inject */ @HiltViewModel internal class SettingViewModel -@Inject constructor( - private val getBaudRate: IGetCustomBaudRateUseCase, - private val setBaudRate: ISetCustomBaudRateUseCase, -): ViewModel() { + @Inject + constructor( + private val getBaudRate: IGetCustomBaudRateUseCase, + private val setBaudRate: ISetCustomBaudRateUseCase, + private val getAutoScroll: IGetAutoScrollUseCase, + private val setAutoScroll: ISetAutoScrollUseCase, + ) : ViewModel() { + val currentBaudRate: Flow + get() = getBaudRate() - val currentBaudRate: Flow - get() = getBaudRate() + fun setNewBaudRate(baudRate: Int) = setBaudRate(baudRate) - fun setNewBaudRate(baudRate: Int) = setBaudRate(baudRate) + val currentAutoScroll: Flow + get() = getAutoScroll() - fun resetDefault() { - setBaudRate(defaultBaudRate) + fun setAutoScrollEnabled(enabled: Boolean) = setAutoScroll(enabled) + + fun resetDefault() { + setBaudRate(defaultBaudRate) + } } -} \ No newline at end of file diff --git a/app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt new file mode 100644 index 0000000..dc566de --- /dev/null +++ b/app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt @@ -0,0 +1,105 @@ +package org.kabiri.android.usbterminal.viewmodel + +import com.google.common.truth.Truth.assertThat +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.kabiri.android.usbterminal.domain.IGetAutoScrollUseCase +import org.kabiri.android.usbterminal.domain.IGetCustomBaudRateUseCase +import org.kabiri.android.usbterminal.domain.ISetAutoScrollUseCase +import org.kabiri.android.usbterminal.domain.ISetCustomBaudRateUseCase +import org.kabiri.android.usbterminal.ui.setting.SettingViewModel + +internal class SettingViewModelTest { + private val mockGetBaudRate: IGetCustomBaudRateUseCase = mockk() + private val mockSetBaudRate: ISetCustomBaudRateUseCase = mockk() + private val mockGetAutoScroll: IGetAutoScrollUseCase = mockk() + private val mockSetAutoScroll: ISetAutoScrollUseCase = mockk() + + private lateinit var sut: SettingViewModel + + @Before + internal fun setup() { + sut = + SettingViewModel( + getBaudRate = mockGetBaudRate, + setBaudRate = mockSetBaudRate, + getAutoScroll = mockGetAutoScroll, + setAutoScroll = mockSetAutoScroll, + ) + } + + @After + internal fun cleanUp() { + clearAllMocks() + } + + @Test + internal fun `test currentBaudRate calls getBaudRate`() = + runTest { + // arrange + val expected = 123 + every { mockGetBaudRate() } returns flowOf(expected) + + // act + val actual = sut.currentBaudRate.first() + + // assert + assertThat(actual).isEqualTo(expected) + verify(exactly = 1) { + @Suppress("UnusedFlow") + mockGetBaudRate() + } + } + + @Test + internal fun `test setNewBaudRate calls setBaudRate`() { + // arrange + val expected = 123 + every { mockSetBaudRate(expected) } returns Unit + + // act + sut.setNewBaudRate(expected) + + // assert + verify(exactly = 1) { mockSetBaudRate(expected) } + } + + @Test + internal fun `test currentAutoScroll calls getAutoScroll`() = + runTest { + // arrange + val expected = true + every { mockGetAutoScroll() } returns flowOf(expected) + + // act + val actual = sut.currentAutoScroll.first() + + // assert + assertThat(actual).isEqualTo(expected) + verify(exactly = 1) { + @Suppress("UnusedFlow") + mockGetAutoScroll() + } + } + + @Test + internal fun `test setAutoScrollEnabled calls setAutoScroll`() { + // arrange + val expected = true + every { mockSetAutoScroll(expected) } returns Unit + + // act + sut.setAutoScrollEnabled(expected) + + // assert + verify(exactly = 1) { mockSetAutoScroll(expected) } + } +} From e69868a267c573e798374854ea93dd3bb80b5e7e Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:31:12 +0200 Subject: [PATCH 12/21] feat: add auto-scroll toggle to settings UI --- .../usbterminal/ui/setting/SettingContent.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt index 94de228..104ac1b 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt @@ -17,16 +17,22 @@ internal fun SettingContent( onDismiss: () -> Unit, ) { val currentBaudRate by settingViewModel.currentBaudRate.collectAsState(defaultBaudRate) + val autoScrollEnabled by settingViewModel.currentAutoScroll.collectAsState(true) Column(modifier = modifier) { - // Settings Header SettingsHeader() + // Auto-scroll Setting + SettingAutoScrollItem( + enabled = autoScrollEnabled, + onToggle = settingViewModel::setAutoScrollEnabled, + ) + // Baud Rate Setting SettingValueItem( currentValue = currentBaudRate, - onNewValue = settingViewModel::setNewBaudRate + onNewValue = settingViewModel::setNewBaudRate, ) // Reset Default Button @@ -35,10 +41,9 @@ internal fun SettingContent( settingViewModel.resetDefault() onDismiss() }, - onClickDismiss = onDismiss + onClickDismiss = onDismiss, ) Spacer(modifier = Modifier.height(8.dp)) } } - From 9ecf7b2bbbffde3f88543a9856a7418b8f0e272d Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:31:20 +0200 Subject: [PATCH 13/21] feat: implement auto-scroll functionality in MainActivity based on settings --- .../java/org/kabiri/android/usbterminal/MainActivity.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt b/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt index 2b3f1a4..c722126 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt @@ -52,12 +52,19 @@ class MainActivity : AppCompatActivity() { // make the text view scrollable: tvOutput.movementMethod = ScrollingMovementMethod() + var autoScrollEnabled = true + lifecycleScope.launch { + settingViewModel.currentAutoScroll.collect { enabled -> + autoScrollEnabled = enabled + } + } + lifecycleScope.launch { viewModel.getLiveOutput() viewModel.output.collect { tvOutput.apply { text = it - scrollToLastLine() + if (autoScrollEnabled) scrollToLastLine() } } } From cb894e62fada41ee1f0642add0d58085779eec20 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:37:29 +0200 Subject: [PATCH 14/21] feat: set default auto-scroll value when the user clicks on default button in the settings; replace default values with constants for baud rate and auto-scroll; update tests --- .../data/repository/ArduinoRepository.kt | 4 ++-- .../data/repository/UserSettingRepository.kt | 6 +++--- .../usbterminal/model/UserSettingPreferences.kt | 7 ++++--- .../usbterminal/ui/setting/SettingContent.kt | 4 ++-- .../usbterminal/ui/setting/SettingViewModel.kt | 6 ++++-- .../viewmodel/SettingViewModelTest.kt | 16 ++++++++++++++++ 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/data/repository/ArduinoRepository.kt b/app/src/main/java/org/kabiri/android/usbterminal/data/repository/ArduinoRepository.kt index 70ddf55..d05ec02 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/data/repository/ArduinoRepository.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/data/repository/ArduinoRepository.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.kabiri.android.usbterminal.R import org.kabiri.android.usbterminal.domain.IGetCustomBaudRateUseCase -import org.kabiri.android.usbterminal.model.defaultBaudRate +import org.kabiri.android.usbterminal.model.DEFAULT_BAUD_RATE import javax.inject.Inject /** @@ -44,7 +44,7 @@ internal class ArduinoRepository private val getBaudRate: IGetCustomBaudRateUseCase, ): IArduinoRepository { - private var currentBaudRate = defaultBaudRate // Default value + private var currentBaudRate = DEFAULT_BAUD_RATE // Default value private val _messageFlow = MutableStateFlow("") override val messageFlow: Flow diff --git a/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt b/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt index d1e6f01..57fd6cb 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepository.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import org.kabiri.android.usbterminal.model.UserSettingPreferences -import org.kabiri.android.usbterminal.model.defaultBaudRate +import org.kabiri.android.usbterminal.model.DEFAULT_BAUD_RATE import java.io.IOException import javax.inject.Inject @@ -69,7 +69,7 @@ internal class UserSettingRepository override suspend fun clear() { dataStore.edit { preferences -> - preferences[PreferencesKeys.BAUD_RATE] = defaultBaudRate.toString() + preferences[PreferencesKeys.BAUD_RATE] = DEFAULT_BAUD_RATE.toString() preferences[PreferencesKeys.AUTO_SCROLL] = true } } @@ -78,7 +78,7 @@ internal class UserSettingRepository // Get the user settings from preferences // and convert it to a [UserSettingPreferences] object return UserSettingPreferences( - baudRate = preferences[PreferencesKeys.BAUD_RATE]?.toIntOrNull() ?: defaultBaudRate, + baudRate = preferences[PreferencesKeys.BAUD_RATE]?.toIntOrNull() ?: DEFAULT_BAUD_RATE, autoScroll = preferences[PreferencesKeys.AUTO_SCROLL] ?: true, ) } diff --git a/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt b/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt index f4f8722..43403ff 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/model/UserSettingPreferences.kt @@ -1,8 +1,9 @@ package org.kabiri.android.usbterminal.model -internal const val defaultBaudRate: Int = 9600 +internal const val DEFAULT_BAUD_RATE: Int = 9600 +internal const val DEFAULT_AUTO_SCROLL: Boolean = true data class UserSettingPreferences( - val baudRate: Int = defaultBaudRate, // Arduino default - val autoScroll: Boolean = true, + val baudRate: Int = DEFAULT_BAUD_RATE, // Arduino default + val autoScroll: Boolean = DEFAULT_AUTO_SCROLL, ) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt index 104ac1b..d324852 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import org.kabiri.android.usbterminal.model.defaultBaudRate +import org.kabiri.android.usbterminal.model.DEFAULT_BAUD_RATE @Composable internal fun SettingContent( @@ -16,7 +16,7 @@ internal fun SettingContent( settingViewModel: SettingViewModel, onDismiss: () -> Unit, ) { - val currentBaudRate by settingViewModel.currentBaudRate.collectAsState(defaultBaudRate) + val currentBaudRate by settingViewModel.currentBaudRate.collectAsState(DEFAULT_BAUD_RATE) val autoScrollEnabled by settingViewModel.currentAutoScroll.collectAsState(true) Column(modifier = modifier) { diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt index 9f82812..346734d 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingViewModel.kt @@ -7,7 +7,8 @@ import org.kabiri.android.usbterminal.domain.IGetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.IGetCustomBaudRateUseCase import org.kabiri.android.usbterminal.domain.ISetAutoScrollUseCase import org.kabiri.android.usbterminal.domain.ISetCustomBaudRateUseCase -import org.kabiri.android.usbterminal.model.defaultBaudRate +import org.kabiri.android.usbterminal.model.DEFAULT_AUTO_SCROLL +import org.kabiri.android.usbterminal.model.DEFAULT_BAUD_RATE import javax.inject.Inject /** @@ -33,6 +34,7 @@ internal class SettingViewModel fun setAutoScrollEnabled(enabled: Boolean) = setAutoScroll(enabled) fun resetDefault() { - setBaudRate(defaultBaudRate) + setBaudRate(DEFAULT_BAUD_RATE) + setAutoScroll(DEFAULT_AUTO_SCROLL) } } diff --git a/app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt index dc566de..360f0b6 100644 --- a/app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt +++ b/app/src/test/java/org/kabiri/android/usbterminal/viewmodel/SettingViewModelTest.kt @@ -102,4 +102,20 @@ internal class SettingViewModelTest { // assert verify(exactly = 1) { mockSetAutoScroll(expected) } } + + @Test + internal fun `test resetDefault calls setBaudRate and setAutoScroll`() { + // arrange + val expectedBaudRate = 9600 + val expectedAutoScroll = true + every { mockSetBaudRate(expectedBaudRate) } returns Unit + every { mockSetAutoScroll(expectedAutoScroll) } returns Unit + + // act + sut.resetDefault() + + // assert + verify(exactly = 1) { mockSetBaudRate(expectedBaudRate) } + verify(exactly = 1) { mockSetAutoScroll(expectedAutoScroll) } + } } From b51a688c0d22d3464c3554df64ee3253d1cd8068 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:46:20 +0200 Subject: [PATCH 15/21] test: move Get/Set-CustomBaudRateUseCase tests to unit tests directory and adjust them to use mockk --- .../GetCustomBaudRateUseCaseAndroidTest.kt | 30 ----------- .../SetCustomBaudRateUseCaseAndroidTest.kt | 33 ------------ .../domain/GetCustomBaudRateUseCaseTest.kt | 52 +++++++++++++++++++ .../domain/SetCustomBaudRateUseCaseTest.kt | 46 ++++++++++++++++ 4 files changed, 98 insertions(+), 63 deletions(-) delete mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseAndroidTest.kt delete mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseAndroidTest.kt create mode 100644 app/src/test/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseTest.kt create mode 100644 app/src/test/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseTest.kt diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseAndroidTest.kt deleted file mode 100644 index 82a33ac..0000000 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseAndroidTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.kabiri.android.usbterminal.domain - -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.kabiri.android.usbterminal.common.getFakeUserSettingRepository -import org.kabiri.android.usbterminal.model.UserSettingPreferences - -internal class GetCustomBaudRateUseCaseAndroidTest { - - @Test - fun testGetCustomBaudRateUseCaseAndroidTestReturns() = runTest { - - // arrange - val expectedBaudRate = 1234 - val fakeUserSettings = UserSettingPreferences(baudRate = expectedBaudRate) - val fakeUserSettingRepository = - getFakeUserSettingRepository(fakeUserSetting = fakeUserSettings) - val sut = GetCustomBaudRateUseCase( - userSettingRepository = fakeUserSettingRepository - ) - - // act - val actualBaudRate = sut().first() - - // assert - assertThat(actualBaudRate).isEqualTo(expectedBaudRate) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseAndroidTest.kt deleted file mode 100644 index 6bfb814..0000000 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseAndroidTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.kabiri.android.usbterminal.domain - -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.kabiri.android.usbterminal.common.getFakeUserSettingRepository - -@OptIn(ExperimentalCoroutinesApi::class) -internal class SetCustomBaudRateUseCaseAndroidTest { - - @Test - fun testSetCustomBaudRateUseCaseAndroidTestCallsSetBaudRateOnRepository() = runTest { - - // arrange - var isCalledSetBaudRate = false - val fakeUserSettingRepository = getFakeUserSettingRepository(onSetBaudRate = { - isCalledSetBaudRate = true - }) - val sut = SetCustomBaudRateUseCase( - userSettingRepository = fakeUserSettingRepository, - scope = this, - ) - - // act - sut.invoke(123) - advanceUntilIdle() - - // assert - assertThat(isCalledSetBaudRate).isTrue() - } -} \ No newline at end of file diff --git a/app/src/test/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseTest.kt new file mode 100644 index 0000000..a4563dd --- /dev/null +++ b/app/src/test/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseTest.kt @@ -0,0 +1,52 @@ +package org.kabiri.android.usbterminal.domain + +import com.google.common.truth.Truth.assertThat +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository +import org.kabiri.android.usbterminal.model.UserSettingPreferences + +internal class GetCustomBaudRateUseCaseTest { + private lateinit var sut: GetCustomBaudRateUseCase + + private val mockUserSettingRepository: IUserSettingRepository = mockk() + + @Before + internal fun setup() { + sut = + GetCustomBaudRateUseCase( + userSettingRepository = mockUserSettingRepository, + ) + } + + @After + internal fun cleanUp() { + clearAllMocks() + } + + @Test + fun `test invoke returns expected baudRate`() = + runTest { + // arrange + val expected = 1234 + mockUserSettingRepository.apply { + coEvery { preferenceFlow } returns + flowOf( + UserSettingPreferences(baudRate = expected), + ) + } + + // act + val actualBaudRate = sut().first() + + // assert + assertThat(actualBaudRate).isEqualTo(expected) + } +} diff --git a/app/src/test/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseTest.kt b/app/src/test/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseTest.kt new file mode 100644 index 0000000..a3d409d --- /dev/null +++ b/app/src/test/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseTest.kt @@ -0,0 +1,46 @@ +package org.kabiri.android.usbterminal.domain + +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository + +internal class SetCustomBaudRateUseCaseTest { + private lateinit var sut: SetCustomBaudRateUseCase + + private val mockUserSettingRepository: IUserSettingRepository = mockk() + + @Before + internal fun setup() { + sut = + SetCustomBaudRateUseCase( + userSettingRepository = mockUserSettingRepository, + ) + } + + @After + internal fun cleanUp() { + clearAllMocks() + } + + @Test + fun `test invoke calls setBaudRate with expected value on repository`() = + runTest { + // arrange + val expected = 1234 + mockUserSettingRepository.apply { + coEvery { setBaudRate(expected) } returns Unit + } + + // act + sut(expected) + + // assert + coVerify(exactly = 1) { mockUserSettingRepository.setBaudRate(expected) } + } +} From db7935ec8ade1388e66996e7585910e47e29f498 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 13:46:43 +0200 Subject: [PATCH 16/21] refactor: remove FrequentMocks.kt --- .../usbterminal/common/FrequentMocks.kt | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt deleted file mode 100644 index 40a2fea..0000000 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.kabiri.android.usbterminal.common - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository -import org.kabiri.android.usbterminal.model.UserSettingPreferences - -internal fun getFakeUserSettingRepository( - onSetBaudRate: () -> Unit = {}, - onSetAutoScroll: () -> Unit = {}, - fakeUserSetting: UserSettingPreferences = - UserSettingPreferences( - baudRate = 123, - autoScroll = true, - ), -): IUserSettingRepository = - object : IUserSettingRepository { - override val preferenceFlow: Flow - get() = flowOf(fakeUserSetting) - - override suspend fun setBaudRate(baudRate: Int) { - onSetBaudRate() - } - - override suspend fun setAutoScroll(enabled: Boolean) { - onSetAutoScroll() - } - - override suspend fun clear() {} - - override suspend fun fetchInitialPreferences(): UserSettingPreferences = fakeUserSetting - } From 5929df85299d46935b97bd64298326f4c84621e5 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 14:07:13 +0200 Subject: [PATCH 17/21] test: add Compose tests for SettingContent to verify displayed texts --- app/build.gradle.kts | 2 + .../ui/setting/SettingContentAndroidTest.kt | 76 +++++++++++++++++++ gradle/libs.versions.toml | 2 + 3 files changed, 80 insertions(+) create mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingContentAndroidTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 15c1db3..0fe44de 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -291,6 +291,8 @@ dependencies { // Instrumented Test Libraries androidTestImplementation(composeBom) + androidTestImplementation(libs.compose.ui.test.junit4) + debugImplementation(libs.compose.ui.test.manifest) androidTestImplementation(libs.coroutines.test) androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.test.ext.junit) diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingContentAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingContentAndroidTest.kt new file mode 100644 index 0000000..4bda8bc --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingContentAndroidTest.kt @@ -0,0 +1,76 @@ +package org.kabiri.android.usbterminal.ui.setting + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import org.hamcrest.CoreMatchers.notNullValue +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.kabiri.android.usbterminal.R +import org.kabiri.android.usbterminal.ui.theme.UsbTerminalTheme + +@RunWith(AndroidJUnit4::class) +class SettingContentAndroidTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + private fun showContet(viewModel: SettingViewModel) { + composeRule.setContent { + UsbTerminalTheme { + SettingContent( + settingViewModel = viewModel, + onDismiss = {}, + ) + } + } + } + + @Test + fun settingContent_displaysExpectedTexts() { + // arrange + val context = composeRule.activity + assertThat(context, notNullValue()) + val viewModel = mockk(relaxed = true) + every { viewModel.currentBaudRate } returns flowOf(9600) + every { viewModel.currentAutoScroll } returns flowOf(true) + + // act + showContet(viewModel) + + // assert + composeRule.onNodeWithText(context.getString(R.string.settings_title)).assertIsDisplayed() + composeRule + .onNodeWithText(context.getString(R.string.settings_subtitle)) + .assertIsDisplayed() + + // Baud rate label (ensure unmerged tree so label is found) + composeRule + .onNodeWithText( + context.getString(R.string.settings_label_baud_rate), + useUnmergedTree = true, + ).assertIsDisplayed() + + // Buttons and bottom text + composeRule + .onNodeWithText(context.getString(R.string.settings_bt_reset_default)) + .assertIsDisplayed() + composeRule + .onNodeWithText(context.getString(R.string.settings_bt_dismiss_sheet)) + .assertIsDisplayed() + composeRule + .onNodeWithText(context.getString(R.string.settings_bottom_text)) + .assertIsDisplayed() + + // Auto-scroll label + composeRule + .onNodeWithText(context.getString(R.string.settings_label_auto_scroll)) + .assertIsDisplayed() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3092bf..e86624f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,6 +64,8 @@ compose-foundation = { module = "androidx.compose.foundation:foundation" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } # --- Google/Material --- material = { module = "com.google.android.material:material", version.ref = "material" } From 2b59b2eb7691a558bf207acbe1b2a7e4a83ab47e Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 14:14:32 +0200 Subject: [PATCH 18/21] fix: improve auto-scroll text in settings item --- .../usbterminal/ui/setting/SettingSwitchItem.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt index b4cf952..3752bd0 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -16,15 +18,20 @@ import org.kabiri.android.usbterminal.R internal fun SettingAutoScrollItem( enabled: Boolean, onToggle: (Boolean) -> Unit, + modifier: Modifier = Modifier, ) { Row( modifier = - Modifier + modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - Text(text = stringResource(id = R.string.settings_label_auto_scroll)) + Text( + text = stringResource(id = R.string.settings_label_auto_scroll), + color = MaterialTheme.colorScheme.onPrimary, + ) Switch(checked = enabled, onCheckedChange = onToggle) } } From 14945c36fd8ba95cb5d0cb85540f57172239ffe7 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 14:32:17 +0200 Subject: [PATCH 19/21] refactor: rename auto-scroll setting component to SettingSwitchItem for clarity --- .../org/kabiri/android/usbterminal/ui/setting/SettingContent.kt | 2 +- .../kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt index d324852..1c5bdb9 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingContent.kt @@ -24,7 +24,7 @@ internal fun SettingContent( SettingsHeader() // Auto-scroll Setting - SettingAutoScrollItem( + SettingSwitchItem( enabled = autoScrollEnabled, onToggle = settingViewModel::setAutoScrollEnabled, ) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt index 3752bd0..604a722 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItem.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp import org.kabiri.android.usbterminal.R @Composable -internal fun SettingAutoScrollItem( +internal fun SettingSwitchItem( enabled: Boolean, onToggle: (Boolean) -> Unit, modifier: Modifier = Modifier, From e8450f36bdfec9468d5b282605dd7f6dd7a61977 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 14:40:21 +0200 Subject: [PATCH 20/21] tests: add Android Compose tests for SettingSwitchItem functionality --- .../setting/SettingSwitchItemAndroidTest.kt | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItemAndroidTest.kt diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItemAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItemAndroidTest.kt new file mode 100644 index 0000000..2a95638 --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/ui/setting/SettingSwitchItemAndroidTest.kt @@ -0,0 +1,85 @@ +package org.kabiri.android.usbterminal.ui.setting + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.hasAnySibling +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isToggleable +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.kabiri.android.usbterminal.R +import org.kabiri.android.usbterminal.ui.theme.UsbTerminalTheme + +@RunWith(AndroidJUnit4::class) +class SettingSwitchItemAndroidTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + private fun showContet(enabled: Boolean = true) { + composeRule.setContent { + UsbTerminalTheme { + SettingSwitchItem( + enabled = enabled, + onToggle = {}, + modifier = Modifier.fillMaxSize(), + ) + } + } + } + + @Test + fun autoScrollSwitch_displaysLabel() { + // arrange + val context = composeRule.activity + + // act + showContet() + + // assert + composeRule + .onNodeWithText(context.getString(R.string.settings_label_auto_scroll)) + .assertIsDisplayed() + } + + @Test + fun autoScrollSwitch_isOn_whenEnabledTrue() { + // arrange + val context = composeRule.activity + + // act + showContet(enabled = true) + + // assert + composeRule + .onNode( + hasAnySibling( + hasText(context.getString(R.string.settings_label_auto_scroll)), + ).and(isToggleable()), + ).assertIsOn() + } + + @Test + fun autoScrollSwitch_isOff_whenEnabledFalse() { + // arrange + val context = composeRule.activity + + // act + showContet(enabled = false) + + // assert + composeRule + .onNode( + hasAnySibling( + hasText(context.getString(R.string.settings_label_auto_scroll)), + ).and(isToggleable()), + ).assertIsOff() + } +} From 90dd52852e0c89b0dc8b2e49aba927faabe7df42 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sat, 30 Aug 2025 14:50:14 +0200 Subject: [PATCH 21/21] refactor: change interface declaration to a fun interface for GetAutoScrollUseCase --- .../kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt b/app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt index 65e4ff6..e8a3366 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/domain/GetAutoScrollUseCase.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.map import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository import javax.inject.Inject -internal interface IGetAutoScrollUseCase { +internal fun interface IGetAutoScrollUseCase { operator fun invoke(): Flow }