#3760 Convert SearchCategoriesFragment to use Pagination (#3770)

This commit is contained in:
Seán Mac Gillicuddy 2020-06-10 13:57:13 +01:00 committed by GitHub
parent 63ab4a25aa
commit f4d81eb4ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 956 additions and 685 deletions

View file

@ -1,11 +1,10 @@
package fr.free.nrw.commons.explore.depictions
package fr.free.nrw.commons.explore
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
@ -14,25 +13,25 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class SearchDepictionsFragmentPresenterTest {
class BaseSearchPresenterTest {
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
internal lateinit var view: SearchDepictionsFragmentContract.View
internal lateinit var view: SearchFragmentContract.View<String>
private lateinit var searchDepictionsFragmentPresenter: SearchDepictionsFragmentPresenter
private lateinit var baseSearchPresenter: BaseSearchPresenter<String>
private lateinit var testScheduler: TestScheduler
@Mock
private lateinit var searchableDepictionsDataSourceFactory: SearchableDepictionsDataSourceFactory
private lateinit var pageableDataSource: PageableDataSource<String>
private var loadingStates: PublishProcessor<LoadingState> = PublishProcessor.create()
private var searchResults: PublishProcessor<LiveData<PagedList<DepictedItem>>> =
private var searchResults: PublishProcessor<LiveData<PagedList<String>>> =
PublishProcessor.create()
private var noItemLoadedEvent: PublishProcessor<Unit> = PublishProcessor.create()
@ -41,21 +40,19 @@ class SearchDepictionsFragmentPresenterTest {
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(searchableDepictionsDataSourceFactory.searchResults).thenReturn(searchResults)
whenever(searchableDepictionsDataSourceFactory.loadingStates).thenReturn(loadingStates)
whenever(searchableDepictionsDataSourceFactory.noItemsLoadedEvent)
whenever(pageableDataSource.searchResults).thenReturn(searchResults)
whenever(pageableDataSource.loadingStates).thenReturn(loadingStates)
whenever(pageableDataSource.noItemsLoadedEvent)
.thenReturn(noItemLoadedEvent)
testScheduler = TestScheduler()
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
testScheduler,
searchableDepictionsDataSourceFactory
)
searchDepictionsFragmentPresenter.onAttachView(view)
baseSearchPresenter =
object : BaseSearchPresenter<String>(testScheduler, pageableDataSource) {}
baseSearchPresenter.onAttachView(view)
}
@Test
fun `searchResults emission updates the view`() {
val pagedListLiveData = mock<LiveData<PagedList<DepictedItem>>>()
val pagedListLiveData = mock<LiveData<PagedList<String>>>()
searchResults.offer(pagedListLiveData)
verify(view).observeSearchResults(pagedListLiveData)
}
@ -63,14 +60,13 @@ class SearchDepictionsFragmentPresenterTest {
@Test
fun `Loading offers a loading list item`() {
onLoadingState(LoadingState.Loading)
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(listOf(FooterItem.LoadingItem))
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.LoadingItem))
}
@Test
fun `Complete offers an empty list item and hides initial loader`() {
onLoadingState(LoadingState.Complete)
searchDepictionsFragmentPresenter.listFooterData.test()
baseSearchPresenter.listFooterData.test()
.assertValue(emptyList())
verify(view).hideInitialLoadProgress()
}
@ -83,13 +79,12 @@ class SearchDepictionsFragmentPresenterTest {
@Test
fun `Error offers a refresh list item, hides initial loader and shows error with a set text`() {
searchDepictionsFragmentPresenter.onQueryUpdated("test")
baseSearchPresenter.onQueryUpdated("test")
onLoadingState(LoadingState.Error)
verify(view).setEmptyViewText("test")
verify(view).showSnackbar()
verify(view).hideInitialLoadProgress()
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(listOf(FooterItem.RefreshItem))
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem))
}
@Test
@ -98,35 +93,33 @@ class SearchDepictionsFragmentPresenterTest {
verify(view, never()).setEmptyViewText(any())
verify(view).showSnackbar()
verify(view).hideInitialLoadProgress()
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(listOf(FooterItem.RefreshItem))
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem))
}
@Test
fun `no Items event sets empty view text`() {
searchDepictionsFragmentPresenter.onQueryUpdated("test")
baseSearchPresenter.onQueryUpdated("test")
noItemLoadedEvent.offer(Unit)
verify(view).setEmptyViewText("test")
}
@Test
fun `retryFailedRequest calls retry`() {
searchDepictionsFragmentPresenter.retryFailedRequest()
verify(searchableDepictionsDataSourceFactory).retryFailedRequest()
baseSearchPresenter.retryFailedRequest()
verify(pageableDataSource).retryFailedRequest()
}
@Test
fun `onDetachView stops subscriptions`() {
searchDepictionsFragmentPresenter.onDetachView()
baseSearchPresenter.onDetachView()
onLoadingState(LoadingState.Loading)
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(emptyList())
baseSearchPresenter.listFooterData.test().assertValue(emptyList())
}
@Test
fun `onQueryUpdated updates dataSourceFactory`() {
searchDepictionsFragmentPresenter.onQueryUpdated("test")
verify(searchableDepictionsDataSourceFactory).onQueryUpdated("test")
baseSearchPresenter.onQueryUpdated("test")
verify(pageableDataSource).onQueryUpdated("test")
}
private fun onLoadingState(loadingState: LoadingState) {

View file

@ -0,0 +1,73 @@
package fr.free.nrw.commons.explore
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.explore.depictions.LoadFunction
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class PageableDataSourceTest {
@Mock
private lateinit var liveDataConverter: LiveDataConverter
private lateinit var pageableDataSource: PageableDataSource<String>
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
pageableDataSource = object: PageableDataSource<String>(liveDataConverter){
override val loadFunction: LoadFunction<String>
get() = mock()
}
}
@Test
fun `onQueryUpdated emits new liveData`() {
val (_, liveData) = expectNewLiveData()
pageableDataSource.searchResults.test()
.also { pageableDataSource.onQueryUpdated("test") }
.assertValue(liveData)
}
@Test
fun `onQueryUpdated invokes livedatconverter with no items emitter`() {
val (zeroItemsFuncCaptor, _) = expectNewLiveData()
pageableDataSource.onQueryUpdated("test")
pageableDataSource.noItemsLoadedEvent.test()
.also { zeroItemsFuncCaptor.firstValue.invoke() }
.assertValue(Unit)
}
/*
* Just for coverage, no way to really assert this
* */
@Test
fun `retryFailedRequest does nothing without a factory`() {
pageableDataSource.retryFailedRequest()
}
@Test
@Ignore("Rewrite with Mockk constructor mocks")
fun `retryFailedRequest retries with a factory`() {
val (_, _, dataSourceFactoryCaptor) = expectNewLiveData()
pageableDataSource.onQueryUpdated("test")
val dataSourceFactory = spy(dataSourceFactoryCaptor.firstValue)
pageableDataSource.retryFailedRequest()
verify(dataSourceFactory).retryFailedRequest()
}
private fun expectNewLiveData(): Triple<KArgumentCaptor<() -> Unit>, LiveData<PagedList<String>>, KArgumentCaptor<SearchDataSourceFactory<String>>> {
val captor = argumentCaptor<() -> Unit>()
val dataSourceFactoryCaptor = argumentCaptor<SearchDataSourceFactory<String>>()
val liveData: LiveData<PagedList<String>> = mock()
whenever(liveDataConverter.convert(dataSourceFactoryCaptor.capture(), captor.capture()))
.thenReturn(liveData)
return Triple(captor, liveData, dataSourceFactoryCaptor)
}
}

View file

@ -0,0 +1,58 @@
package fr.free.nrw.commons.explore
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.verify
import fr.free.nrw.commons.explore.depictions.DepictsClient
import io.reactivex.processors.PublishProcessor
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.instanceOf
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
class SearchDataSourceFactoryTest {
@Mock
private lateinit var depictsClient: DepictsClient
@Mock
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var factory: SearchDataSourceFactory<String>
private var function: (Int, Int) -> List<String> = mock()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
factory = object : SearchDataSourceFactory<String>(loadingStates) {
override val loadFunction get() = function
}
}
@Test
fun `create returns a dataSource`() {
assertThat(
factory.create(),
instanceOf(SearchDataSource::class.java)
)
}
@Test
@Ignore("Rewrite with Mockk constructor mocks")
fun `retryFailedRequest invokes method if not null`() {
val spyFactory = spy(factory)
val dataSource = mock<SearchDataSource<String>>()
Mockito.doReturn(dataSource).`when`(spyFactory).create()
factory.retryFailedRequest()
verify(dataSource).retryFailedRequest()
}
@Test
fun `retryFailedRequest does not invoke method if null`() {
factory.retryFailedRequest()
}
}

View file

@ -0,0 +1,120 @@
package fr.free.nrw.commons.explore
import androidx.paging.PositionalDataSource
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.explore.depictions.LoadingStates
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class SearchDataSourceTest {
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var searchDepictionsDataSource: TestSearchDataSource
@Mock
private lateinit var mockGetItems: MockGetItems
@Before
fun setUp() {
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
MockitoAnnotations.initMocks(this)
loadingStates = PublishProcessor.create()
searchDepictionsDataSource =
TestSearchDataSource(
loadingStates,
mockGetItems
)
}
@After
fun tearDown() {
RxJavaPlugins.reset()
}
@Test
fun `loadInitial returns results and emits InitialLoad & Complete`() {
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
val callback = mock<PositionalDataSource.LoadInitialCallback<String>>()
whenever(mockGetItems.getItems(1, 0)).thenReturn(emptyList())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadInitial(params, callback)
verify(callback).onResult(emptyList(), 0)
testSubscriber.assertValues(LoadingState.InitialLoad, LoadingState.Complete)
}
@Test
fun `loadInitial onError does not return results and emits InitialLoad & Error`() {
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
val callback = mock<PositionalDataSource.LoadInitialCallback<String>>()
whenever(mockGetItems.getItems(1, 0)).thenThrow(RuntimeException())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadInitial(params, callback)
verify(callback, never()).onResult(any(), any())
testSubscriber.assertValues(LoadingState.InitialLoad, LoadingState.Error)
}
@Test
fun `loadRange returns results and emits Loading & Complete`() {
val callback: PositionalDataSource.LoadRangeCallback<String> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(mockGetItems.getItems(params.loadSize, params.startPosition))
.thenReturn(emptyList())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback).onResult(emptyList())
testSubscriber.assertValues(LoadingState.Loading, LoadingState.Complete)
}
@Test
fun `loadRange onError does not return results and emits Loading & Error`() {
val callback: PositionalDataSource.LoadRangeCallback<String> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(mockGetItems.getItems(params.loadSize, params.startPosition))
.thenThrow(RuntimeException())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback, never()).onResult(any())
testSubscriber.assertValues(LoadingState.Loading, LoadingState.Error)
}
@Test
fun `retryFailedRequest does nothing when null`() {
searchDepictionsDataSource.retryFailedRequest()
verifyNoMoreInteractions(mockGetItems)
}
@Test
fun `retryFailedRequest retries last request`() {
val callback: PositionalDataSource.LoadRangeCallback<String> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(mockGetItems.getItems(params.loadSize, params.startPosition))
.thenThrow(RuntimeException()).thenReturn(emptyList())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback, never()).onResult(any())
searchDepictionsDataSource.retryFailedRequest()
verify(callback).onResult(emptyList())
testSubscriber.assertValues(
LoadingState.Loading,
LoadingState.Error,
LoadingState.Loading,
LoadingState.Complete
)
}
}
class TestSearchDataSource(loadingStates: LoadingStates, val mockGetItems: MockGetItems) :
SearchDataSource<String>(loadingStates) {
override fun getItems(loadSize: Int, startPosition: Int): List<String> =
mockGetItems.getItems(loadSize, startPosition)
}
interface MockGetItems {
fun getItems(loadSize: Int, startPosition: Int): List<String>
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.explore.categroies
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.category.CategoryClient
import fr.free.nrw.commons.explore.categories.PageableCategoriesDataSource
import io.reactivex.Observable
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Test
class PageableCategoriesDataSourceTest {
@Test
fun `loadFunction loads categories`() {
val categoryClient: CategoryClient = mock()
whenever(categoryClient.searchCategories("test", 0, 1))
.thenReturn(Observable.just(emptyList()))
val pageableCategoriesDataSource = PageableCategoriesDataSource(mock(), categoryClient)
pageableCategoriesDataSource.onQueryUpdated("test")
assertThat(pageableCategoriesDataSource.loadFunction(0, 1), Matchers.`is`(emptyList()))
}
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.explore.depictions
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import io.reactivex.Single
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Test
class PageableDepictionsDataSourceTest {
@Test
fun `loadFunction loads depictions`() {
val depictsClient: DepictsClient = mock()
whenever(depictsClient.searchForDepictions("test", 0, 1)).thenReturn(Single.just(emptyList()))
val pageableDepictionsDataSource = PageableDepictionsDataSource(mock(), depictsClient)
pageableDepictionsDataSource.onQueryUpdated("test")
assertThat(pageableDepictionsDataSource.loadFunction.invoke(0, 1), Matchers.`is`(emptyList()))
}
}