Merge remote-tracking branch 'origin/master' into macgills/3760-categories-pagination

# Conflicts:
#	app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java
#	app/src/main/java/fr/free/nrw/commons/explore/SearchModule.java
#	app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java
#	app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictionAdapter.kt
#	app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragment.kt
#	app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentContract.kt
#	app/src/main/java/fr/free/nrw/commons/explore/depictions/SearchDepictionsFragmentPresenter.kt
#	app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt
#	app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java
This commit is contained in:
Sean Mac Gillicuddy 2020-05-28 12:39:59 +01:00
commit 1a1a8389d5
60 changed files with 1024 additions and 259 deletions

View file

@ -0,0 +1,23 @@
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
fun depictedItem(
name: String = "label",
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
commonsCategories: List<String> = listOf(),
isSelected: Boolean = false,
id: String = "entityId"
) = DepictedItem(
name = name,
description = description,
imageUrl = imageUrl,
instanceOfs = instanceOfs,
commonsCategories = commonsCategories,
isSelected = isSelected,
id = id
)
fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)

View file

@ -35,7 +35,9 @@ class BookMarkLocationDaoTest {
COLUMN_WIKIDATA_LINK,
COLUMN_COMMONS_LINK,
COLUMN_LAT,
COLUMN_LONG)
COLUMN_LONG,
COLUMN_PIC,
COLUMN_DESTROYED)
private val client: ContentProviderClient = mock()
private val database: SQLiteDatabase = mock()
private val captor = argumentCaptor<ContentValues>()
@ -93,6 +95,8 @@ class BookMarkLocationDaoTest {
assertEquals(builder.build().wikipediaLink, it.siteLinks.wikipediaLink)
assertEquals(builder.build().wikidataLink, it.siteLinks.wikidataLink)
assertEquals(builder.build().commonsLink, it.siteLinks.commonsLink)
assertEquals("picName",it.pic)
assertEquals("placeDestroyed", it.destroyed)
}
}
}
@ -145,7 +149,7 @@ class BookMarkLocationDaoTest {
assertTrue(testObject.updateBookmarkLocation(examplePlaceBookmark))
verify(client).insert(eq(BASE_URI), captor.capture())
captor.firstValue.let { cv ->
assertEquals(11, cv.size())
assertEquals(12, cv.size())
assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME))
assertEquals(examplePlaceBookmark.longDescription, cv.getAsString(COLUMN_DESCRIPTION))
assertEquals(examplePlaceBookmark.label.text, cv.getAsString(COLUMN_LABEL_TEXT))
@ -156,6 +160,7 @@ class BookMarkLocationDaoTest {
assertEquals(examplePlaceBookmark.siteLinks.wikidataLink.toString(), cv.getAsString(COLUMN_WIKIDATA_LINK))
assertEquals(examplePlaceBookmark.siteLinks.commonsLink.toString(), cv.getAsString(COLUMN_COMMONS_LINK))
assertEquals(examplePlaceBookmark.pic.toString(), cv.getAsString(COLUMN_PIC))
assertEquals(examplePlaceBookmark.destroyed.toString(), cv.getAsString(COLUMN_DESTROYED))
}
}
@ -251,12 +256,18 @@ class BookMarkLocationDaoTest {
verify(database).execSQL(CREATE_TABLE_STATEMENT)
}
@Test
fun migrateTableVersionFrom_v12_to_v13() {
onUpdate(database, 12, 13)
verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;")
}
private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply {
for (i in 0 until rowCount) {
addRow(listOf("placeName", "placeDescription","placeCategory", exampleLabel.text, exampleLabel.icon,
exampleUri, builder.build().wikipediaLink, builder.build().wikidataLink, builder.build().commonsLink,
exampleLocation.latitude, exampleLocation.longitude))
exampleLocation.latitude, exampleLocation.longitude, "picName", "placeDestroyed"))
}
}
}

View file

@ -1,7 +1,10 @@
package fr.free.nrw.commons.category
import categoryItem
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.upload.GpsCategoryModel
import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject
@ -36,11 +39,11 @@ class CategoriesModelTest {
// Checking if both return "Test"
val expectedItems = expectedList.map { CategoryItem(it, false) }
categoriesModel.searchAll("tes", emptyList())
categoriesModel.searchAll("tes", emptyList(), emptyList())
.test()
.assertValues(expectedItems)
categoriesModel.searchAll("Tes", emptyList())
categoriesModel.searchAll("Tes", emptyList(), emptyList())
.test()
.assertValues(expectedItems)
}
@ -48,6 +51,7 @@ class CategoriesModelTest {
@Test
fun `searchAll with empty search terms creates results from gps, title search & recents`() {
val gpsCategoryModel: GpsCategoryModel = mock()
val depictedItem = depictedItem(commonsCategories = listOf("depictionCategory"))
whenever(gpsCategoryModel.categoriesFromLocation)
.thenReturn(BehaviorSubject.createDefault(listOf("gpsCategory")))
@ -55,13 +59,14 @@ class CategoriesModelTest {
.thenReturn(Observable.just(listOf("titleSearch")))
whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories"))
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
.searchAll("", listOf("tes"))
.searchAll("", listOf("tes"), listOf(depictedItem))
.test()
.assertValue(
listOf(
CategoryItem("gpsCategory", false),
CategoryItem("titleSearch", false),
CategoryItem("recentCategories", false)
categoryItem("depictionCategory"),
categoryItem("gpsCategory"),
categoryItem("titleSearch"),
categoryItem("recentCategories")
)
)
}

View file

@ -0,0 +1,53 @@
package fr.free.nrw.commons.explore.depictions
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.verify
import io.reactivex.processors.PublishProcessor
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
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 SearchDepictionsDataSourceFactoryTest {
@Mock
private lateinit var depictsClient: DepictsClient
@Mock
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var factory: SearchDepictionsDataSourceFactory
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
factory = SearchDepictionsDataSourceFactory(depictsClient, "test", loadingStates)
}
@Test
fun `create returns a dataSource`() {
assertThat(
factory.create(),
`is`(SearchDepictionsDataSource(depictsClient, loadingStates, "test"))
)
}
@Test
@Ignore("Rewrite with Mockk constructor mocks")
fun `retryFailedRequest invokes method if not null`() {
val spyFactory = spy(factory)
val dataSource = mock<SearchDepictionsDataSource>()
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,106 @@
package fr.free.nrw.commons.explore.depictions
import androidx.paging.PositionalDataSource
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.explore.depictions.LoadingState.*
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Single
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 SearchDepictionsDataSourceTest {
@Mock
private lateinit var depictsClient: DepictsClient
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var searchDepictionsDataSource: SearchDepictionsDataSource
@Before
fun setUp() {
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
MockitoAnnotations.initMocks(this)
loadingStates = PublishProcessor.create()
searchDepictionsDataSource =
SearchDepictionsDataSource(depictsClient, loadingStates, "test")
}
@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<DepictedItem>>()
whenever(depictsClient.searchForDepictions("test", 1, 0))
.thenReturn(Single.just(emptyList()))
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadInitial(params, callback)
verify(callback).onResult(emptyList(), 0)
testSubscriber.assertValues(InitialLoad, 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<DepictedItem>>()
whenever(depictsClient.searchForDepictions("test", 1, 0))
.thenThrow(RuntimeException())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadInitial(params, callback)
verify(callback, never()).onResult(any(), any())
testSubscriber.assertValues(InitialLoad, Error)
}
@Test
fun `loadRange returns results and emits Loading & Complete`() {
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
.thenReturn(Single.just(emptyList()))
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback).onResult(emptyList())
testSubscriber.assertValues(Loading, Complete)
}
@Test
fun `loadRange onError does not return results and emits Loading & Error`() {
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
.thenThrow(RuntimeException())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback, never()).onResult(any())
testSubscriber.assertValues(Loading, Error)
}
@Test
fun `retryFailedRequest does nothing when null`() {
searchDepictionsDataSource.retryFailedRequest()
verifyNoMoreInteractions(depictsClient)
}
@Test
fun `retryFailedRequest retries last request`() {
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
.thenThrow(RuntimeException()).thenReturn(Single.just(emptyList()))
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback, never()).onResult(any())
searchDepictionsDataSource.retryFailedRequest()
verify(callback).onResult(emptyList())
testSubscriber.assertValues(Loading, Error, Loading, Complete)
}
}

View file

@ -0,0 +1,136 @@
package fr.free.nrw.commons.explore.depictions
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
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class SearchDepictionsFragmentPresenterTest {
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
internal lateinit var view: SearchDepictionsFragmentContract.View
private lateinit var searchDepictionsFragmentPresenter: SearchDepictionsFragmentPresenter
private lateinit var testScheduler: TestScheduler
@Mock
private lateinit var searchableDepictionsDataSourceFactory: SearchableDepictionsDataSourceFactory
private var loadingStates: PublishProcessor<LoadingState> = PublishProcessor.create()
private var searchResults: PublishProcessor<LiveData<PagedList<DepictedItem>>> =
PublishProcessor.create()
private var noItemLoadedEvent: PublishProcessor<Unit> = PublishProcessor.create()
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(searchableDepictionsDataSourceFactory.searchResults).thenReturn(searchResults)
whenever(searchableDepictionsDataSourceFactory.loadingStates).thenReturn(loadingStates)
whenever(searchableDepictionsDataSourceFactory.noItemsLoadedEvent)
.thenReturn(noItemLoadedEvent)
testScheduler = TestScheduler()
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
testScheduler,
searchableDepictionsDataSourceFactory
)
searchDepictionsFragmentPresenter.onAttachView(view)
}
@Test
fun `searchResults emission updates the view`() {
val pagedListLiveData = mock<LiveData<PagedList<DepictedItem>>>()
searchResults.offer(pagedListLiveData)
verify(view).observeSearchResults(pagedListLiveData)
}
@Test
fun `Loading offers a loading list item`() {
onLoadingState(LoadingState.Loading)
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(listOf(FooterItem.LoadingItem))
}
@Test
fun `Complete offers an empty list item and hides initial loader`() {
onLoadingState(LoadingState.Complete)
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(emptyList())
verify(view).hideInitialLoadProgress()
}
@Test
fun `InitialLoad shows initial loader`() {
onLoadingState(LoadingState.InitialLoad)
verify(view).showInitialLoadInProgress()
}
@Test
fun `Error offers a refresh list item, hides initial loader and shows error with a set text`() {
searchDepictionsFragmentPresenter.onQueryUpdated("test")
onLoadingState(LoadingState.Error)
verify(view).setEmptyViewText("test")
verify(view).showSnackbar()
verify(view).hideInitialLoadProgress()
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(listOf(FooterItem.RefreshItem))
}
@Test
fun `Error offers a refresh list item, hides initial loader and shows error with a unset text`() {
onLoadingState(LoadingState.Error)
verify(view, never()).setEmptyViewText(any())
verify(view).showSnackbar()
verify(view).hideInitialLoadProgress()
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(listOf(FooterItem.RefreshItem))
}
@Test
fun `no Items event sets empty view text`() {
searchDepictionsFragmentPresenter.onQueryUpdated("test")
noItemLoadedEvent.offer(Unit)
verify(view).setEmptyViewText("test")
}
@Test
fun `retryFailedRequest calls retry`() {
searchDepictionsFragmentPresenter.retryFailedRequest()
verify(searchableDepictionsDataSourceFactory).retryFailedRequest()
}
@Test
fun `onDetachView stops subscriptions`() {
searchDepictionsFragmentPresenter.onDetachView()
onLoadingState(LoadingState.Loading)
searchDepictionsFragmentPresenter.listFooterData.test()
.assertValue(emptyList())
}
@Test
fun `onQueryUpdated updates dataSourceFactory`() {
searchDepictionsFragmentPresenter.onQueryUpdated("test")
verify(searchableDepictionsDataSourceFactory).onQueryUpdated("test")
}
private fun onLoadingState(loadingState: LoadingState) {
loadingStates.offer(loadingState)
testScheduler.triggerActions()
}
}

View file

@ -0,0 +1,80 @@
package fr.free.nrw.commons.explore.depictions
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.processors.PublishProcessor
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class SearchableDepictionsDataSourceFactoryTest {
@Mock
private lateinit var searchDepictionsDataSourceFactoryFactory: SearchDepictionsDataSourceFactoryFactory
@Mock
private lateinit var liveDataConverter: LiveDataConverter
private lateinit var factory: SearchableDepictionsDataSourceFactory
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
factory = SearchableDepictionsDataSourceFactory(
searchDepictionsDataSourceFactoryFactory,
liveDataConverter
)
}
@Test
fun `onQueryUpdated emits new liveData`() {
val (_, liveData) = expectNewLiveData()
factory.searchResults.test()
.also { factory.onQueryUpdated("test") }
.assertValue(liveData)
}
@Test
fun `onQueryUpdated invokes livedatconverter with no items emitter`() {
val (captor, _) = expectNewLiveData()
factory.onQueryUpdated("test")
factory.noItemsLoadedEvent.test()
.also { captor.firstValue.invoke() }
.assertValue(Unit)
}
/*
* Just for coverage, no way to really assert this
* */
@Test
fun `retryFailedRequest does nothing without a factory`() {
factory.retryFailedRequest()
}
@Test
fun `retryFailedRequest retries with a factory`() {
val (_, _, dataSourceFactory) = expectNewLiveData()
factory.onQueryUpdated("test")
factory.retryFailedRequest()
verify(dataSourceFactory).retryFailedRequest()
}
private fun expectNewLiveData(): Triple<KArgumentCaptor<() -> Unit>, LiveData<PagedList<DepictedItem>>, SearchDepictionsDataSourceFactory> {
val dataSourceFactory: SearchDepictionsDataSourceFactory = mock()
whenever(
searchDepictionsDataSourceFactoryFactory.create(
"test",
factory.loadingStates as PublishProcessor<LoadingState>
)
).thenReturn(dataSourceFactory)
val captor = argumentCaptor<() -> Unit>()
val liveData: LiveData<PagedList<DepictedItem>> = mock()
whenever(liveDataConverter.convert(eq(dataSourceFactory), captor.capture()))
.thenReturn(liveData)
return Triple(captor, liveData, dataSourceFactory)
}
}

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.upload
import categoryItem
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.categories.CategoriesContract
import fr.free.nrw.commons.upload.categories.CategoriesPresenter
@ -27,11 +27,6 @@ class CategoriesPresenterTest {
private lateinit var testScheduler: TestScheduler
private val categoryItems: ArrayList<CategoryItem> = ArrayList()
@Mock
lateinit var categoryItem: CategoryItem
/**
* initial setup
*/
@ -40,7 +35,6 @@ class CategoriesPresenterTest {
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
categoryItems.add(categoryItem)
categoriesPresenter = CategoriesPresenter(repository, testScheduler, testScheduler)
categoriesPresenter.onAttachView(view)
}
@ -62,7 +56,7 @@ class CategoriesPresenterTest {
emptyCaptionUploadItem
)
)
whenever(repository.searchAll("test", listOf("nonEmpty")))
whenever(repository.searchAll("test", listOf("nonEmpty"), repository.selectedDepictions))
.thenReturn(
Observable.just(
listOf(
@ -87,7 +81,7 @@ class CategoriesPresenterTest {
@Test
fun `searchForCategoriesTest sets Error when list is empty`() {
whenever(repository.uploads).thenReturn(listOf())
whenever(repository.searchAll(any(), any())).thenReturn(Observable.just(listOf()))
whenever(repository.searchAll(any(), any(), any())).thenReturn(Observable.just(listOf()))
whenever(repository.selectedCategories).thenReturn(listOf())
categoriesPresenter.searchForCategories("test")
testScheduler.triggerActions()
@ -124,10 +118,8 @@ class CategoriesPresenterTest {
*/
@Test
fun onCategoryItemClickedTest() {
val categoryItem = categoryItem()
categoriesPresenter.onCategoryItemClicked(categoryItem)
verify(repository).onCategoryClicked(categoryItem)
}
private fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)
}

View file

@ -4,11 +4,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.DepictsContract
import fr.free.nrw.commons.upload.depicts.DepictsPresenter
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
import io.reactivex.Flowable
import io.reactivex.schedulers.TestScheduler
@ -62,8 +62,8 @@ class DepictsPresenterTest {
depictedItem(id="nonUnique"),
depictedItem(id="nonUnique"),
depictedItem(
id = "unique",
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id)
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id),
id = "unique"
)
)
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults))
@ -78,6 +78,7 @@ class DepictsPresenterTest {
.assertValue(listOf(selectedItem, depictedItem(id="nonUnique")))
}
@Test
fun `empty search results with empty term do not show error`() {
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(emptyList()))
@ -137,15 +138,4 @@ class DepictsPresenterTest {
depictsPresenter.verifyDepictions()
verify(view).noDepictionSelected()
}
}
fun depictedItem(
name: String = "label",
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
isSelected: Boolean = false,
id: String = "entityId"
) = DepictedItem(name, description, imageUrl, instanceOfs, isSelected, id)