From 6d944c0f8c0ca4abc9a0a01c4fb519ecb2d480cb Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Thu, 5 Feb 2026 11:54:17 -0500 Subject: [PATCH] Add tests for MemberLabelViewModel. --- .../memberlabel/MemberLabelViewModel.kt | 2 +- .../memberlabel/MemberLabelViewModelTest.kt | 221 ++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModelTest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModel.kt index 9686b654a5..ac0e39f349 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModel.kt @@ -134,7 +134,7 @@ data class MemberLabelUiState( val isSaveEnabled: Boolean get() { val isCleared = labelText.isEmpty() && labelEmoji.isEmpty() - val hasValidLabel = labelText.length >= MIN_LABEL_TEXT_LENGTH + val hasValidLabel = labelText.length in MIN_LABEL_TEXT_LENGTH..MAX_LABEL_TEXT_LENGTH return hasChanges && (hasValidLabel || isCleared) && saveState != SaveState.InProgress } diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModelTest.kt new file mode 100644 index 0000000000..2919163808 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelViewModelTest.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.groups.memberlabel + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule + +@OptIn(ExperimentalCoroutinesApi::class) +class MemberLabelViewModelTest { + private val testDispatcher = UnconfinedTestDispatcher() + + @get:Rule + val dispatcherRule = CoroutineDispatcherRule(testDispatcher) + + private val memberLabelRepo = mockk(relaxUnitFun = true) + private val recipientId = RecipientId.from(1L) + + @Test + fun `isSaveEnabled returns true when label text is different from the original value`() { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelTextChanged("Modified") + + assertTrue(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns false when label text is the same as the original value`() { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelTextChanged("Original") + + assertFalse(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns true when label text is valid and the emoji is different from the original value`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Label") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelEmojiChanged("🎉") + + assertTrue(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns false when the label and emoji are not changed`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = "🎉", text = "Label") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + + assertFalse(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns false when the label and emoji are changed to the original value`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = "🎉", text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + + viewModel.onLabelEmojiChanged("🫢") + viewModel.onLabelTextChanged("Modified") + + viewModel.onLabelEmojiChanged("🎉") + viewModel.onLabelTextChanged("Original") + + assertFalse(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns false when label is too short`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Label") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelTextChanged("") + viewModel.onLabelEmojiChanged("🎉") + + assertFalse(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns true when clearLabel is called with existing label and emoji`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = "🎉", text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.clearLabel() + + assertTrue(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns true when clearLabel is called with existing label without emoji`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.clearLabel() + + assertTrue(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns false when clearLabel is called with no existing label`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns null + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.clearLabel() + + assertFalse(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns true when both emoji and label are modified`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = "🎉", text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelTextChanged("New Label") + viewModel.onLabelEmojiChanged("🚀") + + assertTrue(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `isSaveEnabled returns false when only emoji is changed without an existing label`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns null + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelEmojiChanged("🎉") + + assertFalse(viewModel.uiState.value.isSaveEnabled) + } + + @Test + fun `save does not call setLabel when isSaveEnabled is false`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Label") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.save() + + coVerify(exactly = 0) { memberLabelRepo.setLabel(any()) } + } + + @Test + fun `save does not call setLabel when label is less than 1 character`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Label") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelTextChanged("") + viewModel.onLabelEmojiChanged("🎉") + viewModel.save() + + coVerify(exactly = 0) { memberLabelRepo.setLabel(any()) } + } + + @Test + fun `save calls setLabel with truncated label when label exceeds max length`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns null + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelTextChanged("A".repeat(30)) + viewModel.save() + + coVerify(exactly = 1) { + memberLabelRepo.setLabel( + match { it.text.length == 24 } + ) + } + } + + @Test + fun `save does not call setLabel when emoji is set with no label`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns null + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelEmojiChanged("🎉") + viewModel.save() + + coVerify(exactly = 0) { memberLabelRepo.setLabel(any()) } + } + + @Test + fun `save calls setLabel when label change is valid`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = null, text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.onLabelTextChanged("New Label") + viewModel.onLabelEmojiChanged("🎉") + viewModel.save() + + coVerify(exactly = 1) { + memberLabelRepo.setLabel(MemberLabel(text = "New Label", emoji = "🎉")) + } + } + + @Test + fun `save calls setLabel with cleared values when clearLabel is called`() = runTest(testDispatcher) { + coEvery { memberLabelRepo.getLabel(any()) } returns MemberLabel(emoji = "🎉", text = "Original") + + val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId) + viewModel.clearLabel() + viewModel.save() + + coVerify(exactly = 1) { + memberLabelRepo.setLabel(MemberLabel(text = "", emoji = null)) + } + } +}