From a0002281653fc353e53bb1a15e4485263168798c Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 20 Jan 2022 18:30:21 +0100 Subject: [PATCH] correctly detect end of pagination in network timeline (#2296) * correctly detect end of pagination in network timeline closes #2293 * improve NetworkTimelineRemoteMediatorTest * remove unused import --- .../NetworkTimelineRemoteMediator.kt | 6 +- .../NetworkTimelineRemoteMediatorTest.kt | 100 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt index 55a187670..4ce6c4cb4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt @@ -47,7 +47,11 @@ class NetworkTimelineRemoteMediator( } LoadType.APPEND -> { val maxId = viewModel.nextKey - viewModel.fetchStatusesForKind(maxId, null, limit = state.config.pageSize) + if (maxId != null) { + viewModel.fetchStatusesForKind(maxId, null, limit = state.config.pageSize) + } else { + return MediatorResult.Success(endOfPaginationReached = true) + } } } diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt index b44d2487f..601fc00a4 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt @@ -6,6 +6,7 @@ import androidx.paging.PagingConfig import androidx.paging.PagingSource import androidx.paging.PagingState import androidx.paging.RemoteMediator +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineRemoteMediator import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel import com.keylesspalace.tusky.db.AccountEntity @@ -15,15 +16,21 @@ import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.doThrow import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify import kotlinx.coroutines.runBlocking +import okhttp3.Headers import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config import retrofit2.HttpException import retrofit2.Response import java.lang.RuntimeException +@Config(sdk = [29]) +@RunWith(AndroidJUnit4::class) class NetworkTimelineRemoteMediatorTest { private val accountManager: AccountManager = mock { @@ -70,6 +77,52 @@ class NetworkTimelineRemoteMediatorTest { assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is RuntimeException) } + @Test + @ExperimentalPagingApi + fun `should do initial loading`() { + val statuses: MutableList = mutableListOf() + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn null + onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success( + listOf( + mockStatus("7"), + mockStatus("6"), + mockStatus("5") + ), + Headers.headersOf( + "Link", "; rel=\"next\", ; rel=\"prev\"" + ) + ) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = null + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) } + + val newStatusData = mutableListOf( + mockStatusViewData("7"), + mockStatusViewData("6"), + mockStatusViewData("5"), + ) + + verify(timelineViewModel).nextKey = "4" + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + @Test @ExperimentalPagingApi fun `should not prepend statuses`() { @@ -246,6 +299,9 @@ class NetworkTimelineRemoteMediatorTest { mockStatus("3"), mockStatus("2"), mockStatus("1") + ), + Headers.headersOf( + "Link", "; rel=\"next\", ; rel=\"prev\"" ) ) } @@ -277,11 +333,55 @@ class NetworkTimelineRemoteMediatorTest { mockStatusViewData("1"), ) + verify(timelineViewModel).nextKey = "0" assertTrue(result is RemoteMediator.MediatorResult.Success) assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) assertEquals(newStatusData, statuses) } + @Test + @ExperimentalPagingApi + fun `should not append statuses when pagination end has been reached`() { + val statuses: MutableList = mutableListOf( + mockStatusViewData("8"), + mockStatusViewData("7"), + mockStatusViewData("5"), + ) + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn null + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + mockStatusViewData("8"), + mockStatusViewData("7"), + mockStatusViewData("5"), + ), + prevKey = null, + nextKey = null + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) } + + val newStatusData = mutableListOf( + mockStatusViewData("8"), + mockStatusViewData("7"), + mockStatusViewData("5") + ) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + private fun state(pages: List> = emptyList()) = PagingState( pages = pages, anchorPosition = null,