diff --git a/Simplenote/src/main/java/com/automattic/simplenote/CollaboratorsActivity.kt b/Simplenote/src/main/java/com/automattic/simplenote/CollaboratorsActivity.kt index 794501e61..3c6ab69b0 100644 --- a/Simplenote/src/main/java/com/automattic/simplenote/CollaboratorsActivity.kt +++ b/Simplenote/src/main/java/com/automattic/simplenote/CollaboratorsActivity.kt @@ -4,20 +4,27 @@ import android.content.Intent import android.os.Bundle import android.text.SpannableString import android.view.HapticFeedbackConstants +import android.view.Menu import android.view.MenuItem import android.view.View +import android.widget.LinearLayout import android.widget.Toast import androidx.activity.viewModels +import androidx.annotation.DrawableRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ContextThemeWrapper +import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.recyclerview.widget.LinearLayoutManager import com.automattic.simplenote.databinding.ActivityCollaboratorsBinding import com.automattic.simplenote.utils.CollaboratorsAdapter import com.automattic.simplenote.utils.CollaboratorsAdapter.CollaboratorDataItem.* import com.automattic.simplenote.utils.DisplayUtils +import com.automattic.simplenote.utils.DrawableUtils +import com.automattic.simplenote.utils.HtmlCompat import com.automattic.simplenote.utils.IntentUtils import com.automattic.simplenote.utils.SystemBarUtils +import com.automattic.simplenote.utils.getColorStr import com.automattic.simplenote.utils.toast import com.automattic.simplenote.viewmodels.CollaboratorsViewModel import com.automattic.simplenote.viewmodels.CollaboratorsViewModel.Event @@ -62,6 +69,47 @@ class CollaboratorsActivity : ThemedAppCompatActivity() { } } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + + menuInflater.inflate(R.menu.collaborators_list, menu) + DrawableUtils.tintMenuWithAttribute(this, menu, R.attr.toolbarIconColor) + val searchMenuItem = menu.findItem(R.id.menu_search) + val searchView = searchMenuItem.actionView as SearchView + val searchEditFrame = searchView.findViewById(R.id.search_edit_frame) + (searchEditFrame.layoutParams as LinearLayout.LayoutParams).leftMargin = 0 + + val hintHexColor = getColorStr(R.color.text_title_disabled) + searchView.queryHint = HtmlCompat.fromHtml(String.format( + "%s", + hintHexColor, + getString(R.string.search_collaborators_hint) + )) + + searchView.setOnQueryTextListener( + object : SearchView.OnQueryTextListener { + override fun onQueryTextChange(query: String): Boolean { + if (searchMenuItem.isActionViewExpanded) { + viewModel.search(query) + } + + return true + } + + override fun onQueryTextSubmit(queryText: String): Boolean { + return true + } + } + ) + + searchView.setOnCloseListener { + viewModel.closeSearch() + false + } + + return true + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { viewModel.close() @@ -70,6 +118,17 @@ class CollaboratorsActivity : ThemedAppCompatActivity() { return super.onOptionsItemSelected(item) } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + viewModel.uiState.observe(this@CollaboratorsActivity, { uiState -> + menu?.findItem(R.id.menu_search)?.isVisible = when (uiState) { + is EmptyCollaborators -> !uiState.allCollaboratorsRemoved && !uiState.searchUpdate + else -> true + } + }) + + return super.onPrepareOptionsMenu(menu) + } + override fun onResume() { super.onResume() @@ -117,8 +176,12 @@ class CollaboratorsActivity : ThemedAppCompatActivity() { private fun ActivityCollaboratorsBinding.setObservers() { viewModel.uiState.observe(this@CollaboratorsActivity, { uiState -> when (uiState) { - is EmptyCollaborators -> handleEmptyCollaborators() - is CollaboratorsList -> handleCollaboratorsList(uiState.collaborators) + is EmptyCollaborators -> { + handleEmptyCollaborators(uiState.allCollaboratorsRemoved, uiState.searchUpdate) + } + is CollaboratorsList -> { + handleCollaboratorsList(uiState.collaborators, uiState.searchUpdate, uiState.searchQuery) + } is NoteDeleted -> { toast(R.string.collaborators_note_deleted, Toast.LENGTH_LONG) navigateToNotesList() @@ -156,15 +219,33 @@ class CollaboratorsActivity : ThemedAppCompatActivity() { toast(R.string.remove_collaborator) } - private fun ActivityCollaboratorsBinding.handleCollaboratorsList(collaborators: List) { + private fun ActivityCollaboratorsBinding.handleCollaboratorsList(collaborators: List, searchUpdate: Boolean, searchQuery: String?) { hideEmptyView() val items = listOf(HeaderItem) + collaborators.map { CollaboratorItem(it) } - (collaboratorsList.adapter as CollaboratorsAdapter).submitList(items) + (collaboratorsList.adapter as CollaboratorsAdapter).submitList(items) { + if (searchUpdate) { + collaboratorsList.scrollToPosition(0) + + if (searchQuery != null) { + setEmptyViewSearch() + } else { + setEmptyViewDefault() + } + } + } } - private fun ActivityCollaboratorsBinding.handleEmptyCollaborators() { + private fun ActivityCollaboratorsBinding.handleEmptyCollaborators(allCollaboratorsRemoved: Boolean, searchUpdate: Boolean) { showEmptyView() - (collaboratorsList.adapter as CollaboratorsAdapter).submitList(emptyList()) + (collaboratorsList.adapter as CollaboratorsAdapter).submitList(emptyList()) { + if (allCollaboratorsRemoved) { + invalidateOptionsMenu() + setEmptyViewDefault() + } else if (searchUpdate) { + collaboratorsList.scrollToPosition(0) + setEmptyViewSearch() + } + } } private fun ActivityCollaboratorsBinding.hideEmptyView() { @@ -179,6 +260,53 @@ class CollaboratorsActivity : ThemedAppCompatActivity() { empty.message.visibility = View.VISIBLE } + private fun ActivityCollaboratorsBinding.setEmptyListImage(@DrawableRes image: Int) { + if (image != -1) { + empty.image.visibility = View.VISIBLE + empty.image.setImageResource(image) + } else { + empty.image.visibility = View.GONE + } + } + + private fun ActivityCollaboratorsBinding.setEmptyListMessage(message: String?) { + message?.let { + empty.message.text = it + } + } + + private fun ActivityCollaboratorsBinding.setEmptyListTitle(title: String?) { + title?.let { + empty.title.text = it + } + } + + private fun ActivityCollaboratorsBinding.setEmptyViewDefault() { + if (DisplayUtils.isLandscape(this@CollaboratorsActivity) && + !DisplayUtils.isLargeScreen(this@CollaboratorsActivity) + ) { + setEmptyListImage(-1) + } else { + setEmptyListImage(R.drawable.ic_collaborate_24dp) + } + + setEmptyListMessage(getString(R.string.add_email_collaborator_message)) + setEmptyListTitle(getString(R.string.no_collaborators)) + } + + private fun ActivityCollaboratorsBinding.setEmptyViewSearch() { + if (DisplayUtils.isLandscape(this@CollaboratorsActivity) && + !DisplayUtils.isLargeScreen(this@CollaboratorsActivity) + ) { + setEmptyListImage(-1) + } else { + setEmptyListImage(R.drawable.ic_search_24dp) + } + + setEmptyListMessage("") + setEmptyListTitle(getString(R.string.no_collaborators_search)) + } + private fun showAddCollaboratorFragment(event: Event.AddCollaboratorEvent) { val dialog = AddCollaboratorFragment(event.noteId) dialog.show(supportFragmentManager.beginTransaction(), DIALOG_TAG) diff --git a/Simplenote/src/main/java/com/automattic/simplenote/repositories/CollaboratorsRepository.kt b/Simplenote/src/main/java/com/automattic/simplenote/repositories/CollaboratorsRepository.kt index fb4e21c2e..d3a555aad 100644 --- a/Simplenote/src/main/java/com/automattic/simplenote/repositories/CollaboratorsRepository.kt +++ b/Simplenote/src/main/java/com/automattic/simplenote/repositories/CollaboratorsRepository.kt @@ -10,9 +10,9 @@ interface CollaboratorsRepository { fun isValidCollaborator(collaborator: String): Boolean /** - * Get a list of collaborators for a given [noteId]. + * Get list of collaborators for given [noteId] and {optional} [query]. */ - suspend fun getCollaborators(noteId: String): CollaboratorsActionResult + suspend fun getCollaborators(noteId: String, query: String? = null): CollaboratorsActionResult suspend fun addCollaborator(noteId: String, collaborator: String): CollaboratorsActionResult diff --git a/Simplenote/src/main/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepository.kt b/Simplenote/src/main/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepository.kt index 313bcf04c..1305dc044 100644 --- a/Simplenote/src/main/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepository.kt +++ b/Simplenote/src/main/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepository.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext +import java.util.Locale import javax.inject.Inject import javax.inject.Named @@ -32,14 +33,20 @@ class SimperiumCollaboratorsRepository @Inject constructor( } /** - * Return a list of collaborators (email addresses as tags) if the note for the given simperiumKey ([noteId]) is - * not in the trash and has not been deleted + * Get list of collaborators (email addresses as tags) for given [noteId] which is not deleted, not trashed, and + * contains given (optional) [query]. + * + * @param noteId [String] Simperium key of note to retrieve collaborators from + * @param query [String] (optional) to filter list of collaborators with + * + * @return [List]<[String]> of collaborators for [noteId] containing [query] */ override suspend fun getCollaborators( - noteId: String + noteId: String, + query: String? ) = when (val result = getNote(noteId)) { is Either.Left -> result.l - is Either.Right -> CollaboratorsActionResult.CollaboratorsList(filterCollaborators(result.r)) + is Either.Right -> CollaboratorsActionResult.CollaboratorsList(filterCollaborators(result.r, query)) } override suspend fun addCollaborator( @@ -106,6 +113,11 @@ class SimperiumCollaboratorsRepository @Inject constructor( } } + private fun filterCollaborators(note: Note) = note.tags.filter { tag -> + isValidCollaborator(tag) + } - private fun filterCollaborators(note: Note) = note.tags.filter { tag -> isValidCollaborator(tag) } + private fun filterCollaborators(note: Note, query: String?) = note.tags.filter { tag -> + isValidCollaborator(tag) && tag.lowercase(Locale.ROOT).contains(query?.lowercase(Locale.ROOT) ?: "") + } } diff --git a/Simplenote/src/main/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModel.kt b/Simplenote/src/main/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModel.kt index 389dd8ddb..8882cfd61 100644 --- a/Simplenote/src/main/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModel.kt +++ b/Simplenote/src/main/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModel.kt @@ -24,7 +24,7 @@ class CollaboratorsViewModel @Inject constructor( private lateinit var noteId: String - private var jobTagsFlow: Job? = null + private var jobCollaborators: Job? = null fun loadCollaborators(noteId: String) { this.noteId = noteId @@ -33,12 +33,12 @@ class CollaboratorsViewModel @Inject constructor( } } - private suspend fun updateUiState(noteId: String) { - when (val result = collaboratorsRepository.getCollaborators(noteId)) { + private suspend fun updateUiState(noteId: String, searchUpdate: Boolean = false, searchQuery: String? = null) { + when (val result = collaboratorsRepository.getCollaborators(noteId, searchQuery)) { is CollaboratorsActionResult.CollaboratorsList -> _uiState.value = when (result.collaborators.isEmpty()) { - true -> UiState.EmptyCollaborators - false -> UiState.CollaboratorsList(result.collaborators) + true -> UiState.EmptyCollaborators(allCollaboratorsRemoved = searchQuery.isNullOrEmpty(), searchUpdate) + false -> UiState.CollaboratorsList(result.collaborators, searchUpdate, searchQuery) } is CollaboratorsActionResult.NoteDeleted -> _uiState.value = UiState.NoteDeleted is CollaboratorsActionResult.NoteInTrash -> _uiState.value = UiState.NoteInTrash @@ -46,7 +46,7 @@ class CollaboratorsViewModel @Inject constructor( } fun startListeningChanges() { - jobTagsFlow = viewModelScope.launch { + jobCollaborators = viewModelScope.launch { collaboratorsRepository.collaboratorsChanged(noteId).collect { updateUiState(noteId) } @@ -54,7 +54,7 @@ class CollaboratorsViewModel @Inject constructor( } fun stopListeningChanges() { - jobTagsFlow?.cancel() + jobCollaborators?.cancel() } fun clickAddCollaborator() { @@ -77,12 +77,24 @@ class CollaboratorsViewModel @Inject constructor( _event.value = Event.CloseCollaboratorsEvent } + fun closeSearch() { + viewModelScope.launch { + updateUiState(noteId, searchUpdate = false) + } + } + + fun search(searchQuery: String) { + viewModelScope.launch { + updateUiState(noteId, searchUpdate = true, searchQuery) + } + } + fun removeCollaborator(collaborator: String) { viewModelScope.launch { when (val result = collaboratorsRepository.removeCollaborator(noteId, collaborator)) { is CollaboratorsActionResult.CollaboratorsList -> { _uiState.value = when (result.collaborators.isEmpty()) { - true -> UiState.EmptyCollaborators + true -> UiState.EmptyCollaborators(allCollaboratorsRemoved = true) false -> UiState.CollaboratorsList(result.collaborators) } @@ -102,8 +114,8 @@ class CollaboratorsViewModel @Inject constructor( sealed class UiState { object NoteInTrash : UiState() object NoteDeleted : UiState() - object EmptyCollaborators : UiState() - data class CollaboratorsList(val collaborators: List) : UiState() + data class EmptyCollaborators(val allCollaboratorsRemoved: Boolean, val searchUpdate: Boolean = false) : UiState() + data class CollaboratorsList(val collaborators: List, val searchUpdate: Boolean = false, val searchQuery: String? = null) : UiState() } sealed class Event { diff --git a/Simplenote/src/main/res/menu/collaborators_list.xml b/Simplenote/src/main/res/menu/collaborators_list.xml new file mode 100644 index 000000000..63f01b60c --- /dev/null +++ b/Simplenote/src/main/res/menu/collaborators_list.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/Simplenote/src/main/res/values/strings.xml b/Simplenote/src/main/res/values/strings.xml index 741878017..376206893 100644 --- a/Simplenote/src/main/res/values/strings.xml +++ b/Simplenote/src/main/res/values/strings.xml @@ -46,6 +46,8 @@ Note added Search Notes or Tags Search notes or tags + Search Collaborators + Search collaborators Search Tags Search tags Notes @@ -347,6 +349,7 @@ Note was deleted. You cannot edit collaborators on a note that was deleted. Note was trashed. You cannot edit collaborators on a note that is in the trash. No Collaborators + No collaborators found Change passcode diff --git a/Simplenote/src/test/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepositoryTest.kt b/Simplenote/src/test/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepositoryTest.kt index 95e33832a..3594e6a49 100644 --- a/Simplenote/src/test/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepositoryTest.kt +++ b/Simplenote/src/test/java/com/automattic/simplenote/repositories/SimperiumCollaboratorsRepositoryTest.kt @@ -34,7 +34,7 @@ class SimperiumCollaboratorsRepositoryTest { private val noteId = "key1" private val note = Note(noteId).apply { content = "Hello World" - tags = listOf("tag1", "tag2", "test@emil.com", "name@example.co.jp", "name@test", "あいうえお@example.com") + tags = listOf("tag1", "tag2", "test@email.com", "name@example.co.jp", "name@test", "あいうえお@example.com") bucket = mockBucket } @@ -73,7 +73,7 @@ class SimperiumCollaboratorsRepositoryTest { @Test fun getCollaboratorsShouldReturnJustEmails() = runTest { - val expected = CollaboratorsActionResult.CollaboratorsList(listOf("test@emil.com", "name@example.co.jp")) + val expected = CollaboratorsActionResult.CollaboratorsList(listOf("test@email.com", "name@example.co.jp")) val result = collaboratorsRepository.getCollaborators(noteId) assertEquals(expected, result) @@ -105,7 +105,7 @@ class SimperiumCollaboratorsRepositoryTest { val result = collaboratorsRepository.addCollaborator(noteId, collaborator) - val newCollaborators = listOf("test@emil.com", "name@example.co.jp", collaborator) + val newCollaborators = listOf("test@email.com", "name@example.co.jp", collaborator) val expected = CollaboratorsActionResult.CollaboratorsList(newCollaborators) assertEquals(expected, result) } @@ -138,7 +138,7 @@ class SimperiumCollaboratorsRepositoryTest { val result = collaboratorsRepository.removeCollaborator(noteId, collaborator) - val newCollaborators = listOf("test@emil.com") + val newCollaborators = listOf("test@email.com") val expected = CollaboratorsActionResult.CollaboratorsList(newCollaborators) assertEquals(expected, result) } diff --git a/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/AddCollaboratorViewModelTest.kt b/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/AddCollaboratorViewModelTest.kt index 84068a425..800727be7 100644 --- a/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/AddCollaboratorViewModelTest.kt +++ b/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/AddCollaboratorViewModelTest.kt @@ -54,7 +54,7 @@ class AddCollaboratorViewModelTest { @Test fun addValidCollaboratorShouldTriggerAddedEvent() = runTest { - viewModel.addCollaborator(noteId,"test@emil.com") + viewModel.addCollaborator(noteId,"test@email.com") assertEquals(AddCollaboratorViewModel.Event.CollaboratorAdded, viewModel.event.value) } @@ -70,7 +70,7 @@ class AddCollaboratorViewModelTest { fun addValidCollaboratorToNoteInTrashShouldTriggerNoteInTrash() = runTest { note.isDeleted = true - viewModel.addCollaborator(noteId, "test@emil.com") + viewModel.addCollaborator(noteId, "test@email.com") assertEquals(AddCollaboratorViewModel.Event.NoteInTrash, viewModel.event.value) } @@ -79,7 +79,7 @@ class AddCollaboratorViewModelTest { fun addValidCollaboratorToNoteDeletedShouldTriggerNoteDeleted() = runTest { whenever(notesBucket.get(any())).thenThrow(BucketObjectMissingException()) - viewModel.addCollaborator(noteId, "test@emil.com") + viewModel.addCollaborator(noteId, "test@email.com") assertEquals(AddCollaboratorViewModel.Event.NoteDeleted, viewModel.event.value) } diff --git a/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModelTest.kt b/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModelTest.kt index 593631715..ac0550d8a 100644 --- a/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModelTest.kt +++ b/Simplenote/src/test/java/com/automattic/simplenote/viewmodels/CollaboratorsViewModelTest.kt @@ -7,6 +7,7 @@ import com.automattic.simplenote.repositories.CollaboratorsRepository import com.automattic.simplenote.viewmodels.CollaboratorsViewModel.Event import com.automattic.simplenote.viewmodels.CollaboratorsViewModel.UiState import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -15,6 +16,8 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mockito.mock +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @@ -27,19 +30,26 @@ class CollaboratorsViewModelTest { private val mockCollaboratorsRepository = mock(CollaboratorsRepository::class.java) private val viewModel = CollaboratorsViewModel(mockCollaboratorsRepository) - private val noteId = "key1" + private val collaboratorFoo = "foo@email.com" + private val collaboratorBar = "bar@em.co.de" + private val collaboratorBaz = "baz@e.c" + private val collaborators = listOf( + collaboratorFoo, + collaboratorBar, + ) + private val noteId = "key123" @Before fun setup() = runTest { whenever(mockCollaboratorsRepository.getCollaborators(noteId)) - .thenReturn(CollaboratorsActionResult.CollaboratorsList(listOf("test@emil.com", "name@example.co.jp"))) + .thenReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) } @Test fun loadCollaboratorsShouldUpdateUiStateWithList() = runTest { viewModel.loadCollaborators(noteId) - val expectedCollaborators = UiState.CollaboratorsList(listOf("test@emil.com", "name@example.co.jp")) + val expectedCollaborators = UiState.CollaboratorsList(collaborators) assertEquals(expectedCollaborators, viewModel.uiState.value) } @@ -50,7 +60,7 @@ class CollaboratorsViewModelTest { viewModel.loadCollaborators(noteId) - assertEquals(UiState.EmptyCollaborators, viewModel.uiState.value) + assertEquals(UiState.EmptyCollaborators(allCollaboratorsRemoved = true), viewModel.uiState.value) } @Test @@ -76,33 +86,33 @@ class CollaboratorsViewModelTest { @Test fun removeCollaboratorShouldReturnListEmails() = runTest { viewModel.loadCollaborators(noteId) - whenever(mockCollaboratorsRepository.removeCollaborator(noteId, "test@emil.com")) - .thenReturn(CollaboratorsActionResult.CollaboratorsList(listOf("name@example.co.jp"))) + whenever(mockCollaboratorsRepository.removeCollaborator(noteId, collaboratorFoo)) + .thenReturn(CollaboratorsActionResult.CollaboratorsList(listOf(collaboratorBar))) - viewModel.removeCollaborator("test@emil.com") + viewModel.removeCollaborator(collaboratorFoo) - val expectedCollaborators = UiState.CollaboratorsList(listOf("name@example.co.jp")) + val expectedCollaborators = UiState.CollaboratorsList(listOf(collaboratorBar)) assertEquals(expectedCollaborators, viewModel.uiState.value) } @Test fun removeLastCollaboratorShouldReturnEmpty() = runTest { viewModel.loadCollaborators(noteId) - whenever(mockCollaboratorsRepository.removeCollaborator(noteId, "test@emil.com")) + whenever(mockCollaboratorsRepository.removeCollaborator(noteId, collaboratorFoo)) .thenReturn(CollaboratorsActionResult.CollaboratorsList(emptyList())) - viewModel.removeCollaborator("test@emil.com") + viewModel.removeCollaborator(collaboratorFoo) - assertEquals(UiState.EmptyCollaborators, viewModel.uiState.value) + assertEquals(UiState.EmptyCollaborators(allCollaboratorsRemoved = true), viewModel.uiState.value) } @Test fun removeCollaboratorForNoteInTrashShouldTriggerEvent() = runTest { viewModel.loadCollaborators(noteId) - whenever(mockCollaboratorsRepository.removeCollaborator(noteId, "test@emil.com")) + whenever(mockCollaboratorsRepository.removeCollaborator(noteId, collaboratorFoo)) .thenReturn(CollaboratorsActionResult.NoteInTrash) - viewModel.removeCollaborator("test@emil.com") + viewModel.removeCollaborator(collaboratorFoo) assertEquals(UiState.NoteInTrash, viewModel.uiState.value) } @@ -110,10 +120,10 @@ class CollaboratorsViewModelTest { @Test fun removeCollaboratorForNoteDeletedShouldTriggerEvent() = runTest { viewModel.loadCollaborators(noteId) - whenever(mockCollaboratorsRepository.removeCollaborator(noteId, "test@emil.com")) + whenever(mockCollaboratorsRepository.removeCollaborator(noteId, collaboratorFoo)) .thenReturn(CollaboratorsActionResult.NoteDeleted) - viewModel.removeCollaborator("test@emil.com") + viewModel.removeCollaborator(collaboratorFoo) assertEquals(UiState.NoteDeleted, viewModel.uiState.value) } @@ -136,7 +146,7 @@ class CollaboratorsViewModelTest { @Test fun clickRemoveCollaboratorShouldTriggerEventAddCollaborator() { - val collaborator = "test@emil.com" + val collaborator = collaboratorFoo viewModel.clickRemoveCollaborator(collaborator) assertEquals(Event.RemoveCollaboratorEvent(collaborator), viewModel.event.value) @@ -172,7 +182,7 @@ class CollaboratorsViewModelTest { // Now mock the flow to emit changes and mock getCollaborators to return a different list // This simulates collaborators being added after we stopped listening whenever(mockCollaboratorsRepository.collaboratorsChanged(noteId)).thenReturn(flow { emit(true) }) - val newList = listOf("test@emil.com", "name@example.co.jp", "new@email.com") + val newList = collaborators + collaboratorBaz whenever(mockCollaboratorsRepository.getCollaborators(noteId)) .thenReturn(CollaboratorsActionResult.CollaboratorsList(newList)) @@ -184,7 +194,7 @@ class CollaboratorsViewModelTest { fun collaboratorAddedShouldUpdateUiState() = runTest { viewModel.loadCollaborators(noteId) whenever(mockCollaboratorsRepository.collaboratorsChanged(noteId)).thenReturn(flow { emit(true) }) - val expectedList = listOf("test@emil.com", "name@example.co.jp", "test2@email.com") + val expectedList = collaborators + collaboratorBaz whenever(mockCollaboratorsRepository.getCollaborators(noteId)) .thenReturn(CollaboratorsActionResult.CollaboratorsList(expectedList)) @@ -192,4 +202,115 @@ class CollaboratorsViewModelTest { assertEquals(UiState.CollaboratorsList(expectedList), viewModel.uiState.value) } + + @Test + fun closeSearchShouldCleanQuery() = runTest { + viewModel.loadCollaborators(noteId) + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId) }.doReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) + } + mockCollaboratorsRepository.stub { + onBlocking { collaboratorsChanged(noteId) }.doReturn(emptyFlow()) + } + viewModel.startListeningChanges() + viewModel.closeSearch() + + assertEquals(UiState.CollaboratorsList(collaborators), viewModel.uiState.value) + } + + @Test + fun removeAllCollaboratorsDuringSearchShouldReturnAllRemoved() = runTest { + viewModel.loadCollaborators(noteId) + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId) }.doReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) + } + mockCollaboratorsRepository.stub { + onBlocking { collaboratorsChanged(noteId) }.doReturn(emptyFlow()) + } + viewModel.startListeningChanges() + + val returnedList = listOf(collaboratorBar) + val searchQuery = "@" + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId, searchQuery) }.doReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) + } + viewModel.search(searchQuery) + mockCollaboratorsRepository.stub { + onBlocking { removeCollaborator(noteId, collaboratorFoo) }.doReturn(CollaboratorsActionResult.CollaboratorsList(returnedList)) + } + viewModel.removeCollaborator(collaboratorFoo) + mockCollaboratorsRepository.stub { + onBlocking { removeCollaborator(noteId, collaboratorBar) }.doReturn(CollaboratorsActionResult.CollaboratorsList(emptyList())) + } + viewModel.removeCollaborator(collaboratorBar) + + assertEquals(UiState.EmptyCollaborators(allCollaboratorsRemoved = true), viewModel.uiState.value) + } + + @Test + fun removeOneCollaboratorDuringSearchShouldNotReturnAllRemoved() = runTest { + viewModel.loadCollaborators(noteId) + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId) }.doReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) + } + mockCollaboratorsRepository.stub { + onBlocking { collaboratorsChanged(noteId) }.doReturn(emptyFlow()) + } + viewModel.startListeningChanges() + + val returnedList = listOf(collaboratorBar) + val searchQuery = "@" + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId, searchQuery) }.doReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) + } + viewModel.search(searchQuery) + mockCollaboratorsRepository.stub { + onBlocking { removeCollaborator(noteId, collaboratorFoo) }.doReturn(CollaboratorsActionResult.CollaboratorsList(returnedList)) + } + viewModel.removeCollaborator(collaboratorFoo) + + assertEquals(UiState.CollaboratorsList(returnedList), viewModel.uiState.value) + } + + @Test + fun searchShouldFilterCollaborators() = runTest { + viewModel.loadCollaborators(noteId) + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId) }.doReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) + } + mockCollaboratorsRepository.stub { + onBlocking { collaboratorsChanged(noteId) }.doReturn(emptyFlow()) + } + viewModel.startListeningChanges() + + val collaborator = collaborators[0] + val filteredList = listOf(collaborator) + val searchQuery = collaborator.substringBefore("@") + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId, searchQuery) }.doReturn(CollaboratorsActionResult.CollaboratorsList(filteredList)) + } + viewModel.search(searchQuery) + + assertEquals(UiState.CollaboratorsList(filteredList, true, searchQuery), viewModel.uiState.value) + } + + @Test + fun searchShouldShowNoCollaboratorsForUniqueQuery() = runTest { + viewModel.loadCollaborators(noteId) + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId) }.doReturn(CollaboratorsActionResult.CollaboratorsList(collaborators)) + } + mockCollaboratorsRepository.stub { + onBlocking { collaboratorsChanged(noteId) }.doReturn(emptyFlow()) + } + viewModel.startListeningChanges() + + val searchQuery = "d34db33f" + mockCollaboratorsRepository.stub { + onBlocking { getCollaborators(noteId, searchQuery) }.doReturn(CollaboratorsActionResult.CollaboratorsList(emptyList())) + } + viewModel.search(searchQuery) + + assertEquals(UiState.EmptyCollaborators(allCollaboratorsRemoved = false, searchUpdate = true), viewModel.uiState.value) + } }