mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
refactor [collab/gitlab/github/azd]: rework paged list loaders
Force the client to pass "loadAll" explicitly to the loader Make a test for the abstract implementation instead of one concrete impl GitOrigin-RevId: 27323ebb9967d6c07b32938b71a343ea7f72f422
This commit is contained in:
committed by
intellij-monorepo-bot
parent
15c154b658
commit
212f40edc5
+4
-4
@@ -18,7 +18,7 @@ object GraphQLListLoader {
|
||||
requestRefreshFlow: Flow<Unit>? = null,
|
||||
requestChangeFlow: Flow<Change<V>>? = null,
|
||||
|
||||
shouldTryToLoadAll: Boolean = false,
|
||||
shouldTryToLoadAll: Boolean,
|
||||
|
||||
performRequest: suspend (cursor: String?) -> GraphQLConnectionDTO<V>?,
|
||||
): ReloadablePotentiallyInfiniteListLoader<V> {
|
||||
@@ -34,7 +34,7 @@ object GraphQLListLoader {
|
||||
|
||||
private class GraphQLListLoaderImpl<K, V>(
|
||||
extractKey: (V) -> K,
|
||||
shouldTryToLoadAll: Boolean = false,
|
||||
shouldTryToLoadAll: Boolean,
|
||||
|
||||
private val performRequest: suspend (cursor: String?) -> GraphQLConnectionDTO<V>?,
|
||||
) : PaginatedPotentiallyInfiniteListLoader<PageInfo, K, V>(PageInfo(), extractKey, shouldTryToLoadAll) {
|
||||
@@ -48,11 +48,11 @@ private class GraphQLListLoaderImpl<K, V>(
|
||||
|
||||
override suspend fun performRequestAndProcess(
|
||||
pageInfo: PageInfo,
|
||||
f: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?,
|
||||
createPage: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?,
|
||||
): Page<PageInfo, V>? {
|
||||
val results = performRequest(pageInfo.cursor)
|
||||
val nextCursor = results?.pageInfo?.endCursor
|
||||
|
||||
return f(pageInfo.copy(nextCursor = nextCursor), results?.nodes)
|
||||
return createPage(pageInfo.copy(nextCursor = nextCursor), results?.nodes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,18 @@ interface PotentiallyInfiniteListLoader {
|
||||
interface ReloadablePotentiallyInfiniteListLoader<V>
|
||||
: ListLoader<V>, ReloadableListLoader, PotentiallyInfiniteListLoader
|
||||
|
||||
/**
|
||||
* An abstract class that provides functionality for managing and loading a paginated
|
||||
* potentially infinite list of data items. This loader supports reloading, refreshing,
|
||||
* and incrementally loading more data pages, with support for handling failures and tracking busy states.
|
||||
*
|
||||
* @param PI The type representing page information, which tracks the pagination state.
|
||||
* @param K The type representing keys extracted from data items.
|
||||
* @param V The type of data items being loaded and managed by the loader.
|
||||
* @param initialPageInfo The initial page information used to begin the pagination process.
|
||||
* @param extractKey A lambda to extract a key of type [K] from a data item of type [V].
|
||||
* @param shouldTryToLoadAll Indicates whether the loader should attempt to load all pages upfront.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
abstract class PaginatedPotentiallyInfiniteListLoader<PI : PageInfo<PI>, K, V>(
|
||||
private val initialPageInfo: PI,
|
||||
@@ -212,23 +224,29 @@ abstract class PaginatedPotentiallyInfiniteListLoader<PI : PageInfo<PI>, K, V>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the current state of the paginated list by reloading data for each page
|
||||
* and loading the new pages if [shouldTryToLoadAll] is set
|
||||
*/
|
||||
private suspend fun doRefresh() {
|
||||
coroutineScope {
|
||||
val currentPages = pages.list ?: listOf()
|
||||
doEmitPages(
|
||||
runCatchingUser {
|
||||
State(currentPages.mapNotNull { page ->
|
||||
if (page.info == null) return@mapNotNull null
|
||||
val newState = runCatchingUser {
|
||||
val newPages = currentPages.mapNotNull { page ->
|
||||
if (page.info == null) return@mapNotNull null
|
||||
|
||||
performRequestAndProcess(page.info) { pageInfo, results ->
|
||||
page.copy(info = pageInfo, list = results ?: page.list)
|
||||
}
|
||||
}.toList(), null)
|
||||
}.getOrElse {
|
||||
State(currentPages, it)
|
||||
})
|
||||
performRequestAndProcess(page.info) { pageInfo, results ->
|
||||
page.copy(info = pageInfo, list = results ?: page.list)
|
||||
}
|
||||
}
|
||||
State(newPages, null)
|
||||
}.getOrElse {
|
||||
State(currentPages, it)
|
||||
}
|
||||
doEmitPages(newState)
|
||||
}
|
||||
|
||||
// load the new pages we learned about during the refresh
|
||||
if (shouldTryToLoadAll) {
|
||||
loadAllImpl()
|
||||
}
|
||||
@@ -300,7 +318,7 @@ abstract class PaginatedPotentiallyInfiniteListLoader<PI : PageInfo<PI>, K, V>(
|
||||
*/
|
||||
protected abstract suspend fun performRequestAndProcess(
|
||||
pageInfo: PI,
|
||||
f: (pageInfo: PI?, results: List<V>?) -> Page<PI, V>?
|
||||
createPage: (pageInfo: PI?, results: List<V>?) -> Page<PI, V>?
|
||||
): Page<PI, V>?
|
||||
|
||||
/**
|
||||
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.collaboration.async
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.intellij.collaboration.api.dto.GraphQLConnectionDTO
|
||||
import com.intellij.collaboration.api.dto.GraphQLCursorPageInfoDTO
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifySequence
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class GraphQLListLoaderTest {
|
||||
private data class DummyData(
|
||||
val key: Int,
|
||||
)
|
||||
|
||||
private class DummyDataConnection(
|
||||
pageInfo: GraphQLCursorPageInfoDTO,
|
||||
nodes: List<DummyData>,
|
||||
) : GraphQLConnectionDTO<DummyData>(pageInfo, nodes)
|
||||
|
||||
companion object {
|
||||
private val ALL_TEST_DATA: List<DummyData> = (0 until 100).map { DummyData(it) }
|
||||
|
||||
private fun blockingPageLookup(pageSize: Int, sizeLimiter: () -> Int = { ALL_TEST_DATA.size }): suspend (cursor: String?) -> DummyDataConnection? =
|
||||
suspend@{ cursor ->
|
||||
val startIndex = cursor?.toInt() ?: 0
|
||||
val virtualSize = minOf(ALL_TEST_DATA.size, sizeLimiter())
|
||||
val endIndex = minOf(virtualSize, startIndex + pageSize)
|
||||
|
||||
if (startIndex >= virtualSize) return@suspend null
|
||||
|
||||
val data = ALL_TEST_DATA.subList(startIndex, endIndex)
|
||||
DummyDataConnection(GraphQLCursorPageInfoDTO(
|
||||
startCursor = startIndex.toString(), endCursor = endIndex.toString(),
|
||||
hasPreviousPage = startIndex > 0, hasNextPage = endIndex < virtualSize
|
||||
), data)
|
||||
}
|
||||
}
|
||||
|
||||
// Stub for which method invocations are tracked by Mockito
|
||||
private interface MockingPageLookup : suspend (String?) -> DummyDataConnection? {
|
||||
companion object {
|
||||
fun create(lookup: suspend (String?) -> DummyDataConnection?): MockingPageLookup = mockk<MockingPageLookup>().apply {
|
||||
coEvery { this@apply.invoke(any()) }.coAnswers { lookup(firstArg()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no pages are loaded initially`() = runTest {
|
||||
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 10))
|
||||
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, performRequest = pageLookupMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerify(exactly = 0) { pageLookupMock.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one page is loaded initially with starting reload`() = runTest {
|
||||
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 20))
|
||||
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, performRequest = pageLookupMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
listLoader.reload()
|
||||
|
||||
assertThat(awaitItem().list).isNull()
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(20))
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
pageLookupMock.invoke(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all pages are loaded initially with starting reload`() = runTest {
|
||||
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40))
|
||||
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, shouldTryToLoadAll = true, performRequest = pageLookupMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
listLoader.reload()
|
||||
|
||||
assertThat(awaitItem().list).isNull()
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(40))
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(80))
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA)
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
pageLookupMock.invoke(null)
|
||||
pageLookupMock.invoke(eq("40"))
|
||||
pageLookupMock.invoke(eq("80"))
|
||||
pageLookupMock.invoke(eq("100")) // impl intentionally ignores hasNext in favor of just trying to fetch
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only previously loaded pages are re-fetched upon refresh`() = runTest {
|
||||
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40))
|
||||
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, performRequest = pageLookupMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadMore()
|
||||
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(40))
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.refresh()
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
// loadMore
|
||||
pageLookupMock.invoke(null)
|
||||
|
||||
// refresh
|
||||
pageLookupMock.invoke(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadMore doesnt update state when no new data is available`() = runTest {
|
||||
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40))
|
||||
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, shouldTryToLoadAll = true, performRequest = pageLookupMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.reload()
|
||||
while (awaitItem().list != ALL_TEST_DATA) {
|
||||
}
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadMore()
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
// reload
|
||||
pageLookupMock.invoke(null)
|
||||
pageLookupMock.invoke(eq("40"))
|
||||
pageLookupMock.invoke(eq("80"))
|
||||
pageLookupMock.invoke(eq("100"))
|
||||
|
||||
// loadMore
|
||||
pageLookupMock.invoke(eq("100"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadMore does update state when new data is available`() = runTest {
|
||||
var dynamicSize = 20
|
||||
|
||||
val pageLookupMock = MockingPageLookup.create(lookup = blockingPageLookup(pageSize = 40, sizeLimiter = { dynamicSize }))
|
||||
val listLoader = GraphQLListLoader.startIn<Int, DummyData>(backgroundScope, extractKey = { it.key }, shouldTryToLoadAll = true, performRequest = pageLookupMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadAll()
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(20))
|
||||
expectNoEvents()
|
||||
|
||||
dynamicSize = 60
|
||||
|
||||
listLoader.loadMore()
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(60))
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
// loadAll
|
||||
pageLookupMock.invoke(null)
|
||||
pageLookupMock.invoke(eq("20"))
|
||||
|
||||
// loadMore
|
||||
pageLookupMock.invoke(eq("20"))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Tests for (1) update function, (2) throwing an error inside fetch, (3) tracking reload/refresh flows maybe
|
||||
// For more info, run and check coverage inside com.intellij.collaboration.async
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.collaboration.async
|
||||
|
||||
import app.cash.turbine.test
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifySequence
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class PaginatedPotentiallyInfiniteListLoaderTest {
|
||||
private data class DummyData(
|
||||
val key: Int,
|
||||
)
|
||||
|
||||
private data class DummyPageInfo(
|
||||
val offset: Int,
|
||||
val hasNext: Boolean,
|
||||
) : PaginatedPotentiallyInfiniteListLoader.PageInfo<DummyPageInfo> {
|
||||
override fun createNextPageInfo(): DummyPageInfo? =
|
||||
if (hasNext) copy(offset = offset + PAGE_SIZE) else null
|
||||
}
|
||||
|
||||
private data class DummyPage(
|
||||
val pageInfo: DummyPageInfo?,
|
||||
val data: List<DummyData>?,
|
||||
)
|
||||
|
||||
private fun interface PageLoader {
|
||||
fun computePage(offset: Int): DummyPage?
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGE_SIZE = 20
|
||||
private val ALL_TEST_DATA: List<DummyData> = (0 until 100).map { DummyData(it) }
|
||||
|
||||
private fun pageLoaderMock(sizeLimiter: () -> Int = { ALL_TEST_DATA.size }): PageLoader =
|
||||
mockk<PageLoader> {
|
||||
coEvery { computePage(any()) }.coAnswers {
|
||||
val offset = firstArg() as Int
|
||||
val virtualSize = minOf(ALL_TEST_DATA.size, sizeLimiter())
|
||||
val endIndex = minOf(virtualSize, offset + PAGE_SIZE)
|
||||
|
||||
if (offset >= virtualSize) return@coAnswers null
|
||||
|
||||
val data = ALL_TEST_DATA.subList(offset, endIndex)
|
||||
val hasNext = endIndex < virtualSize
|
||||
DummyPage(DummyPageInfo(offset = offset, hasNext = hasNext), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TestLoader(
|
||||
shouldTryToLoadAll: Boolean,
|
||||
private val pageLoader: PageLoader,
|
||||
) : PaginatedPotentiallyInfiniteListLoader<DummyPageInfo, Int, DummyData>(
|
||||
initialPageInfo = DummyPageInfo(offset = 0, hasNext = true),
|
||||
extractKey = { it.key },
|
||||
shouldTryToLoadAll = shouldTryToLoadAll
|
||||
) {
|
||||
override suspend fun performRequestAndProcess(
|
||||
pageInfo: DummyPageInfo,
|
||||
createPage: (pageInfo: DummyPageInfo?, results: List<DummyData>?) -> Page<DummyPageInfo, DummyData>?,
|
||||
): Page<DummyPageInfo, DummyData>? {
|
||||
val response = pageLoader.computePage(pageInfo.offset) ?: return null
|
||||
return createPage(response.pageInfo, response.data)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no pages are loaded initially`() = runTest {
|
||||
val pageLookupMock = pageLoaderMock(ALL_TEST_DATA::size)
|
||||
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLookupMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerify(exactly = 0) { pageLookupMock.computePage(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one page is loaded initially with starting reload`() = runTest {
|
||||
val pageLoaderMock = pageLoaderMock()
|
||||
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLoaderMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
listLoader.reload()
|
||||
|
||||
assertThat(awaitItem().list).isNull()
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE))
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all pages are loaded initially with starting reload`() = runTest {
|
||||
val pageLoaderMock = pageLoaderMock()
|
||||
val listLoader = TestLoader(shouldTryToLoadAll = true, pageLoader = pageLoaderMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
listLoader.reload()
|
||||
|
||||
assertThat(awaitItem().list).isNull()
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE))
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE * 2))
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE * 3))
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA.take(PAGE_SIZE * 4))
|
||||
assertThat(awaitItem().list).containsExactlyElementsOf(ALL_TEST_DATA)
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE * 2))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE * 3))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE * 4))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only previously loaded pages are re-fetched upon refresh`() = runTest {
|
||||
val pageLoaderMock = pageLoaderMock()
|
||||
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLoaderMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadMore()
|
||||
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE))
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.refresh()
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
// loadMore
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
|
||||
// refresh
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadMore doesnt update state when no new data is available`() = runTest {
|
||||
val pageLoaderMock = pageLoaderMock()
|
||||
val listLoader = TestLoader(shouldTryToLoadAll = true, pageLoader = pageLoaderMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.reload()
|
||||
while (awaitItem().list != ALL_TEST_DATA) {
|
||||
}
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadMore()
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
// reload
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE * 2))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE * 3))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE * 4))
|
||||
|
||||
// loadMore - no call because hasNext is false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadMore does update state when new data is available`() = runTest {
|
||||
val pageLoaderMock = pageLoaderMock()
|
||||
val listLoader = TestLoader(shouldTryToLoadAll = false, pageLoader = pageLoaderMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadMore()
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE))
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadMore()
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE * 2))
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
// loadAll
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
|
||||
// loadMore
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all pages including new ones are loaded during refresh when full loading is requested`() = runTest {
|
||||
var dynamicSize = PAGE_SIZE
|
||||
|
||||
val pageLoaderMock = pageLoaderMock(sizeLimiter = { dynamicSize })
|
||||
val listLoader = TestLoader(shouldTryToLoadAll = true, pageLoader = pageLoaderMock)
|
||||
|
||||
listLoader.stateFlow.test(timeout = 1.seconds) {
|
||||
assertThat(awaitItem().list).isNull()
|
||||
expectNoEvents()
|
||||
|
||||
listLoader.loadAll()
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE))
|
||||
expectNoEvents()
|
||||
|
||||
dynamicSize = PAGE_SIZE * 3
|
||||
|
||||
listLoader.refresh()
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE * 2))
|
||||
assertThat(awaitItem().list).isEqualTo(ALL_TEST_DATA.take(PAGE_SIZE * 3))
|
||||
expectNoEvents()
|
||||
}
|
||||
|
||||
coVerifySequence {
|
||||
// loadAll
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
|
||||
// refresh
|
||||
pageLoaderMock.computePage(eq(0))
|
||||
|
||||
// loadAll after refresh
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE))
|
||||
pageLoaderMock.computePage(eq(PAGE_SIZE * 2))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Tests for (1) update function, (2) throwing an error inside fetch, (3) tracking reload/refresh flows maybe
|
||||
// For more info, run and check coverage inside com.intellij.collaboration.async
|
||||
}
|
||||
+3
-1
@@ -51,7 +51,9 @@ internal class GHPRListLoader(
|
||||
|
||||
requestReloadFlow = reloadRequests,
|
||||
requestRefreshFlow = refreshRequests,
|
||||
requestChangeFlow = updateRequests
|
||||
requestChangeFlow = updateRequests,
|
||||
|
||||
shouldTryToLoadAll = false
|
||||
) { cursor ->
|
||||
val page = GraphQLRequestPagination(afterCursor = cursor, pageSize = 50)
|
||||
requestExecutor.executeSuspend(
|
||||
|
||||
+9
-3
@@ -177,7 +177,9 @@ internal class LoadedGitLabMergeRequest(
|
||||
{ it.id },
|
||||
|
||||
requestReloadFlow = mergeRequestReloadRequest.withInitial(Unit),
|
||||
requestRefreshFlow = mergeRequestRefreshRequest.combine(stateEventsRefreshRequest.withInitial(Unit)) { _, _ -> }
|
||||
requestRefreshFlow = mergeRequestRefreshRequest.combine(stateEventsRefreshRequest.withInitial(Unit)) { _, _ -> },
|
||||
|
||||
shouldTryToLoadAll = false
|
||||
) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabResourceStateEventDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
|
||||
@@ -193,7 +195,9 @@ internal class LoadedGitLabMergeRequest(
|
||||
{ it.id },
|
||||
|
||||
requestReloadFlow = mergeRequestReloadRequest.withInitial(Unit),
|
||||
requestRefreshFlow = mergeRequestRefreshRequest
|
||||
requestRefreshFlow = mergeRequestRefreshRequest,
|
||||
|
||||
shouldTryToLoadAll = false
|
||||
) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabResourceLabelEventDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUEST_LABEL_EVENTS, uri, eTag
|
||||
@@ -209,7 +213,9 @@ internal class LoadedGitLabMergeRequest(
|
||||
{ it.id },
|
||||
|
||||
requestReloadFlow = mergeRequestReloadRequest.withInitial(Unit),
|
||||
requestRefreshFlow = mergeRequestRefreshRequest
|
||||
requestRefreshFlow = mergeRequestRefreshRequest,
|
||||
|
||||
shouldTryToLoadAll = false
|
||||
) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabResourceMilestoneEventDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUEST_MILESTONE_EVENTS, uri, eTag
|
||||
|
||||
+6
-2
@@ -101,7 +101,9 @@ class GitLabMergeRequestDiscussionsContainerImpl(
|
||||
|
||||
requestReloadFlow = reloadRequests,
|
||||
requestRefreshFlow = updateRequests,
|
||||
requestChangeFlow = discussionEvents
|
||||
requestChangeFlow = discussionEvents,
|
||||
|
||||
shouldTryToLoadAll = false
|
||||
) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabDiscussionRestDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUEST_DISCUSSIONS, uri, eTag
|
||||
@@ -158,7 +160,9 @@ class GitLabMergeRequestDiscussionsContainerImpl(
|
||||
|
||||
requestReloadFlow = reloadRequests,
|
||||
requestRefreshFlow = updateRequests,
|
||||
requestChangeFlow = draftNotesEvents
|
||||
requestChangeFlow = draftNotesEvents,
|
||||
|
||||
shouldTryToLoadAll = false
|
||||
) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabMergeRequestDraftNoteRestDTO>(
|
||||
GitLabApiRequestName.REST_GET_DRAFT_NOTES, uri, eTag
|
||||
|
||||
+2
-1
@@ -102,7 +102,8 @@ class CachingGitLabProjectMergeRequestsStore(private val project: Project,
|
||||
getMergeRequestListURI(glProject, searchQuery),
|
||||
{ it.id },
|
||||
|
||||
requestReloadFlow = tokenRefreshFlow.withInitial(Unit)
|
||||
requestReloadFlow = tokenRefreshFlow.withInitial(Unit),
|
||||
shouldTryToLoadAll = false
|
||||
) { uri, etag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabMergeRequestShortRestDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUESTS, uri, etag
|
||||
|
||||
+4
-5
@@ -25,11 +25,11 @@ fun <K, V> startGitLabRestETagListLoaderIn(
|
||||
requestRefreshFlow: Flow<Unit>? = null,
|
||||
requestChangeFlow: Flow<Change<V>>? = null,
|
||||
|
||||
shouldTryToLoadAll: Boolean = false,
|
||||
shouldTryToLoadAll: Boolean,
|
||||
|
||||
performRequest: suspend (uri: URI, eTag: String?) -> HttpResponse<out List<V>?>
|
||||
): ReloadablePotentiallyInfiniteListLoader<V> {
|
||||
val loader = GitLabRestETagListLoader(cs, initialURI, extractKey, shouldTryToLoadAll, performRequest)
|
||||
val loader = GitLabRestETagListLoader(initialURI, extractKey, shouldTryToLoadAll, performRequest)
|
||||
|
||||
cs.launchNow { requestReloadFlow?.collect { loader.reload() } }
|
||||
cs.launch { requestRefreshFlow?.collect { loader.refresh() } }
|
||||
@@ -39,7 +39,6 @@ fun <K, V> startGitLabRestETagListLoaderIn(
|
||||
}
|
||||
|
||||
private class GitLabRestETagListLoader<K, V>(
|
||||
cs: CoroutineScope,
|
||||
private val initialURI: URI,
|
||||
extractKey: (V) -> K,
|
||||
|
||||
@@ -62,7 +61,7 @@ private class GitLabRestETagListLoader<K, V>(
|
||||
|
||||
override suspend fun performRequestAndProcess(
|
||||
pageInfo: PageInfo,
|
||||
f: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?
|
||||
createPage: (pageInfo: PageInfo?, results: List<V>?) -> Page<PageInfo, V>?
|
||||
): Page<PageInfo, V>? {
|
||||
val response = try {
|
||||
performRequest(pageInfo.link, pageInfo.etag)
|
||||
@@ -87,6 +86,6 @@ private class GitLabRestETagListLoader<K, V>(
|
||||
URIUtil.createUriWithCustomScheme(it, initialURI.scheme)
|
||||
}
|
||||
|
||||
return f(pageInfo.copy(nextLink = nextLink, etag = newEtag), results)
|
||||
return createPage(pageInfo.copy(nextLink = nextLink, etag = newEtag), results)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -384,7 +384,8 @@ class GitLabApiTest : GitLabApiTestCase() {
|
||||
val loader = startGitLabRestETagListLoaderIn(backgroundScope,
|
||||
getMergeRequestStateEventsUri(glTest1Coordinates, "1"),
|
||||
{ it.id },
|
||||
reloadRequest) { uri, eTag ->
|
||||
reloadRequest,
|
||||
shouldTryToLoadAll = false) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabResourceStateEventDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
|
||||
)
|
||||
@@ -405,7 +406,8 @@ class GitLabApiTest : GitLabApiTestCase() {
|
||||
val loader = startGitLabRestETagListLoaderIn(backgroundScope,
|
||||
getMergeRequestLabelEventsUri(glTest1Coordinates, "1"),
|
||||
{ it.id },
|
||||
reloadRequest) { uri, eTag ->
|
||||
reloadRequest,
|
||||
shouldTryToLoadAll = false) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabResourceLabelEventDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
|
||||
)
|
||||
@@ -426,7 +428,8 @@ class GitLabApiTest : GitLabApiTestCase() {
|
||||
val loader = startGitLabRestETagListLoaderIn(backgroundScope,
|
||||
getMergeRequestMilestoneEventsUri(glTest1Coordinates, "1"),
|
||||
{ it.id },
|
||||
reloadRequest) { uri, eTag ->
|
||||
reloadRequest,
|
||||
shouldTryToLoadAll = false) { uri, eTag ->
|
||||
api.rest.loadUpdatableJsonList<GitLabResourceMilestoneEventDTO>(
|
||||
GitLabApiRequestName.REST_GET_MERGE_REQUEST_STATE_EVENTS, uri, eTag
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user