mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-05 09:52:13 +00:00
Merge branch 'od' into on-2
This commit is contained in:
@@ -23,6 +23,7 @@ import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.BaseViewModelTest
|
||||
import org.thoughtcrime.securesms.NoOpLogger
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
@@ -32,6 +33,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
|
||||
private val repository = mock<ConversationRepository>()
|
||||
private val storage = mock<Storage>()
|
||||
private val mmsDatabase = mock<MmsDatabase>()
|
||||
|
||||
private val threadId = 123L
|
||||
private val edKeyPair = mock<KeyPair>()
|
||||
@@ -39,7 +41,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
private lateinit var messageRecord: MessageRecord
|
||||
|
||||
private val viewModel: ConversationViewModel by lazy {
|
||||
ConversationViewModel(threadId, edKeyPair, repository, storage)
|
||||
ConversationViewModel(threadId, edKeyPair, repository, storage, mock(), mmsDatabase)
|
||||
}
|
||||
|
||||
@Before
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.Selection
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.thoughtcrime.securesms.conversation.v2.mention.MentionEditable
|
||||
import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class MentionEditableTest {
|
||||
private lateinit var mentionEditable: MentionEditable
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mentionEditable = MentionEditable()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not have query when there is no 'at' symbol`() = runTest {
|
||||
mentionEditable.observeMentionSearchQuery().test {
|
||||
assertThat(awaitItem()).isNull()
|
||||
mentionEditable.simulateTyping("Some text")
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should have empty query after typing 'at' symbol`() = runTest {
|
||||
mentionEditable.observeMentionSearchQuery().test {
|
||||
assertThat(awaitItem()).isNull()
|
||||
|
||||
mentionEditable.simulateTyping("Some text")
|
||||
expectNoEvents()
|
||||
|
||||
mentionEditable.simulateTyping("@")
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionEditable.SearchQuery(9, ""))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should have some query after typing words following 'at' symbol`() = runTest {
|
||||
mentionEditable.observeMentionSearchQuery().test {
|
||||
assertThat(awaitItem()).isNull()
|
||||
|
||||
mentionEditable.simulateTyping("Some text")
|
||||
expectNoEvents()
|
||||
|
||||
mentionEditable.simulateTyping("@words")
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionEditable.SearchQuery(9, "words"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should cancel query after a whitespace or another 'at' is typed`() = runTest {
|
||||
mentionEditable.observeMentionSearchQuery().test {
|
||||
assertThat(awaitItem()).isNull()
|
||||
|
||||
mentionEditable.simulateTyping("@words")
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionEditable.SearchQuery(0, "words"))
|
||||
|
||||
mentionEditable.simulateTyping(" ")
|
||||
assertThat(awaitItem())
|
||||
.isNull()
|
||||
|
||||
mentionEditable.simulateTyping("@query@")
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionEditable.SearchQuery(13, ""))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should move pass the whole span while moving cursor around mentioned block `() {
|
||||
mentionEditable.append("Mention @user here")
|
||||
mentionEditable.addMention(MentionViewModel.Member("user", "User", false), 8..14)
|
||||
|
||||
// Put cursor right before @user, it should then select nothing
|
||||
Selection.setSelection(mentionEditable, 8)
|
||||
assertThat(mentionEditable.selection()).isEqualTo(intArrayOf(8, 8))
|
||||
|
||||
// Put cursor right after '@', it should then select the whole @user
|
||||
Selection.setSelection(mentionEditable, 9)
|
||||
assertThat(mentionEditable.selection()).isEqualTo(intArrayOf(8, 13))
|
||||
|
||||
// Put cursor right after @user, it should then select nothing
|
||||
Selection.setSelection(mentionEditable, 13)
|
||||
assertThat(mentionEditable.selection()).isEqualTo(intArrayOf(13, 13))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should delete the whole mention block while deleting only part of it`() {
|
||||
mentionEditable.append("Mention @user here")
|
||||
mentionEditable.addMention(MentionViewModel.Member("user", "User", false), 8..14)
|
||||
|
||||
mentionEditable.delete(8, 9)
|
||||
assertThat(mentionEditable.toString()).isEqualTo("Mention here")
|
||||
}
|
||||
}
|
||||
|
||||
private fun CharSequence.selection(): IntArray {
|
||||
return intArrayOf(Selection.getSelectionStart(this), Selection.getSelectionEnd(this))
|
||||
}
|
||||
|
||||
private fun Editable.simulateTyping(text: String) {
|
||||
this.append(text)
|
||||
Selection.setSelection(this, this.length)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.text.Selection
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.open_groups.GroupMemberRole
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.MainCoroutineRule
|
||||
import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class MentionViewModelTest {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@get:Rule
|
||||
val mainCoroutineRule = MainCoroutineRule()
|
||||
|
||||
private lateinit var mentionViewModel: MentionViewModel
|
||||
|
||||
private val threadID = 123L
|
||||
|
||||
private data class MemberInfo(
|
||||
val name: String,
|
||||
val pubKey: String,
|
||||
val roles: List<GroupMemberRole>
|
||||
)
|
||||
|
||||
private val threadMembers = listOf(
|
||||
MemberInfo("Alice", "pubkey1", listOf(GroupMemberRole.ADMIN)),
|
||||
MemberInfo("Bob", "pubkey2", listOf(GroupMemberRole.STANDARD)),
|
||||
MemberInfo("Charlie", "pubkey3", listOf(GroupMemberRole.MODERATOR)),
|
||||
MemberInfo("David", "pubkey4", listOf(GroupMemberRole.HIDDEN_ADMIN)),
|
||||
MemberInfo("Eve", "pubkey5", listOf(GroupMemberRole.HIDDEN_MODERATOR)),
|
||||
MemberInfo("李云海", "pubkey6", listOf(GroupMemberRole.ZOOMBIE)),
|
||||
)
|
||||
|
||||
private val memberContacts = threadMembers.map { m ->
|
||||
Contact(m.pubKey).also {
|
||||
it.name = m.name
|
||||
}
|
||||
}
|
||||
|
||||
private val openGroup = OpenGroup(
|
||||
server = "",
|
||||
room = "",
|
||||
id = "open_group_id_1",
|
||||
name = "Open Group",
|
||||
publicKey = "",
|
||||
imageId = null,
|
||||
infoUpdates = 0,
|
||||
canWrite = true
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
mentionViewModel = MentionViewModel(
|
||||
threadID,
|
||||
contentResolver = mock { },
|
||||
threadDatabase = mock {
|
||||
on { getRecipientForThreadId(threadID) } doAnswer {
|
||||
mock<Recipient> {
|
||||
on { isClosedGroupRecipient } doReturn false
|
||||
on { isCommunityRecipient } doReturn true
|
||||
on { isContactRecipient } doReturn false
|
||||
}
|
||||
}
|
||||
},
|
||||
groupDatabase = mock {
|
||||
},
|
||||
mmsDatabase = mock {
|
||||
on { getRecentChatMemberIDs(eq(threadID), any()) } doAnswer {
|
||||
val limit = it.arguments[1] as Int
|
||||
threadMembers.take(limit).map { m -> m.pubKey }
|
||||
}
|
||||
},
|
||||
contactDatabase = mock {
|
||||
on { getContacts(any()) } doAnswer {
|
||||
val ids = it.arguments[0] as Collection<String>
|
||||
memberContacts.filter { contact -> contact.accountID in ids }
|
||||
}
|
||||
},
|
||||
memberDatabase = mock {
|
||||
on { getGroupMembersRoles(eq(openGroup.id), any()) } doAnswer {
|
||||
val memberIDs = it.arguments[1] as Collection<String>
|
||||
memberIDs.associateWith { id ->
|
||||
threadMembers.first { m -> m.pubKey == id }.roles
|
||||
}
|
||||
}
|
||||
},
|
||||
storage = mock {
|
||||
on { getOpenGroup(threadID) } doReturn openGroup
|
||||
},
|
||||
dispatcher = StandardTestDispatcher()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should show candidates after 'at' symbol`() = runTest {
|
||||
mentionViewModel.autoCompleteState.test {
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionViewModel.AutoCompleteState.Idle)
|
||||
|
||||
val editable = mentionViewModel.editableFactory.newEditable("")
|
||||
editable.append("Hello @")
|
||||
expectNoEvents() // Nothing should happen before cursor is put after @
|
||||
Selection.setSelection(editable, editable.length)
|
||||
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionViewModel.AutoCompleteState.Loading)
|
||||
|
||||
// Should show all the candidates
|
||||
awaitItem().let { result ->
|
||||
assertThat(result)
|
||||
.isInstanceOf(MentionViewModel.AutoCompleteState.Result::class.java)
|
||||
result as MentionViewModel.AutoCompleteState.Result
|
||||
|
||||
assertThat(result.members).isEqualTo(threadMembers.mapIndexed { index, m ->
|
||||
val name =
|
||||
memberContacts[index].displayName(Contact.ContactContext.OPEN_GROUP).orEmpty()
|
||||
|
||||
MentionViewModel.Candidate(
|
||||
MentionViewModel.Member(m.pubKey, name, m.roles.any { it.isModerator }),
|
||||
name,
|
||||
0
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Continue typing to filter candidates
|
||||
editable.append("li")
|
||||
Selection.setSelection(editable, editable.length)
|
||||
|
||||
// Should show only Alice and Charlie
|
||||
awaitItem().let { result ->
|
||||
assertThat(result)
|
||||
.isInstanceOf(MentionViewModel.AutoCompleteState.Result::class.java)
|
||||
result as MentionViewModel.AutoCompleteState.Result
|
||||
|
||||
assertThat(result.members[0].member.name).isEqualTo("Alice (pubk...key1)")
|
||||
assertThat(result.members[1].member.name).isEqualTo("Charlie (pubk...key3)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should have normalised message with candidates selected`() = runTest {
|
||||
mentionViewModel.autoCompleteState.test {
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionViewModel.AutoCompleteState.Idle)
|
||||
|
||||
val editable = mentionViewModel.editableFactory.newEditable("")
|
||||
editable.append("Hi @")
|
||||
Selection.setSelection(editable, editable.length)
|
||||
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(MentionViewModel.AutoCompleteState.Loading)
|
||||
|
||||
// Select a candidate now
|
||||
assertThat(awaitItem())
|
||||
.isInstanceOf(MentionViewModel.AutoCompleteState.Result::class.java)
|
||||
mentionViewModel.onCandidateSelected("pubkey1")
|
||||
|
||||
// Should have normalised message with selected candidate
|
||||
assertThat(mentionViewModel.normalizeMessageBody())
|
||||
.isEqualTo("Hi @pubkey1 ")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toCollection
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class FlowUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `timedBuffer should emit buffer when it's full`() = runTest {
|
||||
// Given
|
||||
val flow = flowOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
val timeoutMillis = 1000L
|
||||
val maxItems = 5
|
||||
|
||||
// When
|
||||
val result = flow.timedBuffer(timeoutMillis, maxItems).toList()
|
||||
|
||||
// Then
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(listOf(1, 2, 3, 4, 5), result[0])
|
||||
assertEquals(listOf(6, 7, 8, 9, 10), result[1])
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `timedBuffer should emit buffer when timeout expires`() = runTest {
|
||||
// Given
|
||||
val flow = flow {
|
||||
emit(1)
|
||||
emit(2)
|
||||
emit(3)
|
||||
testScheduler.advanceTimeBy(200L)
|
||||
emit(4)
|
||||
}
|
||||
val timeoutMillis = 100L
|
||||
val maxItems = 5
|
||||
|
||||
// When
|
||||
val result = flow.timedBuffer(timeoutMillis, maxItems).toList()
|
||||
|
||||
// Then
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(listOf(1, 2, 3), result[0])
|
||||
assertEquals(listOf(4), result[1])
|
||||
}
|
||||
}
|
||||
4
app/src/test/resources/TestAndroidManifest.xml
Normal file
4
app/src/test/resources/TestAndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
3
app/src/test/resources/robolectric.properties
Normal file
3
app/src/test/resources/robolectric.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
manifest=TestAndroidManifest.xml
|
||||
sdk=34
|
||||
application=android.app.Application
|
||||
Reference in New Issue
Block a user