diff --git a/app/src/main/java/com/interlinedlist/android/navigation/InterlinedListNavHost.kt b/app/src/main/java/com/interlinedlist/android/navigation/InterlinedListNavHost.kt index cf0ec9d..6dadaff 100644 --- a/app/src/main/java/com/interlinedlist/android/navigation/InterlinedListNavHost.kt +++ b/app/src/main/java/com/interlinedlist/android/navigation/InterlinedListNavHost.kt @@ -329,6 +329,10 @@ private fun MainShell( onBack = { tabNav.popBackStack() }, onOpenThread = { username -> tabNav.navigateToDmThread(username) }, onComposeNew = { tabNav.navigateToNewDm() }, + // DMs are limited to mutual follows; when the recipient set is + // empty the picker routes to the existing user search so the + // user can go and follow someone. + onFindPeople = { tabNav.navigate(Routes.USER_SEARCH) }, ) // ---- Lists ---- diff --git a/feature/directmessages/src/androidTest/kotlin/com/interlinedlist/android/feature/directmessages/ui/NewMessageScreenTest.kt b/feature/directmessages/src/androidTest/kotlin/com/interlinedlist/android/feature/directmessages/ui/NewMessageScreenTest.kt new file mode 100644 index 0000000..4955fe6 --- /dev/null +++ b/feature/directmessages/src/androidTest/kotlin/com/interlinedlist/android/feature/directmessages/ui/NewMessageScreenTest.kt @@ -0,0 +1,109 @@ +package com.interlinedlist.android.feature.directmessages.ui + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.interlinedlist.android.core.designsystem.theme.InterlinedListTheme +import com.interlinedlist.android.feature.directmessages.data.Recipient +import com.interlinedlist.android.feature.directmessages.ui.newmessage.NewMessageScreen +import com.interlinedlist.android.feature.directmessages.ui.newmessage.NewMessageTestTags +import com.interlinedlist.android.feature.directmessages.ui.newmessage.NewMessageUiState +import com.interlinedlist.android.feature.directmessages.ui.newmessage.RecipientRuleCopy +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * The recipient picker must tell three stories apart: still loading, loaded but + * nobody is messageable yet (the mutual-follow rule), and an actual failure. + */ +@RunWith(AndroidJUnit4::class) +class NewMessageScreenTest { + + @get:Rule + val composeRule = createComposeRule() + + private val adron = Recipient("u1", "adron", "Adron Hall", null) + + private fun setScreen( + state: NewMessageUiState, + onFindPeople: () -> Unit = {}, + onRetry: () -> Unit = {}, + ) { + composeRule.setContent { + InterlinedListTheme { + NewMessageScreen( + state = state, + onQueryChange = {}, + onBack = {}, + onRecipientChosen = {}, + onFindPeople = onFindPeople, + onRetry = onRetry, + ) + } + } + } + + @Test + fun emptyRecipients_explainsTheRule_andOffersAWayOut() { + setScreen(NewMessageUiState(recipients = emptyList(), isLoading = false)) + + composeRule.onNodeWithTag(NewMessageTestTags.EMPTY).assertIsDisplayed() + composeRule.onNodeWithText(RecipientRuleCopy.TITLE).assertIsDisplayed() + composeRule.onNodeWithText(RecipientRuleCopy.EXPLANATION).assertIsDisplayed() + composeRule.onNodeWithTag(NewMessageTestTags.FIND_PEOPLE).assertIsDisplayed() + composeRule.onNodeWithTag(NewMessageTestTags.LIST).assertDoesNotExist() + composeRule.onNodeWithTag(NewMessageTestTags.ERROR).assertDoesNotExist() + } + + @Test + fun findPeople_routesToUserSearch() { + var tapped = false + setScreen( + state = NewMessageUiState(recipients = emptyList(), isLoading = false), + onFindPeople = { tapped = true }, + ) + + composeRule.onNodeWithTag(NewMessageTestTags.FIND_PEOPLE).performClick() + + assertThat(tapped).isTrue() + } + + @Test + fun recipients_areStillListed() { + setScreen(NewMessageUiState(recipients = listOf(adron), isLoading = false)) + + composeRule.onNodeWithTag(NewMessageTestTags.LIST).assertIsDisplayed() + composeRule.onNodeWithTag(NewMessageTestTags.row("adron")).assertIsDisplayed() + composeRule.onNodeWithTag(NewMessageTestTags.EMPTY).assertDoesNotExist() + } + + @Test + fun loading_showsProgress_notTheEmptyExplanation() { + setScreen(NewMessageUiState(isLoading = true)) + + composeRule.onNodeWithTag(NewMessageTestTags.PROGRESS).assertIsDisplayed() + composeRule.onNodeWithTag(NewMessageTestTags.EMPTY).assertDoesNotExist() + } + + @Test + fun error_showsRetry_notTheEmptyExplanation() { + var retried = false + setScreen( + state = NewMessageUiState(isLoading = false, errorMessage = "No connection."), + onRetry = { retried = true }, + ) + + composeRule.onNodeWithTag(NewMessageTestTags.ERROR).assertIsDisplayed() + composeRule.onNodeWithText(RecipientRuleCopy.TITLE).assertDoesNotExist() + composeRule.onNodeWithTag(NewMessageTestTags.EMPTY).assertDoesNotExist() + + composeRule.onNodeWithTag(NewMessageTestTags.RETRY).performClick() + + assertThat(retried).isTrue() + } +} diff --git a/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/navigation/DirectMessagesNavigation.kt b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/navigation/DirectMessagesNavigation.kt index 9cba4b3..d1ff3e9 100644 --- a/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/navigation/DirectMessagesNavigation.kt +++ b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/navigation/DirectMessagesNavigation.kt @@ -37,12 +37,14 @@ fun NavController.navigateToNewDm() = navigate(DirectMessagesDestinations.NEW_ME * * The app wires this into its top-level NavHost (see the module's report for the * exact snippet). [onBack] pops the current destination; [onOpenThread] lets the - * host decide how threads are pushed. + * host decide how threads are pushed. [onFindPeople] should open the host's user + * search so someone with no messageable contacts can go and follow people. */ fun NavGraphBuilder.directMessagesGraph( onBack: () -> Unit, onOpenThread: (username: String) -> Unit, onComposeNew: () -> Unit, + onFindPeople: () -> Unit, ) { composable(DirectMessagesDestinations.INBOX) { InboxRoute( @@ -64,6 +66,7 @@ fun NavGraphBuilder.directMessagesGraph( NewMessageRoute( onBack = onBack, onRecipientChosen = onOpenThread, + onFindPeople = onFindPeople, ) } } diff --git a/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageScreen.kt b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageScreen.kt index d98dfb7..f4330d1 100644 --- a/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageScreen.kt +++ b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageScreen.kt @@ -2,17 +2,23 @@ package com.interlinedlist.android.feature.directmessages.ui.newmessage import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.PersonAddAlt +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -27,6 +33,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -39,6 +46,11 @@ object NewMessageTestTags { const val SEARCH = "dmNewSearch" const val LIST = "dmNewList" const val EMPTY = "dmNewEmpty" + const val FIND_PEOPLE = "dmNewFindPeople" + const val NO_MATCHES = "dmNewNoMatches" + const val PROGRESS = "dmNewProgress" + const val ERROR = "dmNewError" + const val RETRY = "dmNewRetry" const val BACK = "dmNewBack" fun row(username: String) = "dmNewRow_$username" } @@ -48,6 +60,7 @@ object NewMessageTestTags { fun NewMessageRoute( onBack: () -> Unit, onRecipientChosen: (username: String) -> Unit, + onFindPeople: () -> Unit, modifier: Modifier = Modifier, viewModel: NewMessageViewModel = hiltViewModel(), ) { @@ -57,6 +70,8 @@ fun NewMessageRoute( onQueryChange = viewModel::onQueryChange, onBack = onBack, onRecipientChosen = { onRecipientChosen(it.username) }, + onFindPeople = onFindPeople, + onRetry = viewModel::load, modifier = modifier, ) } @@ -69,6 +84,8 @@ fun NewMessageScreen( onQueryChange: (String) -> Unit, onBack: () -> Unit, onRecipientChosen: (Recipient) -> Unit, + onFindPeople: () -> Unit, + onRetry: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -92,20 +109,49 @@ fun NewMessageScreen( .fillMaxSize() .padding(padding), ) { - OutlinedTextField( - value = state.query, - onValueChange = onQueryChange, - singleLine = true, - placeholder = { Text("Search people") }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - .testTag(NewMessageTestTags.SEARCH), - ) - if (state.isEmpty) { - EmptyRecipients(modifier = Modifier.fillMaxSize()) - } else { - LazyColumn( + // Searching is only meaningful once there is something to search. + if (state.content == NewMessageContent.Recipients || + state.content == NewMessageContent.NoMatches + ) { + OutlinedTextField( + value = state.query, + onValueChange = onQueryChange, + singleLine = true, + placeholder = { Text("Search people") }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .testTag(NewMessageTestTags.SEARCH), + ) + } + when (state.content) { + NewMessageContent.Loading -> Centered { + CircularProgressIndicator( + modifier = Modifier.testTag(NewMessageTestTags.PROGRESS), + ) + } + + NewMessageContent.Error -> Centered { + ErrorRecipients( + message = state.errorMessage.orEmpty(), + onRetry = onRetry, + ) + } + + NewMessageContent.NoRecipients -> Centered { + NoRecipients(onFindPeople = onFindPeople) + } + + NewMessageContent.NoMatches -> Centered { + Text( + text = RecipientRuleCopy.NO_MATCHES, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.testTag(NewMessageTestTags.NO_MATCHES), + ) + } + + NewMessageContent.Recipients -> LazyColumn( modifier = Modifier .fillMaxSize() .testTag(NewMessageTestTags.LIST), @@ -123,6 +169,19 @@ fun NewMessageScreen( } } +/** Fills the remaining space and centres [content]; used by the non-list states. */ +@Composable +private fun Centered(content: @Composable () -> Unit) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + contentAlignment = Alignment.Center, + ) { + content() + } +} + @Composable private fun RecipientRow( recipient: Recipient, @@ -158,21 +217,64 @@ private fun RecipientRow( } } +/** + * Loaded, but the user may not message anyone yet. This is the normal state for + * a new account, so it explains the rule (see [RecipientRuleCopy]) and offers a + * way out instead of leaving the user on a blank list. + */ @Composable -private fun EmptyRecipients(modifier: Modifier = Modifier) { +private fun NoRecipients(onFindPeople: () -> Unit) { Column( - modifier = modifier.testTag(NewMessageTestTags.EMPTY), + modifier = Modifier.testTag(NewMessageTestTags.EMPTY), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { + Icon( + imageVector = Icons.Filled.PersonAddAlt, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(48.dp), + ) + Spacer(Modifier.height(12.dp)) Text( - text = "No one to message yet", + text = RecipientRuleCopy.TITLE, style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, ) + Spacer(Modifier.height(8.dp)) Text( - text = "Follow people to start conversations.", + text = RecipientRuleCopy.EXPLANATION, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(20.dp)) + Button( + onClick = onFindPeople, + modifier = Modifier.testTag(NewMessageTestTags.FIND_PEOPLE), + ) { + Text(RecipientRuleCopy.FIND_PEOPLE) + } + } +} + +/** A genuine failure to load the recipient set — retryable, never the rule copy. */ +@Composable +private fun ErrorRecipients(message: String, onRetry: () -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.testTag(NewMessageTestTags.ERROR), ) + Spacer(Modifier.height(16.dp)) + Button(onClick = onRetry, modifier = Modifier.testTag(NewMessageTestTags.RETRY)) { + Text("Retry") + } } } diff --git a/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModel.kt b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModel.kt index 4982558..61cf6a5 100644 --- a/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModel.kt +++ b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModel.kt @@ -14,6 +14,28 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +/** + * The mutually exclusive surfaces the recipient picker can show. Keeping these + * distinct stops a successful-but-empty response (the normal state for a new + * account, see [RecipientRuleCopy]) from reading like a failure or a blank list. + */ +enum class NewMessageContent { + /** The recipient set is still being fetched. */ + Loading, + + /** The fetch failed — a real error the user can retry. */ + Error, + + /** Loaded fine, but the user may not message anyone yet. */ + NoRecipients, + + /** There are recipients, but the current query matches none of them. */ + NoMatches, + + /** Recipients to pick from. */ + Recipients, +} + /** UI state for the recipient picker used to start a new conversation. */ data class NewMessageUiState( val query: String = "", @@ -32,7 +54,16 @@ data class NewMessageUiState( r.displayName?.lowercase()?.contains(q) == true } } - val isEmpty: Boolean get() = recipients.isEmpty() && !isLoading + + /** Which of the picker's surfaces to render; see [NewMessageContent]. */ + val content: NewMessageContent + get() = when { + isLoading -> NewMessageContent.Loading + errorMessage != null -> NewMessageContent.Error + recipients.isEmpty() -> NewMessageContent.NoRecipients + filtered.isEmpty() -> NewMessageContent.NoMatches + else -> NewMessageContent.Recipients + } } @HiltViewModel diff --git a/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/RecipientRuleCopy.kt b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/RecipientRuleCopy.kt new file mode 100644 index 0000000..95fec5f --- /dev/null +++ b/feature/directmessages/src/main/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/RecipientRuleCopy.kt @@ -0,0 +1,29 @@ +package com.interlinedlist.android.feature.directmessages.ui.newmessage + +/** + * Copy for the "who you can message" rule enforced by `GET /api/dm/recipients`. + * + * Wording is taken from the public help centre + * (https://interlinedlist.com/help/direct-messages → *Who you can message*): + * "You can only message people you follow each other: you follow them and they + * follow you back (both follow requests approved)… Once you both follow each + * other, they'll appear in your recipient list." + * + * Kept here — rather than inline in the composable — so the wording is asserted + * by tests and never drifts from the rule the server actually enforces. + */ +object RecipientRuleCopy { + /** Headline for the loaded-but-empty picker. Deliberately not an error. */ + const val TITLE = "No one to message yet" + + /** The rule itself, phrased so it holds whatever the reason the set is empty. */ + const val EXPLANATION = + "You can only message people you follow each other with: you follow them, " + + "and they follow you back. Once you both follow each other, they'll appear here." + + /** Route out of the dead end, into the existing people search. */ + const val FIND_PEOPLE = "Find people to follow" + + /** Shown when the picker has recipients but the query matches none of them. */ + const val NO_MATCHES = "No one matches that search." +} diff --git a/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/FakeDirectMessagesRepository.kt b/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/FakeDirectMessagesRepository.kt index 9376022..3fc37ff 100644 --- a/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/FakeDirectMessagesRepository.kt +++ b/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/FakeDirectMessagesRepository.kt @@ -32,6 +32,7 @@ class FakeDirectMessagesRepository( var recipientsResult: ApiResult> = ApiResult.Success(emptyList()) var unreadResult: ApiResult = ApiResult.Success(0) + var recipientsCount = 0 var refreshInboxCount = 0 var refreshThreadCount = 0 var pollCount = 0 @@ -74,7 +75,11 @@ class FakeDirectMessagesRepository( override suspend fun markRead(id: String): ApiResult = markReadResult override suspend fun trash(id: String): ApiResult = trashResult override suspend fun restore(id: String, username: String): ApiResult = restoreResult - override suspend fun recipients(): ApiResult> = recipientsResult + override suspend fun recipients(): ApiResult> { + recipientsCount++ + return recipientsResult + } + override suspend fun unreadCount(): ApiResult = unreadResult fun failEverythingWith(error: AppError) { diff --git a/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModelTest.kt b/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModelTest.kt index 8fedf1c..a22e61f 100644 --- a/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModelTest.kt +++ b/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/NewMessageViewModelTest.kt @@ -67,4 +67,81 @@ class NewMessageViewModelTest { assertThat(vm.uiState.value.isLoading).isFalse() assertThat(vm.uiState.value.errorMessage).isNotNull() } + + // ---- loading / loaded-but-empty / error are three distinct states ---- + + @Test + fun `content is Loading while the first load is in flight`() = runTest(dispatcher) { + val repo = FakeDirectMessagesRepository() + repo.recipientsResult = ApiResult.Success(listOf(adron)) + + val vm = NewMessageViewModel(repo) + + assertThat(vm.uiState.value.content).isEqualTo(NewMessageContent.Loading) + } + + @Test + fun `content is NoRecipients when the server returns an empty set`() = runTest(dispatcher) { + val repo = FakeDirectMessagesRepository() + repo.recipientsResult = ApiResult.Success(emptyList()) + + val vm = NewMessageViewModel(repo) + advanceUntilIdle() + + assertThat(vm.uiState.value.content).isEqualTo(NewMessageContent.NoRecipients) + assertThat(vm.uiState.value.errorMessage).isNull() + } + + @Test + fun `content is Recipients when the server returns people`() = runTest(dispatcher) { + val repo = FakeDirectMessagesRepository() + repo.recipientsResult = ApiResult.Success(listOf(adron, blake)) + + val vm = NewMessageViewModel(repo) + advanceUntilIdle() + + assertThat(vm.uiState.value.content).isEqualTo(NewMessageContent.Recipients) + } + + @Test + fun `content is Error on failure, never the empty explanation`() = runTest(dispatcher) { + val repo = FakeDirectMessagesRepository() + repo.recipientsResult = ApiResult.Failure(AppError.Network("offline")) + + val vm = NewMessageViewModel(repo) + advanceUntilIdle() + + assertThat(vm.uiState.value.content).isEqualTo(NewMessageContent.Error) + assertThat(vm.uiState.value.content).isNotEqualTo(NewMessageContent.NoRecipients) + } + + @Test + fun `a query that matches no one is NoMatches, not the rule explanation`() = + runTest(dispatcher) { + val repo = FakeDirectMessagesRepository() + repo.recipientsResult = ApiResult.Success(listOf(adron, blake)) + val vm = NewMessageViewModel(repo) + advanceUntilIdle() + + vm.onQueryChange("nobody-by-that-name") + + assertThat(vm.uiState.value.content).isEqualTo(NewMessageContent.NoMatches) + } + + @Test + fun `retry reloads and clears the previous error`() = runTest(dispatcher) { + val repo = FakeDirectMessagesRepository() + repo.recipientsResult = ApiResult.Failure(AppError.Network("offline")) + val vm = NewMessageViewModel(repo) + advanceUntilIdle() + assertThat(vm.uiState.value.content).isEqualTo(NewMessageContent.Error) + + repo.recipientsResult = ApiResult.Success(listOf(adron)) + vm.load() + advanceUntilIdle() + + assertThat(repo.recipientsCount).isEqualTo(2) + assertThat(vm.uiState.value.errorMessage).isNull() + assertThat(vm.uiState.value.content).isEqualTo(NewMessageContent.Recipients) + } } diff --git a/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/RecipientRuleCopyTest.kt b/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/RecipientRuleCopyTest.kt new file mode 100644 index 0000000..70c47f3 --- /dev/null +++ b/feature/directmessages/src/test/kotlin/com/interlinedlist/android/feature/directmessages/ui/newmessage/RecipientRuleCopyTest.kt @@ -0,0 +1,31 @@ +package com.interlinedlist.android.feature.directmessages.ui.newmessage + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +/** + * Guards the empty-picker copy against the rule the server actually enforces + * (https://interlinedlist.com/help/direct-messages → *Who you can message*): + * mutual follows only. + */ +class RecipientRuleCopyTest { + + @Test + fun `the explanation states the mutual-follow rule`() { + assertThat(RecipientRuleCopy.EXPLANATION).contains("follow them") + assertThat(RecipientRuleCopy.EXPLANATION).contains("follow you back") + } + + @Test + fun `the empty state does not read as a failure`() { + val copy = (RecipientRuleCopy.TITLE + " " + RecipientRuleCopy.EXPLANATION).lowercase() + listOf("error", "failed", "went wrong", "try again", "couldn't").forEach { word -> + assertThat(copy).doesNotContain(word) + } + } + + @Test + fun `the call to action points at following people`() { + assertThat(RecipientRuleCopy.FIND_PEOPLE.lowercase()).contains("follow") + } +}