diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt index 1de7a0227965..4dbaacd86039 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt @@ -57,7 +57,7 @@ sealed class PageItem(open val type: Type) { override val type: Type ) : PageItem(type) - data class Divider(val title: String) : PageItem(DIVIDER) + data class Divider(val title: String = "") : PageItem(DIVIDER) data class Empty( @StringRes val textResource: Int = R.string.empty_list_default, diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageListViewModel.kt index 922c71afdaf6..0dbab19b38cc 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageListViewModel.kt @@ -130,7 +130,7 @@ class PageListViewModel @Inject constructor() : ViewModel() { } } else { val pagesWithBottomGap = newPages.toMutableList() - pagesWithBottomGap.addAll(listOf(Divider(""), Divider(""))) + pagesWithBottomGap.addAll(listOf(Divider(), Divider())) _pages.postValue(pagesWithBottomGap) } } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt index 894916f12caf..0ad44f3df3e2 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt @@ -4,7 +4,6 @@ import android.arch.lifecycle.LiveData import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.ViewModel import android.support.annotation.StringRes -import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CoroutineDispatcher import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.delay @@ -19,6 +18,7 @@ import org.wordpress.android.fluxc.model.page.PageModel import org.wordpress.android.fluxc.model.page.PageStatus import org.wordpress.android.fluxc.store.PageStore import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded +import org.wordpress.android.modules.COMMON_POOL_CONTEXT import org.wordpress.android.modules.UI_CONTEXT import org.wordpress.android.ui.pages.PageItem.Action import org.wordpress.android.ui.pages.PageItem.Action.DELETE_PERMANENTLY @@ -47,12 +47,17 @@ import javax.inject.Inject import javax.inject.Named import kotlin.coroutines.experimental.Continuation +private const val ACTION_DELAY = 100 +private const val SEARCH_DELAY = 200 +private const val SEARCH_COLLAPSE_DELAY = 500 + class PagesViewModel @Inject constructor( private val pageStore: PageStore, private val dispatcher: Dispatcher, private val actionPerfomer: ActionPerformer, - @Named(UI_CONTEXT) private val uiContext: CoroutineDispatcher + @Named(UI_CONTEXT) private val uiContext: CoroutineDispatcher, + @Named(COMMON_POOL_CONTEXT) private val commonPoolContext: CoroutineDispatcher ) : ViewModel() { private val _isSearchExpanded = MutableLiveData() val isSearchExpanded: LiveData = _isSearchExpanded @@ -84,14 +89,10 @@ class PagesViewModel private val _setPageParent = SingleLiveEvent() val setPageParent: LiveData = _setPageParent - private var _pageMap: MutableMap = mutableMapOf() - private var pageMap: MutableMap - get() { - return _pageMap - } + private var pageMap: Map = mapOf() set(value) { - _pageMap = value - _pages.postValue(pageMap.values.toList()) + field = value + _pages.postValue(field.values.toList()) if (isSearchExpanded.value == true) { onSearch(lastSearchQuery) @@ -137,8 +138,7 @@ class PagesViewModel actionPerfomer.onCleanup() } - private fun reloadPagesAsync() = launch(CommonPool) { - pageMap = pageStore.getPagesFromDb(site).associateBy { it.remoteId }.toMutableMap() + private fun reloadPagesAsync() = launch(commonPoolContext) { refreshPages() val loadState = if (pageMap.isEmpty()) FETCHING else REFRESHING @@ -160,11 +160,11 @@ class PagesViewModel } private suspend fun refreshPages() { - pageMap = pageStore.getPagesFromDb(site).associateBy { it.remoteId }.toMutableMap() + pageMap = pageStore.getPagesFromDb(site).associateBy { it.remoteId } } fun onPageEditFinished(pageId: Long) { - launch { + launch(uiContext) { refreshPages() // show local changes immediately waitForPageUpdate(pageId) reloadPages() @@ -179,7 +179,7 @@ class PagesViewModel } fun onPageParentSet(pageId: Long, parentId: Long) { - launch { + launch(uiContext) { pageMap[pageId]?.let { page -> setParent(page, parentId) } @@ -198,11 +198,11 @@ class PagesViewModel _isNewPageButtonVisible.postOnUi(isNotEmpty && hasNoExceptions) } - fun onSearch(searchQuery: String) { + fun onSearch(searchQuery: String, delay: Int = SEARCH_DELAY) { searchJob?.cancel() if (searchQuery.isNotEmpty()) { - searchJob = launch { - delay(200) + searchJob = launch(uiContext) { + delay(delay) searchJob = null if (isActive) { _lastSearchQuery = searchQuery @@ -218,7 +218,7 @@ class PagesViewModel private suspend fun groupedSearch( site: SiteModel, searchQuery: String - ): SortedMap> = withContext(CommonPool) { + ): SortedMap> = withContext(commonPoolContext) { val list = pageStore.search(site, searchQuery).groupBy { PageListType.fromPageStatus(it.status) } return@withContext list.toSortedMap( Comparator { previous, next -> @@ -250,8 +250,8 @@ class PagesViewModel _isSearchExpanded.value = false clearSearch() - launch { - delay(500) + launch(uiContext) { + delay(SEARCH_COLLAPSE_DELAY) checkIfNewPageButtonShouldBeVisible() } return true @@ -275,7 +275,7 @@ class PagesViewModel } fun onDeleteConfirmed(remoteId: Long) { - launch { + launch(commonPoolContext) { pageMap[remoteId]?.let { deletePage(it) } } } @@ -289,7 +289,7 @@ class PagesViewModel } fun onPullToRefresh() { - launch { + launch(uiContext) { reloadPages(FETCHING) } } @@ -298,54 +298,59 @@ class PagesViewModel val oldParent = page.parent?.remoteId ?: 0 val action = PageAction(UPLOAD) { - launch(CommonPool) { + launch(commonPoolContext) { if (page.parent?.remoteId != parentId) { - page.parent = _pageMap[parentId] - pageMap = _pageMap + val updatedPage = updateParent(page, parentId) - pageStore.uploadPageToServer(page) + pageStore.uploadPageToServer(updatedPage) } } } action.undo = { - launch(CommonPool) { + launch(commonPoolContext) { pageMap[page.remoteId]?.let { changed -> - changed.parent = _pageMap[oldParent] - pageMap = _pageMap + val updatedPage = updateParent(changed, oldParent) - pageStore.uploadPageToServer(changed) + pageStore.uploadPageToServer(updatedPage) } } } action.onSuccess = { - launch(CommonPool) { + launch(commonPoolContext) { reloadPages() - delay(100) + delay(ACTION_DELAY) _showSnackbarMessage.postValue( SnackbarMessageHolder(string.page_parent_changed, string.undo, action.undo)) } } action.onError = { - launch(CommonPool) { + launch(commonPoolContext) { refreshPages() _showSnackbarMessage.postValue(SnackbarMessageHolder(string.page_parent_change_error)) } } - launch { + launch(uiContext) { _arePageActionsEnabled = false actionPerfomer.performAction(action) _arePageActionsEnabled = true } } + private fun updateParent(page: PageModel, parentId: Long): PageModel { + val updatedPage = page.copy(parent = pageMap[parentId]) + val updatedMap = pageMap.toMutableMap() + updatedMap[page.remoteId] = updatedPage + pageMap = updatedMap + return updatedPage + } + private fun deletePage(page: PageModel) { val action = PageAction(REMOVE) { - launch(CommonPool) { - _pageMap.remove(page.remoteId) - pageMap = _pageMap + launch(commonPoolContext) { + pageMap = pageMap.filter { it.key != page.remoteId } checkIfNewPageButtonShouldBeVisible() @@ -353,15 +358,15 @@ class PagesViewModel } } action.onSuccess = { - launch(CommonPool) { - delay(100) + launch(commonPoolContext) { + delay(ACTION_DELAY) reloadPages() _showSnackbarMessage.postValue(SnackbarMessageHolder(string.page_permanently_deleted)) } } action.onError = { - launch(CommonPool) { + launch(commonPoolContext) { refreshPages() _showSnackbarMessage.postValue(SnackbarMessageHolder(string.page_delete_error)) @@ -377,26 +382,26 @@ class PagesViewModel pageMap[remoteId]?.let { page -> val oldStatus = page.status val action = PageAction(UPLOAD) { - page.status = status - launch(CommonPool) { - pageStore.updatePageInDb(page) + val updatedPage = updatePageStatus(page, status) + launch(commonPoolContext) { + pageStore.updatePageInDb(updatedPage) refreshPages() - pageStore.uploadPageToServer(page) + pageStore.uploadPageToServer(updatedPage) } } action.undo = { - page.status = oldStatus - launch(CommonPool) { - pageStore.updatePageInDb(page) + val updatedPage = updatePageStatus(page, oldStatus) + launch(commonPoolContext) { + pageStore.updatePageInDb(updatedPage) refreshPages() - pageStore.uploadPageToServer(page) + pageStore.uploadPageToServer(updatedPage) } } action.onSuccess = { - launch(CommonPool) { - delay(100) + launch(commonPoolContext) { + delay(ACTION_DELAY) reloadPages() val message = prepareStatusChangeSnackbar(status, action.undo) @@ -404,14 +409,14 @@ class PagesViewModel } } action.onError = { - launch(CommonPool) { + launch(commonPoolContext) { action.undo() _showSnackbarMessage.postValue(SnackbarMessageHolder(string.page_status_change_error)) } } - launch { + launch(uiContext) { _arePageActionsEnabled = false actionPerfomer.performAction(action) _arePageActionsEnabled = true @@ -419,6 +424,14 @@ class PagesViewModel } } + private fun updatePageStatus(page: PageModel, oldStatus: PageStatus): PageModel { + val updatedPage = page.copy(status = oldStatus) + val updatedMap = pageMap.toMutableMap() + updatedMap[page.remoteId] = updatedPage + pageMap = updatedMap + return updatedPage + } + private fun prepareStatusChangeSnackbar(newStatus: PageStatus, undo: (() -> Unit)? = null): SnackbarMessageHolder { val message = when (newStatus) { PageStatus.DRAFT -> string.page_moved_to_draft diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/SearchListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/SearchListViewModel.kt index 15cd945e2d20..221747bbe4da 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/SearchListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/SearchListViewModel.kt @@ -4,10 +4,12 @@ import android.arch.lifecycle.LiveData import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.Observer import android.arch.lifecycle.ViewModel +import kotlinx.coroutines.experimental.CoroutineDispatcher import kotlinx.coroutines.experimental.launch import org.wordpress.android.R.string import org.wordpress.android.fluxc.model.page.PageModel import org.wordpress.android.fluxc.model.page.PageStatus +import org.wordpress.android.modules.UI_CONTEXT import org.wordpress.android.ui.pages.PageItem import org.wordpress.android.ui.pages.PageItem.Action import org.wordpress.android.ui.pages.PageItem.Divider @@ -21,9 +23,13 @@ import org.wordpress.android.viewmodel.ResourceProvider import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListType import java.util.SortedMap import javax.inject.Inject +import javax.inject.Named class SearchListViewModel -@Inject constructor(private val resourceProvider: ResourceProvider) : ViewModel() { +@Inject constructor( + private val resourceProvider: ResourceProvider, + @Named(UI_CONTEXT) private val uiContext: CoroutineDispatcher +) : ViewModel() { private val _searchResult: MutableLiveData> = MutableLiveData() val searchResult: LiveData> = _searchResult @@ -50,7 +56,7 @@ class SearchListViewModel pagesViewModel.checkIfNewPageButtonShouldBeVisible() } else { - _searchResult.postValue(listOf(Empty(string.pages_search_suggestion, true))) + _searchResult.value = listOf(Empty(string.pages_search_suggestion, true)) } } @@ -62,7 +68,7 @@ class SearchListViewModel pagesViewModel.onItemTapped(pageItem) } - private fun loadFoundPages(pages: SortedMap>) = launch { + private fun loadFoundPages(pages: SortedMap>) = launch(uiContext) { if (pages.isNotEmpty()) { val pageItems = pages .map { (listType, results) -> @@ -73,9 +79,9 @@ class SearchListViewModel acc.addAll(list) return@fold acc } - _searchResult.postValue(pageItems) + _searchResult.value = pageItems } else { - _searchResult.postValue(listOf(Empty(string.pages_empty_search_result, true))) + _searchResult.value = listOf(Empty(string.pages_empty_search_result, true)) } } diff --git a/WordPress/src/test/java/org/wordpress/android/CoroutinesUtils.kt b/WordPress/src/test/java/org/wordpress/android/CoroutinesUtils.kt new file mode 100644 index 000000000000..2e96de55721d --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/CoroutinesUtils.kt @@ -0,0 +1,10 @@ +package org.wordpress.android + +import kotlinx.coroutines.experimental.CoroutineScope +import kotlinx.coroutines.experimental.runBlocking +import kotlin.coroutines.experimental.CoroutineContext +import kotlin.coroutines.experimental.EmptyCoroutineContext + +fun test(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T) { + runBlocking(context, block) +} diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/LiveDataUtils.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/LiveDataUtils.kt deleted file mode 100644 index 9666c754ee57..000000000000 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/LiveDataUtils.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.wordpress.android.viewmodel - -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.Observer -import kotlinx.coroutines.experimental.CommonPool -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch -import java.util.concurrent.TimeoutException -import kotlin.coroutines.experimental.Continuation -import kotlin.coroutines.experimental.suspendCoroutine - -fun LiveData.test(): TestObserver { - val mutableLiveData = MutableLiveData() - this.observeForever { mutableLiveData.postValue(it) } - mutableLiveData.value = null - val observer = TestObserver() - mutableLiveData.observeForever(observer) - return observer -} - -fun LiveData.start(): LiveData { - val mutableLiveData = MutableLiveData() - this.observeForever { mutableLiveData.postValue(it) } - mutableLiveData.value = null - return mutableLiveData -} - -class TestObserver : Observer { - private val values = mutableListOf() - private var requiredItemCount: Int = -1 - private var continuation: Continuation>? = null - private var nullableContinuation: Continuation>? = null - override fun onChanged(t: T?) { - values.add(t) - if (values.size >= requiredItemCount) { - nullableContinuation?.resume(values.toList()) - nullableContinuation = null - } - if (t != null) { - val nonNullValues = nonNullValues() - if (nonNullValues.size >= requiredItemCount) { - continuation?.resume(nonNullValues) - continuation = null - } - } - } - - suspend fun await(timeout: Int = 1000): T { - return awaitValues(1, timeout)[0] - } - - suspend fun awaitValues(count: Int, timeout: Int = 1000) = suspendCoroutine> { - requiredItemCount = count - continuation = it - launch(CommonPool) { - delay(timeout) - continuation?.resumeWithException(TimeoutException()) - continuation = null - } - val nonNullValues = nonNullValues() - if (nonNullValues.size >= count) { - it.resume(nonNullValues) - continuation = null - } - } - - suspend fun awaitNullableValues(count: Int, timeout: Int = 1000) = suspendCoroutine> { - requiredItemCount = count - continuation = it - launch(CommonPool) { - delay(timeout) - nullableContinuation?.resumeWithException(TimeoutException()) - nullableContinuation = null - } - if (values.size >= count) { - it.resume(values) - continuation = null - } - } - - private fun nonNullValues(): List { - return values.filter { it != null }.map { it!! } - } -} diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt index 8c79530dd6dd..76c37f5cabc0 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.experimental.Unconfined import kotlinx.coroutines.experimental.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -19,11 +18,15 @@ import org.wordpress.android.fluxc.model.page.PageModel import org.wordpress.android.fluxc.model.page.PageStatus.DRAFT import org.wordpress.android.fluxc.store.PageStore import org.wordpress.android.fluxc.store.PostStore.OnPostChanged +import org.wordpress.android.test +import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListState import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListState.DONE import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListState.FETCHING import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListState.REFRESHING -import org.wordpress.android.viewmodel.test +import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListType +import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListType.DRAFTS import java.util.Date +import java.util.SortedMap @RunWith(MockitoJUnitRunner::class) class PagesViewModelTest { @@ -35,43 +38,99 @@ class PagesViewModelTest { @Mock lateinit var dispatcher: Dispatcher @Mock lateinit var actionPerformer: ActionPerformer private lateinit var viewModel: PagesViewModel + private lateinit var listStates: MutableList + private lateinit var pages: MutableList> + private lateinit var searchPages: MutableList>> + private lateinit var pageModel: PageModel @Before fun setUp() { - viewModel = PagesViewModel(pageStore, dispatcher, actionPerformer, Unconfined) + viewModel = PagesViewModel(pageStore, dispatcher, actionPerformer, Unconfined, Unconfined) + listStates = mutableListOf() + pages = mutableListOf() + searchPages = mutableListOf() + viewModel.listState.observeForever { if (it != null) listStates.add(it) } + viewModel.pages.observeForever { if (it != null) pages.add(it) } + viewModel.searchPages.observeForever { if (it != null) searchPages.add(it) } + pageModel = PageModel(site, 1, "title", DRAFT, Date(), false, 1, null) } - @Ignore @Test - fun clearsResultAndLoadsDataOnStart() = runBlocking { - whenever(pageStore.getPagesFromDb(site)).thenReturn(listOf( - PageModel(site, 1, "title", DRAFT, Date(), false, 1, null)) - ) + fun clearsResultAndLoadsDataOnStart() = test { + val pageModel = initPageRepo() whenever(pageStore.requestPagesFromServer(any())).thenReturn(OnPostChanged(1, false)) - val listStateObserver = viewModel.listState.test() - val refreshPagesObserver = viewModel.pages.test() viewModel.start(site) - val listStates = listStateObserver.awaitValues(2) - assertThat(listStates).containsExactly(REFRESHING, DONE) - refreshPagesObserver.awaitNullableValues(2) + assertThat(pages).hasSize(2) + assertThat(pages.last()).containsOnly(pageModel) + } + + private suspend fun initPageRepo(): PageModel { + val expectedPages = listOf( + pageModel + ) + whenever(pageStore.getPagesFromDb(site)).thenReturn( + expectedPages + ) + return pageModel } - @Ignore @Test - fun onSiteWithoutPages() = runBlocking { + fun onSiteWithoutPages() = test { whenever(pageStore.getPagesFromDb(site)).thenReturn(emptyList()) whenever(pageStore.requestPagesFromServer(any())).thenReturn(OnPostChanged(0, false)) - val listStateObserver = viewModel.listState.test() - val refreshPagesObserver = viewModel.pages.test() viewModel.start(site) - val listStates = listStateObserver.awaitValues(2) - assertThat(listStates).containsExactly(FETCHING, DONE) - refreshPagesObserver.awaitNullableValues(2) + assertThat(pages).hasSize(2) + } + + @Test + fun onSearchReturnsResultsFromStore() = test { + initSearch() + val query = "query" + val drafts = listOf(PageModel(site, 1, "title", DRAFT, Date(), false, 1, null)) + val expectedResult = sortedMapOf(DRAFTS to drafts) + whenever(pageStore.search(site, query)).thenReturn(drafts) + + viewModel.onSearch(query, 0) + + val result = viewModel.searchPages.value + + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun onEmptySearchResultEmitsEmptyItem() = runBlocking { + initSearch() + val query = "query" + whenever(pageStore.search(site, query)).thenReturn(listOf()) + + viewModel.onSearch(query, 0) + + val result = viewModel.searchPages.value + + assertThat(result).isEmpty() + } + + @Test + fun onEmptyQueryClearsSearch() = runBlocking { + initSearch() + val query = "" + + viewModel.onSearch(query, 0) + + val result = viewModel.searchPages.value + + assertThat(result).isNull() + } + + private suspend fun initSearch() { + whenever(pageStore.getPagesFromDb(site)).thenReturn(listOf()) + whenever(pageStore.requestPagesFromServer(any())).thenReturn(OnPostChanged(0, false)) + viewModel.start(site) } } diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/SearchListViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/SearchListViewModelTest.kt index 5382af310947..dfa58a182c32 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/SearchListViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/SearchListViewModelTest.kt @@ -1,108 +1,129 @@ package org.wordpress.android.viewmodel.pages import android.arch.core.executor.testing.InstantTaskExecutorRule +import android.arch.lifecycle.MutableLiveData import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever import kotlinx.coroutines.experimental.Unconfined -import kotlinx.coroutines.experimental.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.wordpress.android.R.string -import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.page.PageModel import org.wordpress.android.fluxc.model.page.PageStatus.DRAFT -import org.wordpress.android.fluxc.store.PageStore -import org.wordpress.android.fluxc.store.PostStore.OnPostChanged +import org.wordpress.android.fluxc.model.page.PageStatus.PUBLISHED +import org.wordpress.android.ui.pages.PageItem +import org.wordpress.android.ui.pages.PageItem.Action.VIEW_PAGE import org.wordpress.android.ui.pages.PageItem.Divider import org.wordpress.android.ui.pages.PageItem.DraftPage import org.wordpress.android.ui.pages.PageItem.Empty +import org.wordpress.android.ui.pages.PageItem.PublishedPage import org.wordpress.android.viewmodel.ResourceProvider -import org.wordpress.android.viewmodel.test +import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListType import java.util.Date +import java.util.SortedMap @RunWith(MockitoJUnitRunner::class) class SearchListViewModelTest { @Rule @JvmField val rule = InstantTaskExecutorRule() - @Mock lateinit var pageStore: PageStore - @Mock lateinit var site: SiteModel @Mock lateinit var resourceProvider: ResourceProvider - @Mock lateinit var dispatcher: Dispatcher - @Mock lateinit var actionPerformer: ActionPerformer + @Mock lateinit var site: SiteModel + @Mock lateinit var pagesViewModel: PagesViewModel + + private lateinit var searchPages: MutableLiveData>> private lateinit var viewModel: SearchListViewModel - private lateinit var pagesViewModel: PagesViewModel + + private lateinit var page: PageModel @Before fun setUp() { - viewModel = SearchListViewModel(resourceProvider) - pagesViewModel = PagesViewModel(pageStore, dispatcher, actionPerformer, Unconfined) + page = PageModel(site, 1, "title", PUBLISHED, Date(), false, 11L, null) + viewModel = SearchListViewModel(resourceProvider, Unconfined) + searchPages = MutableLiveData() + whenever(pagesViewModel.searchPages).thenReturn(searchPages) + viewModel.start(pagesViewModel) } - @Ignore @Test - fun onSearchReturnsResultsFromStore() = runBlocking { - initSearch() - whenever(resourceProvider.getString(string.pages_drafts)).thenReturn("Drafts") - val query = "query" - val title = "title" - val drafts = listOf( PageModel(site, 1, "title", DRAFT, Date(), false, 1, null)) - val expectedResult = listOf(Divider("Drafts"), DraftPage(1, title)) - whenever(pageStore.search(site, query)).thenReturn(drafts) - - val observer = viewModel.searchResult.test() - - pagesViewModel.onSearch(query) - - val result = observer.await() + fun `show empty item on start`() { + searchPages.value = null - assertThat(result).isEqualTo(expectedResult) + assertThat(viewModel.searchResult.value).containsOnly(Empty(string.pages_search_suggestion, true)) } - @Ignore @Test - fun onEmptySearchResultEmitsEmptyItem() = runBlocking { - initSearch() - val query = "query" - val pageItems = listOf(Empty(string.pages_empty_search_result, true)) - whenever(pageStore.search(site, query)).thenReturn(listOf()) + fun `adds divider to published group`() { + val expectedTitle = "title" + for (status in PageListType.values()) { + whenever(resourceProvider.getString(status.title)).thenReturn(expectedTitle) - val observer = viewModel.searchResult.test() + searchPages.value = sortedMapOf(status to listOf()) - pagesViewModel.onSearch(query) - - val result = observer.await() + assertThat(viewModel.searchResult.value).containsOnly(Divider(expectedTitle)) + } + } - assertThat(result).isEqualTo(pageItems) + @Test + fun `builds list with dividers from grouped result`() { + val expectedTitle = "title" + whenever(resourceProvider.getString(any())).thenReturn(expectedTitle) + + val publishedPageId = 1 + val publishedPageRemoteId = 11L + val publishedPage = page.copy(pageId = publishedPageId, remoteId = publishedPageRemoteId, status = PUBLISHED) + val publishedList = PageListType.PUBLISHED to listOf(publishedPage) + val draftPageId = 2 + val draftPageRemoteId = 22L + val draftPage = page.copy(pageId = draftPageId, remoteId = draftPageRemoteId, status = DRAFT) + val draftList = PageListType.DRAFTS to listOf(draftPage) + + searchPages.value = sortedMapOf(publishedList, draftList) + + val searchResult = viewModel.searchResult.value + assertThat(searchResult).isNotNull + assertThat(searchResult).hasSize(4) + assertThat(searchResult!![0]).isInstanceOf(Divider::class.java) + (searchResult[0] as Divider).apply { + assertThat(this.title).isEqualTo(expectedTitle) + } + assertThat(searchResult[1]).isInstanceOf(PublishedPage::class.java) + (searchResult[1] as PublishedPage).apply { + assertThat(this.id).isEqualTo(publishedPageRemoteId) + } + assertThat(searchResult[2]).isInstanceOf(Divider::class.java) + (searchResult[2] as Divider).apply { + assertThat(this.title).isEqualTo(expectedTitle) + } + assertThat(searchResult[3]).isInstanceOf(DraftPage::class.java) + (searchResult[3] as DraftPage).apply { + assertThat(this.id).isEqualTo(draftPageRemoteId) + } } - @Ignore @Test - fun onEmptyQueryClearsSearch() = runBlocking { - initSearch() - val query = "" - val pageItems = listOf(Empty(string.pages_search_suggestion, true)) + fun `passes action to page view model on menu action`() { + val clickedPage = PageItem.PublishedPage(1, "title", listOf(), 0, false) + val action = VIEW_PAGE - val observer = viewModel.searchResult.test() + viewModel.onMenuAction(action, clickedPage) - pagesViewModel.onSearch(query) + verify(pagesViewModel).onMenuAction(action, clickedPage) + } - val result = observer.await() + @Test + fun `passes page to page view model on item tapped`() { + val clickedPage = PageItem.PublishedPage(1, "title", listOf(), 0, false) - assertThat(result).isEqualTo(pageItems) - } + viewModel.onItemTapped(clickedPage) - private suspend fun initSearch() { - whenever(pageStore.getPagesFromDb(site)).thenReturn(listOf()) - whenever(pageStore.requestPagesFromServer(any())).thenReturn(OnPostChanged(0, false)) - pagesViewModel.start(site) - viewModel.start(pagesViewModel) + verify(pagesViewModel).onItemTapped(clickedPage) } }