1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.paging
18 
19 import androidx.kruth.assertThat
20 import androidx.paging.LoadState.Loading
21 import androidx.paging.LoadState.NotLoading
22 import androidx.paging.LoadType.PREPEND
23 import androidx.paging.PageEvent.Drop
24 import androidx.paging.PagingSource.LoadResult
25 import kotlin.coroutines.EmptyCoroutineContext
26 import kotlin.test.Test
27 import kotlin.test.assertEquals
28 import kotlin.test.assertFailsWith
29 import kotlin.test.assertFalse
30 import kotlin.test.assertNull
31 import kotlin.test.assertTrue
32 import kotlinx.coroutines.ExperimentalCoroutinesApi
33 import kotlinx.coroutines.Job
34 import kotlinx.coroutines.async
35 import kotlinx.coroutines.cancelChildren
36 import kotlinx.coroutines.channels.Channel
37 import kotlinx.coroutines.delay
38 import kotlinx.coroutines.flow.collectLatest
39 import kotlinx.coroutines.flow.consumeAsFlow
40 import kotlinx.coroutines.flow.emptyFlow
41 import kotlinx.coroutines.flow.filterNotNull
42 import kotlinx.coroutines.flow.first
43 import kotlinx.coroutines.flow.flow
44 import kotlinx.coroutines.flow.flowOf
45 import kotlinx.coroutines.flow.map
46 import kotlinx.coroutines.launch
47 import kotlinx.coroutines.suspendCancellableCoroutine
48 import kotlinx.coroutines.test.TestScope
49 import kotlinx.coroutines.test.UnconfinedTestDispatcher
50 import kotlinx.coroutines.test.advanceTimeBy
51 import kotlinx.coroutines.test.advanceUntilIdle
52 import kotlinx.coroutines.test.runTest
53 
54 /**
55  * run some tests with cached-in to ensure caching does not change behavior in the single consumer
56  * cases.
57  */
58 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalPagingApi::class)
59 class PagingDataPresenterTest {
60     private val testScope = TestScope(UnconfinedTestDispatcher())
61 
62     @Test
63     fun collectFrom_static() =
64         testScope.runTest {
65             val presenter = SimplePresenter()
66             val receiver = UiReceiverFake()
67 
68             val job1 = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
69             advanceUntilIdle()
70             job1.cancel()
71 
72             val job2 = launch { presenter.collectFrom(PagingData.empty()) }
73             advanceUntilIdle()
74             job2.cancel()
75 
76             // Static replacement should also replace the UiReceiver from previous generation.
77             presenter.retry()
78             presenter.refresh()
79             advanceUntilIdle()
80 
81             assertFalse { receiver.retryEvents.isNotEmpty() }
82             assertFalse { receiver.refreshEvents.isNotEmpty() }
83         }
84 
85     @Test
86     fun collectFrom_twice() =
87         testScope.runTest {
88             val presenter = SimplePresenter()
89 
90             launch { presenter.collectFrom(infinitelySuspendingPagingData()) }.cancel()
91             launch { presenter.collectFrom(infinitelySuspendingPagingData()) }.cancel()
92         }
93 
94     @Test
95     fun collectFrom_twiceConcurrently() =
96         testScope.runTest {
97             val presenter = SimplePresenter()
98 
99             val job1 = launch { presenter.collectFrom(infinitelySuspendingPagingData()) }
100 
101             // Ensure job1 is running.
102             assertTrue { job1.isActive }
103 
104             val job2 = launch { presenter.collectFrom(infinitelySuspendingPagingData()) }
105 
106             // job2 collection should complete job1 but not cancel.
107             assertFalse { job1.isCancelled }
108             assertTrue { job1.isCompleted }
109             job2.cancel()
110         }
111 
112     @Test
113     fun retry() =
114         testScope.runTest {
115             val presenter = SimplePresenter()
116             val receiver = UiReceiverFake()
117 
118             val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
119 
120             presenter.retry()
121             assertEquals(1, receiver.retryEvents.size)
122 
123             job.cancel()
124         }
125 
126     @Test
127     fun refresh() =
128         testScope.runTest {
129             val presenter = SimplePresenter()
130             val receiver = UiReceiverFake()
131 
132             val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
133 
134             presenter.refresh()
135 
136             assertEquals(1, receiver.refreshEvents.size)
137 
138             job.cancel()
139         }
140 
141     @Test
142     fun retrySentBeforeCollection() =
143         testScope.run {
144             val presenter = SimplePresenter()
145             val receiver = UiReceiverFake()
146 
147             presenter.retry()
148 
149             val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
150 
151             assertEquals(1, receiver.retryEvents.size)
152 
153             job.cancel()
154         }
155 
156     @Test
157     fun refreshSentBeforeCollection() =
158         testScope.runTest {
159             val presenter = SimplePresenter()
160             val receiver = UiReceiverFake()
161 
162             presenter.refresh()
163 
164             val job = launch { presenter.collectFrom(infinitelySuspendingPagingData(receiver)) }
165 
166             assertEquals(1, receiver.refreshEvents.size)
167 
168             job.cancel()
169         }
170 
171     @Test
172     fun uiReceiverSetImmediately() =
173         testScope.runTest {
174             val presenter = SimplePresenter()
175             val receiver = UiReceiverFake()
176             val pagingData1 = infinitelySuspendingPagingData(uiReceiver = receiver)
177 
178             val job1 = launch { presenter.collectFrom(pagingData1) }
179             assertTrue(job1.isActive) // ensure job started
180 
181             assertThat(receiver.refreshEvents).hasSize(0)
182 
183             presenter.refresh()
184             // double check that the pagingdata's receiver was registered and had received refresh
185             // call
186             // before any PageEvent is collected/presented
187             assertThat(receiver.refreshEvents).hasSize(1)
188 
189             job1.cancel()
190         }
191 
192     @Test
193     fun hintReceiverSetAfterNewListPresented() =
194         testScope.runTest {
195             val presenter = SimplePresenter()
196 
197             // first generation, load something so next gen can access index to trigger hint
198             val hintReceiver1 = HintReceiverFake()
199             val flow =
200                 flowOf(
201                     localRefresh(pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4)))),
202                 )
203 
204             val job1 = launch {
205                 presenter.collectFrom(PagingData(flow, dummyUiReceiver, hintReceiver1))
206             }
207 
208             // access any loaded item to make sure hint is sent
209             presenter[3]
210             assertThat(hintReceiver1.hints)
211                 .containsExactly(
212                     ViewportHint.Access(
213                         pageOffset = 0,
214                         indexInPage = 3,
215                         presentedItemsBefore = 3,
216                         presentedItemsAfter = 1,
217                         originalPageOffsetFirst = 0,
218                         originalPageOffsetLast = 0,
219                     )
220                 )
221 
222             // trigger second generation
223             presenter.refresh()
224 
225             // second generation
226             val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
227             val hintReceiver2 = HintReceiverFake()
228             val job2 = launch {
229                 presenter.collectFrom(
230                     PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver2)
231                 )
232             }
233 
234             // we send the initial load state. this should NOT cause second gen hint receiver
235             // to register
236             pageEventCh.trySend(localLoadStateUpdate(refreshLocal = Loading))
237             assertThat(presenter.nonNullLoadStateFlow.first())
238                 .isEqualTo(localLoadStatesOf(refreshLocal = Loading))
239 
240             // ensure both hint receivers are idle before sending a hint
241             assertThat(hintReceiver1.hints).isEmpty()
242             assertThat(hintReceiver2.hints).isEmpty()
243 
244             // try sending a hint, should be sent to first receiver
245             presenter[4]
246             assertThat(hintReceiver1.hints).hasSize(1)
247             assertThat(hintReceiver2.hints).isEmpty()
248 
249             // now we send actual refresh load and make sure its presented
250             pageEventCh.trySend(
251                 localRefresh(
252                     pages = listOf(TransformablePage(listOf(20, 21, 22, 23, 24))),
253                     placeholdersBefore = 20,
254                     placeholdersAfter = 75
255                 ),
256             )
257             assertThat(presenter.snapshot().items).containsExactlyElementsIn(20 until 25)
258 
259             // access any loaded item to make sure hint is sent to proper receiver
260             presenter[3]
261             // second receiver was registered and received the initial viewport hint
262             assertThat(hintReceiver1.hints).isEmpty()
263             assertThat(hintReceiver2.hints)
264                 .containsExactly(
265                     ViewportHint.Access(
266                         pageOffset = 0,
267                         indexInPage = -17,
268                         presentedItemsBefore = -17,
269                         presentedItemsAfter = 21,
270                         originalPageOffsetFirst = 0,
271                         originalPageOffsetLast = 0,
272                     )
273                 )
274 
275             job2.cancel()
276             job1.cancel()
277         }
278 
279     @Test fun refreshOnLatestGenerationReceiver() = refreshOnLatestGenerationReceiver(false)
280 
281     @Test
282     fun refreshOnLatestGenerationReceiver_collectWithCachedIn() =
283         refreshOnLatestGenerationReceiver(true)
284 
285     private fun refreshOnLatestGenerationReceiver(collectWithCachedIn: Boolean) =
286         runTest(collectWithCachedIn) { presenter, _, uiReceivers, hintReceivers ->
287             // first gen
288             advanceUntilIdle()
289             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
290 
291             // append a page so we can cache an anchorPosition of [8]
292             presenter[8]
293             advanceUntilIdle()
294 
295             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
296 
297             // trigger gen 2, the refresh signal should to sent to gen 1
298             presenter.refresh()
299             assertThat(uiReceivers[0].refreshEvents).hasSize(1)
300             assertThat(uiReceivers[1].refreshEvents).hasSize(0)
301 
302             // trigger gen 3, refresh signal should be sent to gen 2
303             presenter.refresh()
304             assertThat(uiReceivers[0].refreshEvents).hasSize(1)
305             assertThat(uiReceivers[1].refreshEvents).hasSize(1)
306             advanceUntilIdle()
307 
308             assertThat(presenter.snapshot()).containsExactlyElementsIn(8 until 17)
309 
310             // access any item to make sure gen 3 receiver is recipient of the hint
311             presenter[0]
312             assertThat(hintReceivers[2].hints)
313                 .containsExactly(
314                     ViewportHint.Access(
315                         pageOffset = 0,
316                         indexInPage = 0,
317                         presentedItemsBefore = 0,
318                         presentedItemsAfter = 8,
319                         originalPageOffsetFirst = 0,
320                         originalPageOffsetLast = 0,
321                     )
322                 )
323         }
324 
325     @Test fun retryOnLatestGenerationReceiver() = retryOnLatestGenerationReceiver(false)
326 
327     @Test
328     fun retryOnLatestGenerationReceiver_collectWithCachedIn() =
329         retryOnLatestGenerationReceiver(true)
330 
331     private fun retryOnLatestGenerationReceiver(collectWithCachedIn: Boolean) =
332         runTest(collectWithCachedIn) { presenter, pagingSources, uiReceivers, hintReceivers ->
333 
334             // first gen
335             advanceUntilIdle()
336             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
337 
338             // append a page so we can cache an anchorPosition of [8]
339             presenter[8]
340             advanceUntilIdle()
341 
342             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
343 
344             // trigger gen 2, the refresh signal should be sent to gen 1
345             presenter.refresh()
346             assertThat(uiReceivers[0].refreshEvents).hasSize(1)
347             assertThat(uiReceivers[1].refreshEvents).hasSize(0)
348 
349             // to recreate a real use-case of retry based on load error
350             pagingSources[1].errorNextLoad = true
351             advanceUntilIdle()
352             // presenter should still have first gen presenter
353             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
354 
355             // retry should be sent to gen 2 even though it wasn't presented
356             presenter.retry()
357             assertThat(uiReceivers[0].retryEvents).hasSize(0)
358             assertThat(uiReceivers[1].retryEvents).hasSize(1)
359             advanceUntilIdle()
360 
361             // will retry with the correct cached hint
362             assertThat(presenter.snapshot()).containsExactlyElementsIn(8 until 17)
363 
364             // access any item to ensure gen 2 receiver was recipient of the initial hint
365             presenter[0]
366             assertThat(hintReceivers[1].hints)
367                 .containsExactly(
368                     ViewportHint.Access(
369                         pageOffset = 0,
370                         indexInPage = 0,
371                         presentedItemsBefore = 0,
372                         presentedItemsAfter = 8,
373                         originalPageOffsetFirst = 0,
374                         originalPageOffsetLast = 0,
375                     )
376                 )
377         }
378 
379     @Test
380     fun refreshAfterStaticList() =
381         testScope.runTest {
382             val presenter = SimplePresenter()
383 
384             val pagingData1 = PagingData.from(listOf(1, 2, 3))
385             val job1 = launch { presenter.collectFrom(pagingData1) }
386             assertTrue(job1.isCompleted)
387             assertThat(presenter.snapshot()).containsAtLeastElementsIn(listOf(1, 2, 3))
388 
389             val uiReceiver = UiReceiverFake()
390             val pagingData2 = infinitelySuspendingPagingData(uiReceiver = uiReceiver)
391             val job2 = launch { presenter.collectFrom(pagingData2) }
392             assertTrue(job2.isActive)
393 
394             // even though the second paging data never presented, it should be receiver of the
395             // refresh
396             presenter.refresh()
397             assertThat(uiReceiver.refreshEvents).hasSize(1)
398 
399             job2.cancel()
400         }
401 
402     @Test
403     fun retryAfterStaticList() =
404         testScope.runTest {
405             val presenter = SimplePresenter()
406 
407             val pagingData1 = PagingData.from(listOf(1, 2, 3))
408             val job1 = launch { presenter.collectFrom(pagingData1) }
409             assertTrue(job1.isCompleted)
410             assertThat(presenter.snapshot()).containsAtLeastElementsIn(listOf(1, 2, 3))
411 
412             val uiReceiver = UiReceiverFake()
413             val pagingData2 = infinitelySuspendingPagingData(uiReceiver = uiReceiver)
414             val job2 = launch { presenter.collectFrom(pagingData2) }
415             assertTrue(job2.isActive)
416 
417             // even though the second paging data never presented, it should be receiver of the
418             // retry
419             presenter.retry()
420             assertThat(uiReceiver.retryEvents).hasSize(1)
421 
422             job2.cancel()
423         }
424 
425     @Test
426     fun hintCalculationBasedOnCurrentGeneration() =
427         testScope.runTest {
428             val presenter = SimplePresenter()
429 
430             // first generation
431             val hintReceiver1 = HintReceiverFake()
432             val uiReceiver1 = UiReceiverFake()
433             val flow =
434                 flowOf(
435                     localRefresh(
436                         pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4))),
437                         placeholdersBefore = 0,
438                         placeholdersAfter = 95
439                     )
440                 )
441 
442             val job1 = launch {
443                 presenter.collectFrom(PagingData(flow, uiReceiver1, hintReceiver1))
444             }
445             // access any item make sure hint is sent
446             presenter[3]
447             assertThat(hintReceiver1.hints)
448                 .containsExactly(
449                     ViewportHint.Access(
450                         pageOffset = 0,
451                         indexInPage = 3,
452                         presentedItemsBefore = 3,
453                         presentedItemsAfter = 1,
454                         originalPageOffsetFirst = 0,
455                         originalPageOffsetLast = 0,
456                     )
457                 )
458 
459             // jump to another position, triggers invalidation
460             presenter[20]
461             assertThat(hintReceiver1.hints)
462                 .isEqualTo(
463                     listOf(
464                         ViewportHint.Access(
465                             pageOffset = 0,
466                             indexInPage = 20,
467                             presentedItemsBefore = 20,
468                             presentedItemsAfter = -16,
469                             originalPageOffsetFirst = 0,
470                             originalPageOffsetLast = 0,
471                         ),
472                     )
473                 )
474 
475             // jump invalidation happens
476             presenter.refresh()
477             assertThat(uiReceiver1.refreshEvents).hasSize(1)
478 
479             // second generation
480             val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
481             val hintReceiver2 = HintReceiverFake()
482             val job2 = launch {
483                 presenter.collectFrom(
484                     PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver2)
485                 )
486             }
487 
488             // jump to another position while second gen is loading. It should be sent to first gen.
489             presenter[40]
490             assertThat(hintReceiver1.hints)
491                 .isEqualTo(
492                     listOf(
493                         ViewportHint.Access(
494                             pageOffset = 0,
495                             indexInPage = 40,
496                             presentedItemsBefore = 40,
497                             presentedItemsAfter = -36,
498                             originalPageOffsetFirst = 0,
499                             originalPageOffsetLast = 0,
500                         ),
501                     )
502                 )
503             assertThat(hintReceiver2.hints).isEmpty()
504 
505             // gen 2 initial load
506             pageEventCh.trySend(
507                 localRefresh(
508                     pages = listOf(TransformablePage(listOf(20, 21, 22, 23, 24))),
509                     placeholdersBefore = 20,
510                     placeholdersAfter = 75
511                 ),
512             )
513             // access any item make sure hint is sent
514             presenter[3]
515             assertThat(hintReceiver2.hints)
516                 .containsExactly(
517                     ViewportHint.Access(
518                         pageOffset = 0,
519                         indexInPage = -17,
520                         presentedItemsBefore = -17,
521                         presentedItemsAfter = 21,
522                         originalPageOffsetFirst = 0,
523                         originalPageOffsetLast = 0,
524                     )
525                 )
526 
527             // jumping to index 50. Hint.indexInPage should be adjusted accordingly based on
528             // the placeholdersBefore of new presenter. It should be
529             // (index - placeholdersBefore) = 50 - 20 = 30
530             presenter[50]
531             assertThat(hintReceiver2.hints)
532                 .isEqualTo(
533                     listOf(
534                         ViewportHint.Access(
535                             pageOffset = 0,
536                             indexInPage = 30,
537                             presentedItemsBefore = 30,
538                             presentedItemsAfter = -26,
539                             originalPageOffsetFirst = 0,
540                             originalPageOffsetLast = 0,
541                         ),
542                     )
543                 )
544 
545             job2.cancel()
546             job1.cancel()
547         }
548 
549     @Test
550     fun fetch_loadHintResentWhenUnfulfilled() =
551         testScope.runTest {
552             val presenter = SimplePresenter()
553 
554             val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
555             pageEventCh.trySend(
556                 localRefresh(
557                     pages = listOf(TransformablePage(0, listOf(0, 1))),
558                     placeholdersBefore = 4,
559                     placeholdersAfter = 4,
560                 )
561             )
562             pageEventCh.trySend(
563                 localPrepend(
564                     pages = listOf(TransformablePage(-1, listOf(-1, -2))),
565                     placeholdersBefore = 2,
566                 )
567             )
568             pageEventCh.trySend(
569                 localAppend(
570                     pages = listOf(TransformablePage(1, listOf(2, 3))),
571                     placeholdersAfter = 2,
572                 )
573             )
574 
575             val hintReceiver = HintReceiverFake()
576             val job = launch {
577                 presenter.collectFrom(
578                     // Filter the original list of 10 items to 5, removing even numbers.
579                     PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver).filter {
580                         it % 2 != 0
581                     }
582                 )
583             }
584 
585             // Initial state:
586             // [null, null, [-1], [1], [3], null, null]
587             assertNull(presenter[0])
588             assertThat(hintReceiver.hints)
589                 .isEqualTo(
590                     listOf(
591                         ViewportHint.Access(
592                             pageOffset = -1,
593                             indexInPage = -2,
594                             presentedItemsBefore = -2,
595                             presentedItemsAfter = 4,
596                             originalPageOffsetFirst = -1,
597                             originalPageOffsetLast = 1
598                         ),
599                     )
600                 )
601 
602             // Insert a new page, PagingDataPresenter should try to resend hint since index 0 still
603             // points
604             // to a placeholder:
605             // [null, null, [], [-1], [1], [3], null, null]
606             pageEventCh.trySend(
607                 localPrepend(
608                     pages = listOf(TransformablePage(-2, listOf())),
609                     placeholdersBefore = 2,
610                 )
611             )
612             assertThat(hintReceiver.hints)
613                 .isEqualTo(
614                     listOf(
615                         ViewportHint.Access(
616                             pageOffset = -2,
617                             indexInPage = -2,
618                             presentedItemsBefore = -2,
619                             presentedItemsAfter = 4,
620                             originalPageOffsetFirst = -2,
621                             originalPageOffsetLast = 1
622                         )
623                     )
624                 )
625 
626             // Now index 0 has been loaded:
627             // [[-3], [], [-1], [1], [3], null, null]
628             pageEventCh.trySend(
629                 localPrepend(
630                     pages = listOf(TransformablePage(-3, listOf(-3, -4))),
631                     placeholdersBefore = 0,
632                     source = loadStates(prepend = NotLoading.Complete)
633                 )
634             )
635             assertThat(hintReceiver.hints).isEmpty()
636 
637             // This index points to a valid placeholder that ends up removed by filter().
638             assertNull(presenter[5])
639             assertThat(hintReceiver.hints)
640                 .containsExactly(
641                     ViewportHint.Access(
642                         pageOffset = 1,
643                         indexInPage = 2,
644                         presentedItemsBefore = 5,
645                         presentedItemsAfter = -2,
646                         originalPageOffsetFirst = -3,
647                         originalPageOffsetLast = 1
648                     )
649                 )
650 
651             // Should only resend the hint for index 5, since index 0 has already been loaded:
652             // [[-3], [], [-1], [1], [3], [], null, null]
653             pageEventCh.trySend(
654                 localAppend(
655                     pages = listOf(TransformablePage(2, listOf())),
656                     placeholdersAfter = 2,
657                     source = loadStates(prepend = NotLoading.Complete)
658                 )
659             )
660             assertThat(hintReceiver.hints)
661                 .isEqualTo(
662                     listOf(
663                         ViewportHint.Access(
664                             pageOffset = 2,
665                             indexInPage = 1,
666                             presentedItemsBefore = 5,
667                             presentedItemsAfter = -2,
668                             originalPageOffsetFirst = -3,
669                             originalPageOffsetLast = 2
670                         )
671                     )
672                 )
673 
674             // Index 5 hasn't loaded, but we are at the end of the list:
675             // [[-3], [], [-1], [1], [3], [], [5]]
676             pageEventCh.trySend(
677                 localAppend(
678                     pages = listOf(TransformablePage(3, listOf(4, 5))),
679                     placeholdersAfter = 0,
680                     source = loadStates(prepend = NotLoading.Complete, append = NotLoading.Complete)
681                 )
682             )
683             assertThat(hintReceiver.hints).isEmpty()
684 
685             job.cancel()
686         }
687 
688     @Test
689     fun fetch_loadHintResentUnlessPageDropped() =
690         testScope.runTest {
691             val presenter = SimplePresenter()
692 
693             val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
694             pageEventCh.trySend(
695                 localRefresh(
696                     pages = listOf(TransformablePage(0, listOf(0, 1))),
697                     placeholdersBefore = 4,
698                     placeholdersAfter = 4,
699                 )
700             )
701             pageEventCh.trySend(
702                 localPrepend(
703                     pages = listOf(TransformablePage(-1, listOf(-1, -2))),
704                     placeholdersBefore = 2,
705                 )
706             )
707             pageEventCh.trySend(
708                 localAppend(
709                     pages = listOf(TransformablePage(1, listOf(2, 3))),
710                     placeholdersAfter = 2,
711                 )
712             )
713 
714             val hintReceiver = HintReceiverFake()
715             val job = launch {
716                 presenter.collectFrom(
717                     // Filter the original list of 10 items to 5, removing even numbers.
718                     PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver).filter {
719                         it % 2 != 0
720                     }
721                 )
722             }
723 
724             // Initial state:
725             // [null, null, [-1], [1], [3], null, null]
726             assertNull(presenter[0])
727             assertThat(hintReceiver.hints)
728                 .containsExactly(
729                     ViewportHint.Access(
730                         pageOffset = -1,
731                         indexInPage = -2,
732                         presentedItemsBefore = -2,
733                         presentedItemsAfter = 4,
734                         originalPageOffsetFirst = -1,
735                         originalPageOffsetLast = 1
736                     )
737                 )
738 
739             // Insert a new page, PagingDataPresenter should try to resend hint since index 0 still
740             // points
741             // to a placeholder:
742             // [null, null, [], [-1], [1], [3], null, null]
743             pageEventCh.trySend(
744                 localPrepend(
745                     pages = listOf(TransformablePage(-2, listOf())),
746                     placeholdersBefore = 2,
747                 )
748             )
749             assertThat(hintReceiver.hints)
750                 .isEqualTo(
751                     listOf(
752                         ViewportHint.Access(
753                             pageOffset = -2,
754                             indexInPage = -2,
755                             presentedItemsBefore = -2,
756                             presentedItemsAfter = 4,
757                             originalPageOffsetFirst = -2,
758                             originalPageOffsetLast = 1
759                         )
760                     )
761                 )
762 
763             // Drop the previous page, which reset resendable index state in the PREPEND direction.
764             // [null, null, [-1], [1], [3], null, null]
765             pageEventCh.trySend(
766                 Drop(
767                     loadType = PREPEND,
768                     minPageOffset = -2,
769                     maxPageOffset = -2,
770                     placeholdersRemaining = 2
771                 )
772             )
773 
774             // Re-insert the previous page, which should not trigger resending the index due to
775             // previous page drop:
776             // [[-3], [], [-1], [1], [3], null, null]
777             pageEventCh.trySend(
778                 localPrepend(
779                     pages = listOf(TransformablePage(-2, listOf())),
780                     placeholdersBefore = 2,
781                 )
782             )
783 
784             job.cancel()
785         }
786 
787     @Test
788     fun peek() =
789         testScope.runTest {
790             val presenter = SimplePresenter()
791             val pageEventCh = Channel<PageEvent<Int>>(Channel.UNLIMITED)
792             pageEventCh.trySend(
793                 localRefresh(
794                     pages = listOf(TransformablePage(0, listOf(0, 1))),
795                     placeholdersBefore = 4,
796                     placeholdersAfter = 4,
797                 )
798             )
799             pageEventCh.trySend(
800                 localPrepend(
801                     pages = listOf(TransformablePage(-1, listOf(-1, -2))),
802                     placeholdersBefore = 2,
803                 )
804             )
805             pageEventCh.trySend(
806                 localAppend(
807                     pages = listOf(TransformablePage(1, listOf(2, 3))),
808                     placeholdersAfter = 2,
809                 )
810             )
811 
812             val hintReceiver = HintReceiverFake()
813             val job = launch {
814                 presenter.collectFrom(
815                     // Filter the original list of 10 items to 5, removing even numbers.
816                     PagingData(pageEventCh.consumeAsFlow(), dummyUiReceiver, hintReceiver)
817                 )
818             }
819 
820             // Check that peek fetches the correct placeholder
821             assertThat(presenter.peek(4)).isEqualTo(0)
822 
823             // Check that peek fetches the correct placeholder
824             assertNull(presenter.peek(0))
825 
826             // Check that peek does not trigger page fetch.
827             assertThat(hintReceiver.hints).isEmpty()
828 
829             job.cancel()
830         }
831 
832     @Test
833     fun onPagingDataPresentedListener_empty() =
834         testScope.runTest {
835             val presenter = SimplePresenter()
836             val listenerEvents = mutableListOf<Unit>()
837             presenter.addOnPagesUpdatedListener { listenerEvents.add(Unit) }
838 
839             presenter.collectFrom(PagingData.empty())
840             assertThat(listenerEvents.size).isEqualTo(1)
841 
842             // No change to LoadState or presented list should still trigger the listener.
843             presenter.collectFrom(PagingData.empty())
844             assertThat(listenerEvents.size).isEqualTo(2)
845 
846             val pager = Pager(PagingConfig(pageSize = 1)) { TestPagingSource(items = listOf()) }
847             val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
848 
849             // Should wait for new generation to load and apply it first.
850             assertThat(listenerEvents.size).isEqualTo(2)
851 
852             advanceUntilIdle()
853             assertThat(listenerEvents.size).isEqualTo(3)
854 
855             job.cancel()
856         }
857 
858     @Test
859     fun onPagingDataPresentedListener_insertDrop() =
860         testScope.runTest {
861             val presenter = SimplePresenter()
862             val listenerEvents = mutableListOf<Unit>()
863             presenter.addOnPagesUpdatedListener { listenerEvents.add(Unit) }
864 
865             val pager =
866                 Pager(PagingConfig(pageSize = 1, maxSize = 4), initialKey = 50) {
867                     TestPagingSource()
868                 }
869             val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
870 
871             // Should wait for new generation to load and apply it first.
872             assertThat(listenerEvents.size).isEqualTo(0)
873 
874             advanceUntilIdle()
875             assertThat(listenerEvents.size).isEqualTo(1)
876 
877             // Trigger PREPEND.
878             presenter[50]
879             assertThat(listenerEvents.size).isEqualTo(1)
880             advanceUntilIdle()
881             assertThat(listenerEvents.size).isEqualTo(2)
882 
883             // Trigger APPEND + Drop
884             presenter[52]
885             assertThat(listenerEvents.size).isEqualTo(2)
886             advanceUntilIdle()
887             assertThat(listenerEvents.size).isEqualTo(4)
888 
889             job.cancel()
890         }
891 
892     @Test
893     fun onPagingDataPresentedFlow_empty() =
894         testScope.runTest {
895             val presenter = SimplePresenter()
896             val listenerEvents = mutableListOf<Unit>()
897             val job1 = launch { presenter.onPagesUpdatedFlow.collect { listenerEvents.add(Unit) } }
898 
899             presenter.collectFrom(PagingData.empty())
900             assertThat(listenerEvents.size).isEqualTo(1)
901 
902             // No change to LoadState or presented list should still trigger the listener.
903             presenter.collectFrom(PagingData.empty())
904             assertThat(listenerEvents.size).isEqualTo(2)
905 
906             val pager = Pager(PagingConfig(pageSize = 1)) { TestPagingSource(items = listOf()) }
907             val job2 = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
908 
909             // Should wait for new generation to load and apply it first.
910             assertThat(listenerEvents.size).isEqualTo(2)
911 
912             advanceUntilIdle()
913             assertThat(listenerEvents.size).isEqualTo(3)
914 
915             job1.cancel()
916             job2.cancel()
917         }
918 
919     @Test
920     fun onPagingDataPresentedFlow_insertDrop() =
921         testScope.runTest {
922             val presenter = SimplePresenter()
923             val listenerEvents = mutableListOf<Unit>()
924             val job1 = launch { presenter.onPagesUpdatedFlow.collect { listenerEvents.add(Unit) } }
925 
926             val pager =
927                 Pager(PagingConfig(pageSize = 1, maxSize = 4), initialKey = 50) {
928                     TestPagingSource()
929                 }
930             val job2 = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
931 
932             // Should wait for new generation to load and apply it first.
933             assertThat(listenerEvents.size).isEqualTo(0)
934 
935             advanceUntilIdle()
936             assertThat(listenerEvents.size).isEqualTo(1)
937 
938             // Trigger PREPEND.
939             presenter[50]
940             assertThat(listenerEvents.size).isEqualTo(1)
941             advanceUntilIdle()
942             assertThat(listenerEvents.size).isEqualTo(2)
943 
944             // Trigger APPEND + Drop
945             presenter[52]
946             assertThat(listenerEvents.size).isEqualTo(2)
947             advanceUntilIdle()
948             assertThat(listenerEvents.size).isEqualTo(4)
949 
950             job1.cancel()
951             job2.cancel()
952         }
953 
954     @Test
955     fun onPagingDataPresentedFlow_buffer() =
956         testScope.runTest {
957             val presenter = SimplePresenter()
958             val listenerEvents = mutableListOf<Unit>()
959 
960             // Trigger update, which should get ignored due to onPagesUpdatedFlow being hot.
961             presenter.collectFrom(PagingData.empty())
962 
963             val job = launch {
964                 presenter.onPagesUpdatedFlow.collect {
965                     listenerEvents.add(Unit)
966                     // Await advanceUntilIdle() before accepting another event.
967                     delay(100)
968                 }
969             }
970 
971             // Previous update before collection happened should be ignored.
972             assertThat(listenerEvents.size).isEqualTo(0)
973 
974             // Trigger update; should get immediately received.
975             presenter.collectFrom(PagingData.empty())
976             assertThat(listenerEvents.size).isEqualTo(1)
977 
978             // Trigger 64 update while collector is still processing; should all get buffered.
979             repeat(64) { presenter.collectFrom(PagingData.empty()) }
980 
981             // Trigger another update while collector is still processing; should cause event to
982             // drop.
983             presenter.collectFrom(PagingData.empty())
984 
985             // Await all; we should now receive the buffered event.
986             advanceUntilIdle()
987             assertThat(listenerEvents.size).isEqualTo(65)
988 
989             job.cancel()
990         }
991 
992     @Test
993     fun loadStateFlow_synchronouslyUpdates() =
994         testScope.runTest {
995             val presenter = SimplePresenter()
996             var combinedLoadStates: CombinedLoadStates? = null
997             var itemCount = -1
998             val loadStateJob = launch {
999                 presenter.nonNullLoadStateFlow.collect {
1000                     combinedLoadStates = it
1001                     itemCount = presenter.size
1002                 }
1003             }
1004 
1005             val pager =
1006                 Pager(
1007                     config =
1008                         PagingConfig(
1009                             pageSize = 10,
1010                             enablePlaceholders = false,
1011                             initialLoadSize = 10,
1012                             prefetchDistance = 1
1013                         ),
1014                     initialKey = 50
1015                 ) {
1016                     TestPagingSource()
1017                 }
1018             val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
1019 
1020             // Initial refresh
1021             advanceUntilIdle()
1022             assertEquals(localLoadStatesOf(), combinedLoadStates)
1023             assertEquals(10, itemCount)
1024             assertEquals(10, presenter.size)
1025 
1026             // Append
1027             presenter[9]
1028             advanceUntilIdle()
1029             assertEquals(localLoadStatesOf(), combinedLoadStates)
1030             assertEquals(20, itemCount)
1031             assertEquals(20, presenter.size)
1032 
1033             // Prepend
1034             presenter[0]
1035             advanceUntilIdle()
1036             assertEquals(localLoadStatesOf(), combinedLoadStates)
1037             assertEquals(30, itemCount)
1038             assertEquals(30, presenter.size)
1039 
1040             job.cancel()
1041             loadStateJob.cancel()
1042         }
1043 
1044     @Test
1045     fun loadStateFlow_hasNoInitialValue() =
1046         testScope.runTest {
1047             val presenter = SimplePresenter()
1048 
1049             // Should not immediately emit without a real value to a new collector.
1050             val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1051             val loadStateJob = launch {
1052                 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1053             }
1054             assertThat(combinedLoadStates).isEmpty()
1055 
1056             // Add a real value and now we should emit to collector.
1057             presenter.collectFrom(
1058                 PagingData.empty(
1059                     sourceLoadStates =
1060                         loadStates(prepend = NotLoading.Complete, append = NotLoading.Complete)
1061                 )
1062             )
1063             assertThat(combinedLoadStates)
1064                 .containsExactly(
1065                     localLoadStatesOf(
1066                         prependLocal = NotLoading.Complete,
1067                         appendLocal = NotLoading.Complete,
1068                     )
1069                 )
1070 
1071             // Should emit real values to new collectors immediately
1072             val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
1073             val newLoadStateJob = launch {
1074                 presenter.nonNullLoadStateFlow.collect { newCombinedLoadStates.add(it) }
1075             }
1076             assertThat(newCombinedLoadStates)
1077                 .containsExactly(
1078                     localLoadStatesOf(
1079                         prependLocal = NotLoading.Complete,
1080                         appendLocal = NotLoading.Complete,
1081                     )
1082                 )
1083 
1084             loadStateJob.cancel()
1085             newLoadStateJob.cancel()
1086         }
1087 
1088     @Test
1089     fun loadStateFlow_preservesLoadStatesOnEmptyList() =
1090         testScope.runTest {
1091             val presenter = SimplePresenter()
1092 
1093             // Should not immediately emit without a real value to a new collector.
1094             val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1095             val loadStateJob = launch {
1096                 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1097             }
1098             assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1099 
1100             // Send a static list without load states, which should not send anything.
1101             presenter.collectFrom(PagingData.empty())
1102             assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1103 
1104             // Send a real LoadStateUpdate.
1105             presenter.collectFrom(
1106                 PagingData(
1107                     flow =
1108                         flowOf(
1109                             remoteLoadStateUpdate(
1110                                 refreshLocal = Loading,
1111                                 prependLocal = Loading,
1112                                 appendLocal = Loading,
1113                                 refreshRemote = Loading,
1114                                 prependRemote = Loading,
1115                                 appendRemote = Loading,
1116                             )
1117                         ),
1118                     uiReceiver = PagingData.NOOP_UI_RECEIVER,
1119                     hintReceiver = PagingData.NOOP_HINT_RECEIVER
1120                 )
1121             )
1122             assertThat(combinedLoadStates.getAllAndClear())
1123                 .containsExactly(
1124                     remoteLoadStatesOf(
1125                         refresh = Loading,
1126                         prepend = Loading,
1127                         append = Loading,
1128                         refreshLocal = Loading,
1129                         prependLocal = Loading,
1130                         appendLocal = Loading,
1131                         refreshRemote = Loading,
1132                         prependRemote = Loading,
1133                         appendRemote = Loading,
1134                     )
1135                 )
1136 
1137             // Send a static list without load states, which should preserve the previous state.
1138             presenter.collectFrom(PagingData.empty())
1139             // Existing observers should not receive any updates
1140             assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1141             // New observers should receive the previous state.
1142             val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
1143             val newLoadStateJob = launch {
1144                 presenter.nonNullLoadStateFlow.collect { newCombinedLoadStates.add(it) }
1145             }
1146             assertThat(newCombinedLoadStates.getAllAndClear())
1147                 .containsExactly(
1148                     remoteLoadStatesOf(
1149                         refresh = Loading,
1150                         prepend = Loading,
1151                         append = Loading,
1152                         refreshLocal = Loading,
1153                         prependLocal = Loading,
1154                         appendLocal = Loading,
1155                         refreshRemote = Loading,
1156                         prependRemote = Loading,
1157                         appendRemote = Loading,
1158                     )
1159                 )
1160 
1161             loadStateJob.cancel()
1162             newLoadStateJob.cancel()
1163         }
1164 
1165     @Test
1166     fun loadStateFlow_preservesLoadStatesOnStaticList() =
1167         testScope.runTest {
1168             val presenter = SimplePresenter()
1169 
1170             // Should not immediately emit without a real value to a new collector.
1171             val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1172             val loadStateJob = launch {
1173                 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1174             }
1175             assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1176 
1177             // Send a static list without load states, which should not send anything.
1178             presenter.collectFrom(PagingData.from(listOf(1)))
1179             assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1180 
1181             // Send a real LoadStateUpdate.
1182             presenter.collectFrom(
1183                 PagingData(
1184                     flow =
1185                         flowOf(
1186                             remoteLoadStateUpdate(
1187                                 refreshLocal = Loading,
1188                                 prependLocal = Loading,
1189                                 appendLocal = Loading,
1190                                 refreshRemote = Loading,
1191                                 prependRemote = Loading,
1192                                 appendRemote = Loading,
1193                             )
1194                         ),
1195                     uiReceiver = PagingData.NOOP_UI_RECEIVER,
1196                     hintReceiver = PagingData.NOOP_HINT_RECEIVER
1197                 )
1198             )
1199             assertThat(combinedLoadStates.getAllAndClear())
1200                 .containsExactly(
1201                     remoteLoadStatesOf(
1202                         refresh = Loading,
1203                         prepend = Loading,
1204                         append = Loading,
1205                         refreshLocal = Loading,
1206                         prependLocal = Loading,
1207                         appendLocal = Loading,
1208                         refreshRemote = Loading,
1209                         prependRemote = Loading,
1210                         appendRemote = Loading,
1211                     )
1212                 )
1213 
1214             // Send a static list without load states, which should preserve the previous state.
1215             presenter.collectFrom(PagingData.from(listOf(1)))
1216             // Existing observers should not receive any updates
1217             assertThat(combinedLoadStates.getAllAndClear()).isEmpty()
1218             // New observers should receive the previous state.
1219             val newCombinedLoadStates = mutableListOf<CombinedLoadStates>()
1220             val newLoadStateJob = launch {
1221                 presenter.nonNullLoadStateFlow.collect { newCombinedLoadStates.add(it) }
1222             }
1223             assertThat(newCombinedLoadStates.getAllAndClear())
1224                 .containsExactly(
1225                     remoteLoadStatesOf(
1226                         refresh = Loading,
1227                         prepend = Loading,
1228                         append = Loading,
1229                         refreshLocal = Loading,
1230                         prependLocal = Loading,
1231                         appendLocal = Loading,
1232                         refreshRemote = Loading,
1233                         prependRemote = Loading,
1234                         appendRemote = Loading,
1235                     )
1236                 )
1237 
1238             loadStateJob.cancel()
1239             newLoadStateJob.cancel()
1240         }
1241 
1242     @Test
1243     fun loadStateFlow_deduplicate() =
1244         testScope.runTest {
1245             val presenter = SimplePresenter()
1246 
1247             val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1248             backgroundScope.launch {
1249                 presenter.nonNullLoadStateFlow.collect { combinedLoadStates.add(it) }
1250             }
1251 
1252             presenter.collectFrom(
1253                 PagingData(
1254                     flow =
1255                         flowOf(
1256                             remoteLoadStateUpdate(
1257                                 prependLocal = Loading,
1258                                 appendLocal = Loading,
1259                             ),
1260                             remoteLoadStateUpdate(
1261                                 appendLocal = Loading,
1262                             ),
1263                             // duplicate update
1264                             remoteLoadStateUpdate(
1265                                 appendLocal = Loading,
1266                             ),
1267                         ),
1268                     uiReceiver = PagingData.NOOP_UI_RECEIVER,
1269                     hintReceiver = PagingData.NOOP_HINT_RECEIVER
1270                 )
1271             )
1272             advanceUntilIdle()
1273             assertThat(combinedLoadStates)
1274                 .containsExactly(
1275                     remoteLoadStatesOf(
1276                         prependLocal = Loading,
1277                         appendLocal = Loading,
1278                     ),
1279                     remoteLoadStatesOf(
1280                         appendLocal = Loading,
1281                     )
1282                 )
1283         }
1284 
1285     @Test
1286     fun loadStateFlowListeners_deduplicate() =
1287         testScope.runTest {
1288             val presenter = SimplePresenter()
1289             val combinedLoadStates = mutableListOf<CombinedLoadStates>()
1290 
1291             presenter.addLoadStateListener { combinedLoadStates.add(it) }
1292 
1293             presenter.collectFrom(
1294                 PagingData(
1295                     flow =
1296                         flowOf(
1297                             remoteLoadStateUpdate(
1298                                 prependLocal = Loading,
1299                                 appendLocal = Loading,
1300                             ),
1301                             remoteLoadStateUpdate(
1302                                 appendLocal = Loading,
1303                             ),
1304                             // duplicate update
1305                             remoteLoadStateUpdate(
1306                                 appendLocal = Loading,
1307                             ),
1308                         ),
1309                     uiReceiver = PagingData.NOOP_UI_RECEIVER,
1310                     hintReceiver = PagingData.NOOP_HINT_RECEIVER
1311                 )
1312             )
1313             advanceUntilIdle()
1314             assertThat(combinedLoadStates)
1315                 .containsExactly(
1316                     remoteLoadStatesOf(
1317                         prependLocal = Loading,
1318                         appendLocal = Loading,
1319                     ),
1320                     remoteLoadStatesOf(
1321                         appendLocal = Loading,
1322                     )
1323                 )
1324         }
1325 
1326     @Test
1327     fun addLoadStateListener_SynchronouslyUpdates() =
1328         testScope.runTest {
1329             val presenter = SimplePresenter()
1330             var combinedLoadStates: CombinedLoadStates? = null
1331             var itemCount = -1
1332             presenter.addLoadStateListener {
1333                 combinedLoadStates = it
1334                 itemCount = presenter.size
1335             }
1336 
1337             val pager =
1338                 Pager(
1339                     config =
1340                         PagingConfig(
1341                             pageSize = 10,
1342                             enablePlaceholders = false,
1343                             initialLoadSize = 10,
1344                             prefetchDistance = 1
1345                         ),
1346                     initialKey = 50
1347                 ) {
1348                     TestPagingSource()
1349                 }
1350             val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
1351 
1352             // Initial refresh
1353             advanceUntilIdle()
1354             assertEquals(localLoadStatesOf(), combinedLoadStates)
1355             assertEquals(10, itemCount)
1356             assertEquals(10, presenter.size)
1357 
1358             // Append
1359             presenter[9]
1360             advanceUntilIdle()
1361             assertEquals(localLoadStatesOf(), combinedLoadStates)
1362             assertEquals(20, itemCount)
1363             assertEquals(20, presenter.size)
1364 
1365             // Prepend
1366             presenter[0]
1367             advanceUntilIdle()
1368             assertEquals(localLoadStatesOf(), combinedLoadStates)
1369             assertEquals(30, itemCount)
1370             assertEquals(30, presenter.size)
1371 
1372             job.cancel()
1373         }
1374 
1375     @Test
1376     fun addLoadStateListener_hasNoInitialValue() =
1377         testScope.runTest {
1378             val presenter = SimplePresenter()
1379             val combinedLoadStateCapture = CombinedLoadStatesCapture()
1380 
1381             // Adding a new listener without a real value should not trigger it.
1382             presenter.addLoadStateListener(combinedLoadStateCapture)
1383             assertThat(combinedLoadStateCapture.newEvents()).isEmpty()
1384 
1385             // Add a real value and now the listener should trigger.
1386             presenter.collectFrom(
1387                 PagingData.empty(
1388                     sourceLoadStates =
1389                         loadStates(
1390                             prepend = NotLoading.Complete,
1391                             append = NotLoading.Complete,
1392                         )
1393                 )
1394             )
1395             assertThat(combinedLoadStateCapture.newEvents())
1396                 .containsExactly(
1397                     localLoadStatesOf(
1398                         prependLocal = NotLoading.Complete,
1399                         appendLocal = NotLoading.Complete,
1400                     )
1401                 )
1402 
1403             // Should emit real values to new listeners immediately
1404             val newCombinedLoadStateCapture = CombinedLoadStatesCapture()
1405             presenter.addLoadStateListener(newCombinedLoadStateCapture)
1406             assertThat(newCombinedLoadStateCapture.newEvents())
1407                 .containsExactly(
1408                     localLoadStatesOf(
1409                         prependLocal = NotLoading.Complete,
1410                         appendLocal = NotLoading.Complete,
1411                     )
1412                 )
1413         }
1414 
1415     @Test
1416     fun uncaughtException() =
1417         testScope.runTest {
1418             val presenter = SimplePresenter()
1419             val pager =
1420                 Pager(
1421                     PagingConfig(1),
1422                 ) {
1423                     object : PagingSource<Int, Int>() {
1424                         override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
1425                             throw IllegalStateException()
1426                         }
1427 
1428                         override fun getRefreshKey(state: PagingState<Int, Int>): Int? = null
1429                     }
1430                 }
1431 
1432             val pagingData = pager.flow.first()
1433             val deferred = async(Job()) { presenter.collectFrom(pagingData) }
1434 
1435             advanceUntilIdle()
1436             assertFailsWith<IllegalStateException> { deferred.await() }
1437         }
1438 
1439     @Test
1440     fun handledLoadResultInvalid() =
1441         testScope.runTest {
1442             val presenter = SimplePresenter()
1443             var generation = 0
1444             val pager =
1445                 Pager(
1446                     PagingConfig(1),
1447                 ) {
1448                     TestPagingSource().also {
1449                         if (generation == 0) {
1450                             it.nextLoadResult = LoadResult.Invalid()
1451                         }
1452                         generation++
1453                     }
1454                 }
1455 
1456             val pagingData = pager.flow.first()
1457             val deferred = async {
1458                 // only returns if flow is closed, or work canclled, or exception thrown
1459                 // in this case it should cancel due LoadResult.Invalid causing collectFrom to
1460                 // return
1461                 presenter.collectFrom(pagingData)
1462             }
1463 
1464             advanceUntilIdle()
1465             // this will return only if presenter.collectFrom returns
1466             deferred.await()
1467         }
1468 
1469     @Test fun refresh_pagingDataEvent() = refresh_pagingDataEvent(false)
1470 
1471     @Test fun refresh_pagingDataEvent_collectWithCachedIn() = refresh_pagingDataEvent(true)
1472 
1473     private fun refresh_pagingDataEvent(collectWithCachedIn: Boolean) =
1474         runTest(collectWithCachedIn, initialKey = 50) { presenter, _, _, _ ->
1475             // execute queued initial REFRESH
1476             advanceUntilIdle()
1477 
1478             val event =
1479                 PageStore(
1480                     pages =
1481                         listOf(
1482                             TransformablePage(
1483                                 originalPageOffsets = intArrayOf(0),
1484                                 data = listOf(50, 51, 52, 53, 54, 55, 56, 57, 58),
1485                                 hintOriginalPageOffset = 0,
1486                                 hintOriginalIndices = null,
1487                             )
1488                         ),
1489                     placeholdersBefore = 0,
1490                     placeholdersAfter = 0,
1491                 )
1492                     as PlaceholderPaddedList<Int>
1493 
1494             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1495             assertThat(presenter.newEvents())
1496                 .containsExactly(
1497                     PagingDataEvent.Refresh(
1498                         previousList = PageStore.initial<Int>(null) as PlaceholderPaddedList<Int>,
1499                         newList = event
1500                     )
1501                 )
1502 
1503             presenter.refresh()
1504 
1505             // execute second REFRESH load
1506             advanceUntilIdle()
1507 
1508             // // second refresh loads from initialKey = 0 because anchorPosition/refreshKey is null
1509             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1510             assertThat(presenter.newEvents())
1511                 .containsExactly(
1512                     PagingDataEvent.Refresh(
1513                         previousList = event,
1514                         newList =
1515                             PageStore(
1516                                 pages =
1517                                     listOf(
1518                                         TransformablePage(
1519                                             originalPageOffsets = intArrayOf(0),
1520                                             data = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8),
1521                                             hintOriginalPageOffset = 0,
1522                                             hintOriginalIndices = null,
1523                                         )
1524                                     ),
1525                                 placeholdersBefore = 0,
1526                                 placeholdersAfter = 0,
1527                             )
1528                                 as PlaceholderPaddedList<Int>
1529                     )
1530                 )
1531         }
1532 
1533     @Test fun append_pagingDataEvent() = append_pagingDataEvent(false)
1534 
1535     @Test fun append_pagingDataEvent_collectWithCachedIn() = append_pagingDataEvent(true)
1536 
1537     private fun append_pagingDataEvent(collectWithCachedIn: Boolean) =
1538         runTest(collectWithCachedIn) { presenter, _, _, _ ->
1539 
1540             // initial REFRESH
1541             advanceUntilIdle()
1542 
1543             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1544 
1545             // trigger append
1546             presenter[7]
1547             advanceUntilIdle()
1548 
1549             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
1550             assertThat(presenter.newEvents().last())
1551                 .isEqualTo(
1552                     PagingDataEvent.Append(
1553                         startIndex = 9,
1554                         inserted = listOf(9, 10, 11),
1555                         newPlaceholdersAfter = 0,
1556                         oldPlaceholdersAfter = 0
1557                     )
1558                 )
1559         }
1560 
1561     @Test fun appendDrop_pagingDataEvent() = appendDrop_pagingDataEvent(false)
1562 
1563     @Test fun appendDrop_pagingDataEvent_collectWithCachedIn() = appendDrop_pagingDataEvent(true)
1564 
1565     private fun appendDrop_pagingDataEvent(collectWithCachedIn: Boolean) =
1566         runTest(
1567             collectWithCachedIn,
1568             initialKey = 96,
1569             config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1570         ) { presenter, _, _, _ ->
1571             // initial REFRESH
1572             advanceUntilIdle()
1573 
1574             assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 99)
1575 
1576             // trigger append to reach max page size
1577             presenter[2]
1578             advanceUntilIdle()
1579 
1580             assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 100)
1581             assertThat(presenter.newEvents().last())
1582                 .isEqualTo(
1583                     PagingDataEvent.Append(
1584                         startIndex = 3,
1585                         inserted = listOf(99),
1586                         newPlaceholdersAfter = 0,
1587                         oldPlaceholdersAfter = 0
1588                     )
1589                 )
1590             // trigger prepend and drop from append direction
1591             presenter[0]
1592             advanceUntilIdle()
1593 
1594             assertThat(presenter.snapshot()).containsExactlyElementsIn(95 until 99)
1595             // drop is processed before inserts
1596             assertThat(presenter.newEvents().first())
1597                 .isEqualTo(
1598                     PagingDataEvent.DropAppend<Int>(
1599                         startIndex = 3,
1600                         dropCount = 1,
1601                         newPlaceholdersAfter = 0,
1602                         oldPlaceholdersAfter = 0
1603                     )
1604                 )
1605         }
1606 
1607     @Test fun prepend_pagingDataEvent() = prepend_pagingDataEvent(false)
1608 
1609     @Test fun prepend_pagingDataEvent_collectWithCachedIn() = prepend_pagingDataEvent(true)
1610 
1611     private fun prepend_pagingDataEvent(collectWithCachedIn: Boolean) =
1612         runTest(collectWithCachedIn, initialKey = 50) { presenter, _, _, _ ->
1613 
1614             // initial REFRESH
1615             advanceUntilIdle()
1616 
1617             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1618 
1619             // trigger prepend
1620             presenter[0]
1621             advanceUntilIdle()
1622 
1623             assertThat(presenter.snapshot()).containsExactlyElementsIn(47 until 59)
1624             assertThat(presenter.newEvents().last())
1625                 .isEqualTo(
1626                     PagingDataEvent.Prepend(
1627                         inserted = listOf(47, 48, 49),
1628                         newPlaceholdersBefore = 0,
1629                         oldPlaceholdersBefore = 0
1630                     )
1631                 )
1632         }
1633 
1634     @Test fun prependDrop_pagingDataEvent() = prependDrop_pagingDataEvent(false)
1635 
1636     @Test fun prependDrop_pagingDataEvent_collectWithCachedIn() = prependDrop_pagingDataEvent(true)
1637 
1638     private fun prependDrop_pagingDataEvent(collectWithCachedIn: Boolean) =
1639         runTest(
1640             collectWithCachedIn,
1641             initialKey = 1,
1642             config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1643         ) { presenter, _, _, _ ->
1644             // initial REFRESH
1645             advanceUntilIdle()
1646 
1647             assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 4)
1648 
1649             // trigger prepend to reach max page size
1650             presenter[0]
1651             advanceUntilIdle()
1652 
1653             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 4)
1654             assertThat(presenter.newEvents().last())
1655                 .isEqualTo(
1656                     PagingDataEvent.Prepend(
1657                         inserted = listOf(0),
1658                         newPlaceholdersBefore = 0,
1659                         oldPlaceholdersBefore = 0
1660                     )
1661                 )
1662 
1663             // trigger append and drop from prepend direction
1664             presenter[3]
1665             advanceUntilIdle()
1666 
1667             assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 5)
1668             // drop is processed before insert
1669             assertThat(presenter.newEvents().first())
1670                 .isEqualTo(
1671                     PagingDataEvent.DropPrepend<Int>(
1672                         dropCount = 1,
1673                         newPlaceholdersBefore = 0,
1674                         oldPlaceholdersBefore = 0
1675                     )
1676                 )
1677         }
1678 
1679     @Test fun refresh_loadStates() = refresh_loadStates(false)
1680 
1681     @Test fun refresh_loadStates_collectWithCachedIn() = refresh_loadStates(true)
1682 
1683     private fun refresh_loadStates(collectWithCachedIn: Boolean) =
1684         runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
1685             val collectLoadStates = launch { presenter.collectLoadStates() }
1686 
1687             // execute queued initial REFRESH
1688             advanceUntilIdle()
1689 
1690             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1691             assertThat(presenter.newCombinedLoadStates())
1692                 .containsExactly(
1693                     localLoadStatesOf(refreshLocal = Loading),
1694                     localLoadStatesOf(),
1695                 )
1696 
1697             presenter.refresh()
1698 
1699             // execute second REFRESH load
1700             advanceUntilIdle()
1701 
1702             // second refresh loads from initialKey = 0 because anchorPosition/refreshKey is null
1703             assertThat(pagingSources.size).isEqualTo(2)
1704             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1705             assertThat(presenter.newCombinedLoadStates())
1706                 .containsExactly(
1707                     localLoadStatesOf(refreshLocal = Loading),
1708                     localLoadStatesOf(prependLocal = NotLoading.Complete)
1709                 )
1710 
1711             collectLoadStates.cancel()
1712         }
1713 
1714     @Test
1715     fun refresh_loadStates_afterEndOfPagination() = refresh_loadStates_afterEndOfPagination(false)
1716 
1717     @Test
1718     fun refresh_loadStates_afterEndOfPagination_collectWithCachedIn() =
1719         refresh_loadStates_afterEndOfPagination(true)
1720 
1721     private fun refresh_loadStates_afterEndOfPagination(collectWithCachedIn: Boolean) =
1722         runTest(collectWithCachedIn) { presenter, _, _, _ ->
1723             val loadStateCallbacks = mutableListOf<CombinedLoadStates>()
1724             presenter.addLoadStateListener { loadStateCallbacks.add(it) }
1725             val collectLoadStates = launch { presenter.collectLoadStates() }
1726             // execute initial refresh
1727             advanceUntilIdle()
1728             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1729             assertThat(presenter.newCombinedLoadStates())
1730                 .containsExactly(
1731                     localLoadStatesOf(refreshLocal = Loading),
1732                     localLoadStatesOf(
1733                         refreshLocal = NotLoading(endOfPaginationReached = false),
1734                         prependLocal = NotLoading(endOfPaginationReached = true)
1735                     )
1736                 )
1737             loadStateCallbacks.clear()
1738             presenter.refresh()
1739             // after a refresh, make sure the loading event comes in 1 piece w/ the end of
1740             // pagination
1741             // reset
1742             advanceUntilIdle()
1743             assertThat(presenter.newCombinedLoadStates())
1744                 .containsExactly(
1745                     localLoadStatesOf(
1746                         refreshLocal = Loading,
1747                         prependLocal = NotLoading(endOfPaginationReached = false)
1748                     ),
1749                     localLoadStatesOf(
1750                         refreshLocal = NotLoading(endOfPaginationReached = false),
1751                         prependLocal = NotLoading(endOfPaginationReached = true)
1752                     ),
1753                 )
1754             assertThat(loadStateCallbacks)
1755                 .containsExactly(
1756                     localLoadStatesOf(
1757                         refreshLocal = Loading,
1758                         prependLocal = NotLoading(endOfPaginationReached = false)
1759                     ),
1760                     localLoadStatesOf(
1761                         refreshLocal = NotLoading(endOfPaginationReached = false),
1762                         prependLocal = NotLoading(endOfPaginationReached = true)
1763                     ),
1764                 )
1765             collectLoadStates.cancel()
1766         }
1767 
1768     // TODO(b/195028524) the tests from here on checks the state after Invalid/Error results.
1769     //  Upon changes due to b/195028524, the asserts on these tests should see a new resetting
1770     //  LoadStateUpdate event
1771 
1772     @Test fun appendInvalid_loadStates() = appendInvalid_loadStates(false)
1773 
1774     @Test fun appendInvalid_loadStates_collectWithCachedIn() = appendInvalid_loadStates(true)
1775 
1776     private fun appendInvalid_loadStates(collectWithCachedIn: Boolean) =
1777         runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
1778             val collectLoadStates = launch { presenter.collectLoadStates() }
1779 
1780             // initial REFRESH
1781             advanceUntilIdle()
1782 
1783             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1784             assertThat(presenter.newCombinedLoadStates())
1785                 .containsExactly(
1786                     localLoadStatesOf(refreshLocal = Loading),
1787                     localLoadStatesOf(prependLocal = NotLoading.Complete),
1788                 )
1789 
1790             // normal append
1791             presenter[8]
1792 
1793             advanceUntilIdle()
1794 
1795             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
1796             assertThat(presenter.newCombinedLoadStates())
1797                 .containsExactly(
1798                     localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
1799                     localLoadStatesOf(prependLocal = NotLoading.Complete)
1800                 )
1801 
1802             // do invalid append which will return LoadResult.Invalid
1803             presenter[11]
1804             pagingSources[0].nextLoadResult = LoadResult.Invalid()
1805 
1806             // using advanceTimeBy instead of advanceUntilIdle, otherwise this invalid APPEND +
1807             // subsequent
1808             // REFRESH will auto run consecutively and we won't be able to assert them incrementally
1809             advanceTimeBy(1001)
1810 
1811             assertThat(pagingSources.size).isEqualTo(2)
1812             assertThat(presenter.newCombinedLoadStates())
1813                 .containsExactly(
1814                     // the invalid append
1815                     localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
1816                     // REFRESH on new paging source. Append/Prepend local states is reset because
1817                     // the
1818                     // LoadStateUpdate from refresh sends the full map of a local LoadStates which
1819                     // was
1820                     // initialized as IDLE upon new Snapshot.
1821                     localLoadStatesOf(
1822                         refreshLocal = Loading,
1823                     ),
1824                 )
1825 
1826             // the LoadResult.Invalid from failed APPEND triggers new pagingSource + initial REFRESH
1827             advanceUntilIdle()
1828 
1829             assertThat(presenter.snapshot()).containsExactlyElementsIn(11 until 20)
1830             assertThat(presenter.newCombinedLoadStates())
1831                 .containsExactly(
1832                     localLoadStatesOf(),
1833                 )
1834 
1835             collectLoadStates.cancel()
1836         }
1837 
1838     @Test fun appendDrop_loadStates() = appendDrop_loadStates(false)
1839 
1840     @Test fun appendDrop_loadStates_collectWithCachedIn() = appendDrop_loadStates(true)
1841 
1842     private fun appendDrop_loadStates(collectWithCachedIn: Boolean) =
1843         runTest(
1844             collectWithCachedIn,
1845             initialKey = 96,
1846             config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1847         ) { presenter, _, _, _ ->
1848             val collectLoadStates = launch { presenter.collectLoadStates() }
1849 
1850             // initial REFRESH
1851             advanceUntilIdle()
1852 
1853             assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 99)
1854             assertThat(presenter.newCombinedLoadStates())
1855                 .containsExactly(
1856                     localLoadStatesOf(refreshLocal = Loading),
1857                     // ensure append has reached end of pagination
1858                     localLoadStatesOf(),
1859                 )
1860 
1861             // trigger append to reach max page size
1862             presenter[2]
1863             advanceUntilIdle()
1864 
1865             assertThat(presenter.snapshot()).containsExactlyElementsIn(96 until 100)
1866             assertThat(presenter.newCombinedLoadStates())
1867                 .containsExactly(
1868                     localLoadStatesOf(appendLocal = Loading),
1869                     localLoadStatesOf(appendLocal = NotLoading.Complete),
1870                 )
1871 
1872             // trigger prepend and drop from append direction
1873             presenter[0]
1874             advanceUntilIdle()
1875 
1876             assertThat(presenter.snapshot()).containsExactlyElementsIn(95 until 99)
1877             assertThat(presenter.newCombinedLoadStates())
1878                 .containsExactly(
1879                     localLoadStatesOf(prependLocal = Loading, appendLocal = NotLoading.Complete),
1880                     // page from the end is dropped so now appendLocal should be
1881                     // NotLoading.Incomplete
1882                     localLoadStatesOf(prependLocal = Loading),
1883                     localLoadStatesOf(),
1884                 )
1885             collectLoadStates.cancel()
1886         }
1887 
1888     @Test fun prependInvalid_loadStates() = prependInvalid_loadStates(false)
1889 
1890     @Test fun prependInvalid_loadStates_collectWithCachedIn() = prependInvalid_loadStates(true)
1891 
1892     private fun prependInvalid_loadStates(collectWithCachedIn: Boolean) =
1893         runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
1894             val collectLoadStates = launch { presenter.collectLoadStates() }
1895 
1896             // initial REFRESH
1897             advanceUntilIdle()
1898 
1899             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
1900             assertThat(presenter.newCombinedLoadStates())
1901                 .containsExactly(
1902                     localLoadStatesOf(refreshLocal = Loading),
1903                     // all local states NotLoading.Incomplete
1904                     localLoadStatesOf(),
1905                 )
1906 
1907             // normal prepend to ensure LoadStates for Page returns remains the same
1908             presenter[0]
1909 
1910             advanceUntilIdle()
1911 
1912             assertThat(presenter.snapshot()).containsExactlyElementsIn(47 until 59)
1913             assertThat(presenter.newCombinedLoadStates())
1914                 .containsExactly(
1915                     localLoadStatesOf(prependLocal = Loading),
1916                     // all local states NotLoading.Incomplete
1917                     localLoadStatesOf(),
1918                 )
1919 
1920             // do an invalid prepend which will return LoadResult.Invalid
1921             presenter[0]
1922             pagingSources[0].nextLoadResult = LoadResult.Invalid()
1923             advanceTimeBy(1001)
1924 
1925             assertThat(pagingSources.size).isEqualTo(2)
1926             assertThat(presenter.newCombinedLoadStates())
1927                 .containsExactly(
1928                     // the invalid prepend
1929                     localLoadStatesOf(prependLocal = Loading),
1930                     // REFRESH on new paging source. Append/Prepend local states is reset because
1931                     // the
1932                     // LoadStateUpdate from refresh sends the full map of a local LoadStates which
1933                     // was
1934                     // initialized as IDLE upon new Snapshot.
1935                     localLoadStatesOf(refreshLocal = Loading),
1936                 )
1937 
1938             // the LoadResult.Invalid from failed PREPEND triggers new pagingSource + initial
1939             // REFRESH
1940             advanceUntilIdle()
1941 
1942             // load starts from 0 again because the provided initialKey = 50 is not
1943             // multi-generational
1944             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
1945             assertThat(presenter.newCombinedLoadStates())
1946                 .containsExactly(
1947                     localLoadStatesOf(prependLocal = NotLoading.Complete),
1948                 )
1949 
1950             collectLoadStates.cancel()
1951         }
1952 
1953     @Test fun prependDrop_loadStates() = prependDrop_loadStates(false)
1954 
1955     @Test fun prependDrop_loadStates_collectWithCachedIn() = prependDrop_loadStates(true)
1956 
1957     private fun prependDrop_loadStates(collectWithCachedIn: Boolean) =
1958         runTest(
1959             collectWithCachedIn,
1960             initialKey = 1,
1961             config = PagingConfig(pageSize = 1, maxSize = 4, enablePlaceholders = false)
1962         ) { presenter, _, _, _ ->
1963             val collectLoadStates = launch { presenter.collectLoadStates() }
1964 
1965             // initial REFRESH
1966             advanceUntilIdle()
1967 
1968             assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 4)
1969             assertThat(presenter.newCombinedLoadStates())
1970                 .containsExactly(
1971                     localLoadStatesOf(refreshLocal = Loading),
1972                     // ensure append has reached end of pagination
1973                     localLoadStatesOf(),
1974                 )
1975 
1976             // trigger prepend to reach max page size
1977             presenter[0]
1978             advanceUntilIdle()
1979 
1980             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 4)
1981             assertThat(presenter.newCombinedLoadStates())
1982                 .containsExactly(
1983                     localLoadStatesOf(prependLocal = Loading),
1984                     localLoadStatesOf(prependLocal = NotLoading.Complete),
1985                 )
1986 
1987             // trigger append and drop from prepend direction
1988             presenter[3]
1989             advanceUntilIdle()
1990 
1991             assertThat(presenter.snapshot()).containsExactlyElementsIn(1 until 5)
1992             assertThat(presenter.newCombinedLoadStates())
1993                 .containsExactly(
1994                     localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
1995                     // first page is dropped so now prependLocal should be NotLoading.Incomplete
1996                     localLoadStatesOf(appendLocal = Loading),
1997                     localLoadStatesOf(),
1998                 )
1999 
2000             collectLoadStates.cancel()
2001         }
2002 
2003     @Test fun refreshInvalid_loadStates() = refreshInvalid_loadStates(false)
2004 
2005     @Test fun refreshInvalid_loadStates_collectWithCachedIn() = refreshInvalid_loadStates(true)
2006 
2007     private fun refreshInvalid_loadStates(collectWithCachedIn: Boolean) =
2008         runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
2009             val collectLoadStates = launch { presenter.collectLoadStates() }
2010 
2011             // execute queued initial REFRESH load which will return LoadResult.Invalid()
2012             pagingSources[0].nextLoadResult = LoadResult.Invalid()
2013             advanceTimeBy(1001)
2014 
2015             assertThat(presenter.snapshot()).isEmpty()
2016             assertThat(presenter.newCombinedLoadStates())
2017                 .containsExactly(
2018                     // invalid first refresh. The second refresh state update that follows is
2019                     // identical to
2020                     // this LoadStates so it gets de-duped
2021                     localLoadStatesOf(refreshLocal = Loading),
2022                 )
2023 
2024             // execute second REFRESH load
2025             advanceUntilIdle()
2026 
2027             // second refresh still loads from initialKey = 50 because anchorPosition/refreshKey is
2028             // null
2029             assertThat(pagingSources.size).isEqualTo(2)
2030             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2031             assertThat(presenter.newCombinedLoadStates())
2032                 .containsExactly(
2033                     localLoadStatesOf(
2034                         prependLocal = NotLoading.Complete,
2035                     )
2036                 )
2037 
2038             collectLoadStates.cancel()
2039         }
2040 
2041     @Test fun appendError_retryLoadStates() = appendError_retryLoadStates(false)
2042 
2043     @Test fun appendError_retryLoadStates_collectWithCachedIn() = appendError_retryLoadStates(true)
2044 
2045     private fun appendError_retryLoadStates(collectWithCachedIn: Boolean) =
2046         runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
2047             val collectLoadStates = launch { presenter.collectLoadStates() }
2048 
2049             // initial REFRESH
2050             advanceUntilIdle()
2051 
2052             assertThat(presenter.newCombinedLoadStates())
2053                 .containsExactly(
2054                     localLoadStatesOf(refreshLocal = Loading),
2055                     localLoadStatesOf(prependLocal = NotLoading.Complete),
2056                 )
2057             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2058 
2059             // append returns LoadResult.Error
2060             presenter[8]
2061             val exception = Throwable()
2062             pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2063 
2064             advanceUntilIdle()
2065 
2066             assertThat(presenter.newCombinedLoadStates())
2067                 .containsExactly(
2068                     localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
2069                     localLoadStatesOf(
2070                         prependLocal = NotLoading.Complete,
2071                         appendLocal = LoadState.Error(exception)
2072                     ),
2073                 )
2074             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2075 
2076             // retry append
2077             presenter.retry()
2078             advanceUntilIdle()
2079 
2080             // make sure append success
2081             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 12)
2082             // no reset
2083             assertThat(presenter.newCombinedLoadStates())
2084                 .containsExactly(
2085                     localLoadStatesOf(prependLocal = NotLoading.Complete, appendLocal = Loading),
2086                     localLoadStatesOf(prependLocal = NotLoading.Complete),
2087                 )
2088 
2089             collectLoadStates.cancel()
2090         }
2091 
2092     @Test fun prependError_retryLoadStates() = prependError_retryLoadStates(false)
2093 
2094     @Test
2095     fun prependError_retryLoadStates_collectWithCachedIn() = prependError_retryLoadStates(true)
2096 
2097     private fun prependError_retryLoadStates(collectWithCachedIn: Boolean) =
2098         runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
2099             val collectLoadStates = launch { presenter.collectLoadStates() }
2100 
2101             // initial REFRESH
2102             advanceUntilIdle()
2103 
2104             assertThat(presenter.newCombinedLoadStates())
2105                 .containsExactly(
2106                     localLoadStatesOf(refreshLocal = Loading),
2107                     localLoadStatesOf(),
2108                 )
2109 
2110             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
2111 
2112             // prepend returns LoadResult.Error
2113             presenter[0]
2114             val exception = Throwable()
2115             pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2116 
2117             advanceUntilIdle()
2118 
2119             assertThat(presenter.newCombinedLoadStates())
2120                 .containsExactly(
2121                     localLoadStatesOf(prependLocal = Loading),
2122                     localLoadStatesOf(prependLocal = LoadState.Error(exception)),
2123                 )
2124             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
2125 
2126             // retry prepend
2127             presenter.retry()
2128 
2129             advanceUntilIdle()
2130 
2131             // make sure prepend success
2132             assertThat(presenter.snapshot()).containsExactlyElementsIn(47 until 59)
2133             assertThat(presenter.newCombinedLoadStates())
2134                 .containsExactly(
2135                     localLoadStatesOf(prependLocal = Loading),
2136                     localLoadStatesOf(),
2137                 )
2138 
2139             collectLoadStates.cancel()
2140         }
2141 
2142     @Test fun refreshError_retryLoadStates() = refreshError_retryLoadStates(false)
2143 
2144     @Test
2145     fun refreshError_retryLoadStates_collectWithCachedIn() = refreshError_retryLoadStates(true)
2146 
2147     private fun refreshError_retryLoadStates(collectWithCachedIn: Boolean) =
2148         runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
2149             val collectLoadStates = launch { presenter.collectLoadStates() }
2150 
2151             // initial load returns LoadResult.Error
2152             val exception = Throwable()
2153             pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2154 
2155             advanceUntilIdle()
2156 
2157             assertThat(presenter.newCombinedLoadStates())
2158                 .containsExactly(
2159                     localLoadStatesOf(refreshLocal = Loading),
2160                     localLoadStatesOf(refreshLocal = LoadState.Error(exception)),
2161                 )
2162             assertThat(presenter.snapshot()).isEmpty()
2163 
2164             // retry refresh
2165             presenter.retry()
2166 
2167             advanceUntilIdle()
2168 
2169             // refresh retry does not trigger new gen
2170             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2171             // Goes directly from Error --> Loading without resetting refresh to NotLoading
2172             assertThat(presenter.newCombinedLoadStates())
2173                 .containsExactly(
2174                     localLoadStatesOf(refreshLocal = Loading),
2175                     localLoadStatesOf(prependLocal = NotLoading.Complete),
2176                 )
2177 
2178             collectLoadStates.cancel()
2179         }
2180 
2181     @Test fun prependError_refreshLoadStates() = prependError_refreshLoadStates(false)
2182 
2183     @Test
2184     fun prependError_refreshLoadStates_collectWithCachedIn() = prependError_refreshLoadStates(true)
2185 
2186     private fun prependError_refreshLoadStates(collectWithCachedIn: Boolean) =
2187         runTest(collectWithCachedIn, initialKey = 50) { presenter, pagingSources, _, _ ->
2188             val collectLoadStates = launch { presenter.collectLoadStates() }
2189 
2190             // initial REFRESH
2191             advanceUntilIdle()
2192 
2193             assertThat(presenter.newCombinedLoadStates())
2194                 .containsExactly(
2195                     localLoadStatesOf(refreshLocal = Loading),
2196                     localLoadStatesOf(),
2197                 )
2198             assertThat(presenter.size).isEqualTo(9)
2199             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
2200 
2201             // prepend returns LoadResult.Error
2202             presenter[0]
2203             val exception = Throwable()
2204             pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2205 
2206             advanceUntilIdle()
2207 
2208             assertThat(presenter.newCombinedLoadStates())
2209                 .containsExactly(
2210                     localLoadStatesOf(prependLocal = Loading),
2211                     localLoadStatesOf(prependLocal = LoadState.Error(exception)),
2212                 )
2213 
2214             // refresh() should reset local LoadStates and trigger new REFRESH
2215             presenter.refresh()
2216             advanceUntilIdle()
2217 
2218             // Initial load starts from 0 because initialKey is single gen.
2219             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2220             assertThat(presenter.newCombinedLoadStates())
2221                 .containsExactly(
2222                     // second gen REFRESH load. The Error prepend state was automatically reset to
2223                     // NotLoading.
2224                     localLoadStatesOf(refreshLocal = Loading),
2225                     // REFRESH complete
2226                     localLoadStatesOf(prependLocal = NotLoading.Complete),
2227                 )
2228 
2229             collectLoadStates.cancel()
2230         }
2231 
2232     @Test fun refreshError_refreshLoadStates() = refreshError_refreshLoadStates(false)
2233 
2234     @Test
2235     fun refreshError_refreshLoadStates_collectWithCachedIn() = refreshError_refreshLoadStates(true)
2236 
2237     private fun refreshError_refreshLoadStates(collectWithCachedIn: Boolean) =
2238         runTest(collectWithCachedIn) { presenter, pagingSources, _, _ ->
2239             val collectLoadStates = launch { presenter.collectLoadStates() }
2240 
2241             // the initial load will return LoadResult.Error
2242             val exception = Throwable()
2243             pagingSources[0].nextLoadResult = LoadResult.Error(exception)
2244 
2245             advanceUntilIdle()
2246 
2247             assertThat(presenter.newCombinedLoadStates())
2248                 .containsExactly(
2249                     localLoadStatesOf(refreshLocal = Loading),
2250                     localLoadStatesOf(refreshLocal = LoadState.Error(exception)),
2251                 )
2252             assertThat(presenter.snapshot()).isEmpty()
2253 
2254             // refresh should trigger new generation
2255             presenter.refresh()
2256 
2257             advanceUntilIdle()
2258 
2259             assertThat(presenter.snapshot()).containsExactlyElementsIn(0 until 9)
2260             // Goes directly from Error --> Loading without resetting refresh to NotLoading
2261             assertThat(presenter.newCombinedLoadStates())
2262                 .containsExactly(
2263                     localLoadStatesOf(refreshLocal = Loading),
2264                     localLoadStatesOf(prependLocal = NotLoading.Complete),
2265                 )
2266 
2267             collectLoadStates.cancel()
2268         }
2269 
2270     @Test
2271     fun remoteRefresh_refreshStatePersists() =
2272         testScope.runTest {
2273             val presenter = SimplePresenter()
2274             val remoteMediator =
2275                 RemoteMediatorMock(loadDelay = 1500).apply {
2276                     initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
2277                 }
2278             val pager =
2279                 Pager(
2280                     PagingConfig(pageSize = 3, enablePlaceholders = false),
2281                     remoteMediator = remoteMediator,
2282                 ) {
2283                     TestPagingSource(loadDelay = 500, items = emptyList())
2284                 }
2285 
2286             val collectLoadStates = launch { presenter.collectLoadStates() }
2287             val job = launch { pager.flow.collectLatest { presenter.collectFrom(it) } }
2288             // allow local refresh to complete but not remote refresh
2289             advanceTimeBy(600)
2290 
2291             assertThat(presenter.newCombinedLoadStates())
2292                 .containsExactly(
2293                     // local starts loading
2294                     remoteLoadStatesOf(
2295                         refreshLocal = Loading,
2296                     ),
2297                     // remote starts loading
2298                     remoteLoadStatesOf(
2299                         refresh = Loading,
2300                         refreshLocal = Loading,
2301                         refreshRemote = Loading,
2302                     ),
2303                     // local load returns with empty data, mediator is still loading
2304                     remoteLoadStatesOf(
2305                         refresh = Loading,
2306                         prependLocal = NotLoading.Complete,
2307                         appendLocal = NotLoading.Complete,
2308                         refreshRemote = Loading,
2309                     ),
2310                 )
2311 
2312             // refresh triggers new generation & LoadState reset
2313             presenter.refresh()
2314 
2315             // allow local refresh to complete but not remote refresh
2316             advanceTimeBy(600)
2317 
2318             assertThat(presenter.newCombinedLoadStates())
2319                 .containsExactly(
2320                     // local starts second refresh while mediator continues remote refresh from
2321                     // before
2322                     remoteLoadStatesOf(
2323                         refresh = Loading,
2324                         refreshLocal = Loading,
2325                         refreshRemote = Loading,
2326                     ),
2327                     // local load returns empty data
2328                     remoteLoadStatesOf(
2329                         refresh = Loading,
2330                         prependLocal = NotLoading.Complete,
2331                         appendLocal = NotLoading.Complete,
2332                         refreshRemote = Loading,
2333                     ),
2334                 )
2335 
2336             // allow remote refresh to complete
2337             advanceTimeBy(600)
2338 
2339             assertThat(presenter.newCombinedLoadStates())
2340                 .containsExactly(
2341                     // remote refresh returns empty and triggers remote append/prepend
2342                     remoteLoadStatesOf(
2343                         prepend = Loading,
2344                         append = Loading,
2345                         prependLocal = NotLoading.Complete,
2346                         appendLocal = NotLoading.Complete,
2347                         prependRemote = Loading,
2348                         appendRemote = Loading,
2349                     ),
2350                 )
2351 
2352             // allow remote append and prepend to complete
2353             advanceUntilIdle()
2354 
2355             assertThat(presenter.newCombinedLoadStates())
2356                 .containsExactly(
2357                     // prepend completes first
2358                     remoteLoadStatesOf(
2359                         append = Loading,
2360                         prependLocal = NotLoading.Complete,
2361                         appendLocal = NotLoading.Complete,
2362                         appendRemote = Loading,
2363                     ),
2364                     remoteLoadStatesOf(
2365                         prependLocal = NotLoading.Complete,
2366                         appendLocal = NotLoading.Complete,
2367                     ),
2368                 )
2369 
2370             job.cancel()
2371             collectLoadStates.cancel()
2372         }
2373 
2374     @Test
2375     fun recollectOnNewPresenter_initialLoadStates() =
2376         testScope.runTest {
2377             val pager =
2378                 Pager(
2379                         config = PagingConfig(pageSize = 3, enablePlaceholders = false),
2380                         initialKey = 50,
2381                         pagingSourceFactory = { TestPagingSource() }
2382                     )
2383                     .flow
2384                     .cachedIn(this)
2385 
2386             val presenter = SimplePresenter()
2387             backgroundScope.launch { presenter.collectLoadStates() }
2388 
2389             val job = launch { pager.collectLatest { presenter.collectFrom(it) } }
2390             advanceUntilIdle()
2391 
2392             assertThat(presenter.newCombinedLoadStates())
2393                 .containsExactly(localLoadStatesOf(refreshLocal = Loading), localLoadStatesOf())
2394 
2395             // we start a separate presenter to recollect on cached Pager.flow
2396             val presenter2 = SimplePresenter()
2397             backgroundScope.launch { presenter2.collectLoadStates() }
2398 
2399             val job2 = launch { pager.collectLatest { presenter2.collectFrom(it) } }
2400             advanceUntilIdle()
2401 
2402             assertThat(presenter2.newCombinedLoadStates()).containsExactly(localLoadStatesOf())
2403 
2404             job.cancel()
2405             job2.cancel()
2406             coroutineContext.cancelChildren()
2407         }
2408 
2409     @Test
2410     fun cachedData() {
2411         val data = List(50) { it }
2412         val cachedPagingData = createCachedPagingData(data)
2413         val simplePresenter = SimplePresenter(cachedPagingData)
2414         assertThat(simplePresenter.snapshot()).isEqualTo(data)
2415         assertThat(simplePresenter.size).isEqualTo(data.size)
2416     }
2417 
2418     @Test
2419     fun emptyCachedData() {
2420         val cachedPagingData = createCachedPagingData(emptyList())
2421         val simplePresenter = SimplePresenter(cachedPagingData)
2422         assertThat(simplePresenter.snapshot()).isEmpty()
2423         assertThat(simplePresenter.size).isEqualTo(0)
2424     }
2425 
2426     @Test
2427     fun cachedLoadStates() {
2428         val data = List(50) { it }
2429         val localStates = loadStates(refresh = Loading)
2430         val mediatorStates = loadStates()
2431         val cachedPagingData =
2432             createCachedPagingData(
2433                 data = data,
2434                 sourceLoadStates = localStates,
2435                 mediatorLoadStates = mediatorStates
2436             )
2437         val simplePresenter = SimplePresenter(cachedPagingData)
2438         val expected = simplePresenter.loadStateFlow.value
2439         assertThat(expected).isNotNull()
2440         assertThat(expected!!.source).isEqualTo(localStates)
2441         assertThat(expected.mediator).isEqualTo(mediatorStates)
2442     }
2443 
2444     @Test
2445     fun cachedData_doesNotSetHintReceiver() =
2446         testScope.runTest {
2447             val data = List(50) { it }
2448             val hintReceiver = HintReceiverFake()
2449             val cachedPagingData =
2450                 createCachedPagingData(
2451                     data = data,
2452                     sourceLoadStates = loadStates(refresh = Loading),
2453                     mediatorLoadStates = null,
2454                     hintReceiver = hintReceiver
2455                 )
2456             val presenter = SimplePresenter(cachedPagingData)
2457 
2458             // access item
2459             presenter[5]
2460             assertThat(hintReceiver.hints).hasSize(0)
2461 
2462             val flow =
2463                 flowOf(
2464                     localRefresh(pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4)))),
2465                 )
2466             val hintReceiver2 = HintReceiverFake()
2467 
2468             val job1 = launch {
2469                 presenter.collectFrom(PagingData(flow, dummyUiReceiver, hintReceiver2))
2470             }
2471 
2472             // access item, hint should be sent to the first uncached PagingData
2473             presenter[3]
2474             assertThat(hintReceiver.hints).hasSize(0)
2475             assertThat(hintReceiver2.hints).hasSize(1)
2476             job1.cancel()
2477         }
2478 
2479     @Test
2480     fun cachedData_doesNotSetUiReceiver() =
2481         testScope.runTest {
2482             val data = List(50) { it }
2483             val uiReceiver = UiReceiverFake()
2484             val cachedPagingData =
2485                 createCachedPagingData(
2486                     data = data,
2487                     sourceLoadStates = loadStates(refresh = Loading),
2488                     mediatorLoadStates = null,
2489                     uiReceiver = uiReceiver
2490                 )
2491             val presenter = SimplePresenter(cachedPagingData)
2492             presenter.refresh()
2493             advanceUntilIdle()
2494             assertThat(uiReceiver.refreshEvents).hasSize(0)
2495 
2496             val flow =
2497                 flowOf(
2498                     localRefresh(pages = listOf(TransformablePage(listOf(0, 1, 2, 3, 4)))),
2499                 )
2500             val uiReceiver2 = UiReceiverFake()
2501             val job1 = launch {
2502                 presenter.collectFrom(PagingData(flow, uiReceiver2, dummyHintReceiver))
2503             }
2504             assertThat(uiReceiver.refreshEvents).hasSize(0)
2505             assertThat(uiReceiver2.refreshEvents).hasSize(1)
2506             job1.cancel()
2507         }
2508 
2509     @Test
2510     fun cachedData_thenRealData() =
2511         testScope.runTest {
2512             val data = List(2) { it }
2513             val cachedPagingData =
2514                 createCachedPagingData(
2515                     data = data,
2516                     sourceLoadStates = loadStates(refresh = Loading),
2517                     mediatorLoadStates = null,
2518                 )
2519             val presenter = SimplePresenter(cachedPagingData)
2520             val data2 = List(10) { it }
2521             val flow =
2522                 flowOf(
2523                     localRefresh(pages = listOf(TransformablePage(data2))),
2524                 )
2525             val job1 = launch {
2526                 presenter.collectFrom(PagingData(flow, dummyUiReceiver, dummyHintReceiver))
2527             }
2528 
2529             assertThat(presenter.snapshot()).isEqualTo(data2)
2530             job1.cancel()
2531         }
2532 
2533     @Test
2534     fun cachedData_thenLoadError() =
2535         testScope.runTest {
2536             val data = List(3) { it }
2537             val cachedPagingData =
2538                 createCachedPagingData(
2539                     data = data,
2540                     sourceLoadStates = loadStates(refresh = Loading),
2541                     mediatorLoadStates = null,
2542                 )
2543             val presenter = SimplePresenter(cachedPagingData)
2544 
2545             val channel = Channel<PageEvent<Int>>(Channel.UNLIMITED)
2546             val hintReceiver = HintReceiverFake()
2547             val uiReceiver = UiReceiverFake()
2548             val job1 = launch {
2549                 presenter.collectFrom(PagingData(channel.consumeAsFlow(), uiReceiver, hintReceiver))
2550             }
2551             val error = LoadState.Error(Exception())
2552             channel.trySend(localLoadStateUpdate(refreshLocal = error))
2553             assertThat(presenter.nonNullLoadStateFlow.first())
2554                 .isEqualTo(localLoadStatesOf(refreshLocal = error))
2555 
2556             // ui receiver is set upon processing a LoadStateUpdate so we can still trigger
2557             // refresh/retry
2558             presenter.refresh()
2559             assertThat(uiReceiver.refreshEvents).hasSize(1)
2560             // but hint receiver is only set if presenter has presented a refresh from this
2561             // PagingData
2562             // which did not happen in this case
2563             presenter[2]
2564             assertThat(hintReceiver.hints).hasSize(0)
2565             job1.cancel()
2566         }
2567 
2568     private fun runTest(
2569         collectWithCachedIn: Boolean,
2570         initialKey: Int? = null,
2571         config: PagingConfig = PagingConfig(pageSize = 3, enablePlaceholders = false),
2572         block:
2573             TestScope.(
2574                 presenter: SimplePresenter,
2575                 pagingSources: List<TestPagingSource>,
2576                 uiReceivers: List<TrackableUiReceiverWrapper>,
2577                 hintReceivers: List<TrackableHintReceiverWrapper>
2578             ) -> Unit
2579     ) =
2580         testScope.runTest {
2581             val pagingSources = mutableListOf<TestPagingSource>()
2582             val pager =
2583                 Pager(
2584                     config = config,
2585                     initialKey = initialKey,
2586                     pagingSourceFactory = {
2587                         TestPagingSource(
2588                                 loadDelay = 1000,
2589                             )
2590                             .also { pagingSources.add(it) }
2591                     }
2592                 )
2593             val presenter = SimplePresenter()
2594             val uiReceivers = mutableListOf<TrackableUiReceiverWrapper>()
2595             val hintReceivers = mutableListOf<TrackableHintReceiverWrapper>()
2596 
2597             val collection = launch {
2598                 pager.flow
2599                     .map { pagingData ->
2600                         PagingData(
2601                             flow = pagingData.flow,
2602                             uiReceiver =
2603                                 TrackableUiReceiverWrapper(pagingData.uiReceiver).also {
2604                                     uiReceivers.add(it)
2605                                 },
2606                             hintReceiver =
2607                                 TrackableHintReceiverWrapper(pagingData.hintReceiver).also {
2608                                     hintReceivers.add(it)
2609                                 }
2610                         )
2611                     }
2612                     .let {
2613                         if (collectWithCachedIn) {
2614                             it.cachedIn(this)
2615                         } else {
2616                             it
2617                         }
2618                     }
2619                     .collect { presenter.collectFrom(it) }
2620             }
2621 
2622             try {
2623                 block(presenter, pagingSources, uiReceivers, hintReceivers)
2624             } finally {
2625                 collection.cancel()
2626             }
2627         }
2628 }
2629 
infinitelySuspendingPagingDatanull2630 private fun infinitelySuspendingPagingData(
2631     uiReceiver: UiReceiver = dummyUiReceiver,
2632     hintReceiver: HintReceiver = dummyHintReceiver
2633 ) =
2634     PagingData(
2635         flow { emit(suspendCancellableCoroutine<PageEvent<Int>> {}) },
2636         uiReceiver,
2637         hintReceiver
2638     )
2639 
createCachedPagingDatanull2640 private fun createCachedPagingData(
2641     data: List<Int>,
2642     placeholdersBefore: Int = 0,
2643     placeholdersAfter: Int = 0,
2644     uiReceiver: UiReceiver = PagingData.NOOP_UI_RECEIVER,
2645     hintReceiver: HintReceiver = PagingData.NOOP_HINT_RECEIVER,
2646     sourceLoadStates: LoadStates = LoadStates.IDLE,
2647     mediatorLoadStates: LoadStates? = null,
2648 ): PagingData<Int> =
2649     PagingData(
2650         flow = emptyFlow(),
2651         uiReceiver = uiReceiver,
2652         hintReceiver = hintReceiver,
2653         cachedPageEvent = {
2654             PageEvent.Insert.Refresh(
2655                 pages = listOf(TransformablePage(0, data)),
2656                 placeholdersBefore = placeholdersBefore,
2657                 placeholdersAfter = placeholdersAfter,
2658                 sourceLoadStates = sourceLoadStates,
2659                 mediatorLoadStates = mediatorLoadStates
2660             )
2661         }
2662     )
2663 
2664 private class UiReceiverFake : UiReceiver {
2665     val retryEvents = mutableListOf<Unit>()
2666     val refreshEvents = mutableListOf<Unit>()
2667 
retrynull2668     override fun retry() {
2669         retryEvents.add(Unit)
2670     }
2671 
refreshnull2672     override fun refresh() {
2673         refreshEvents.add(Unit)
2674     }
2675 }
2676 
2677 private class HintReceiverFake : HintReceiver {
2678     private val _hints = mutableListOf<ViewportHint>()
2679     val hints: List<ViewportHint>
2680         get() {
2681             val result = _hints.toList()
<lambda>null2682             @OptIn(ExperimentalStdlibApi::class) repeat(result.size) { _hints.removeFirst() }
2683             return result
2684         }
2685 
accessHintnull2686     override fun accessHint(viewportHint: ViewportHint) {
2687         _hints.add(viewportHint)
2688     }
2689 }
2690 
2691 private class TrackableUiReceiverWrapper(
2692     private val receiver: UiReceiver? = null,
2693 ) : UiReceiver {
2694     val retryEvents = mutableListOf<Unit>()
2695     val refreshEvents = mutableListOf<Unit>()
2696 
retrynull2697     override fun retry() {
2698         retryEvents.add(Unit)
2699         receiver?.retry()
2700     }
2701 
refreshnull2702     override fun refresh() {
2703         refreshEvents.add(Unit)
2704         receiver?.refresh()
2705     }
2706 }
2707 
2708 private class TrackableHintReceiverWrapper(
2709     private val receiver: HintReceiver? = null,
2710 ) : HintReceiver {
2711     private val _hints = mutableListOf<ViewportHint>()
2712     val hints: List<ViewportHint>
2713         get() {
2714             val result = _hints.toList()
<lambda>null2715             @OptIn(ExperimentalStdlibApi::class) repeat(result.size) { _hints.removeFirst() }
2716             return result
2717         }
2718 
accessHintnull2719     override fun accessHint(viewportHint: ViewportHint) {
2720         _hints.add(viewportHint)
2721         receiver?.accessHint(viewportHint)
2722     }
2723 }
2724 
2725 private class SimplePresenter(
2726     cachedPagingData: PagingData<Int>? = null,
2727 ) :
2728     PagingDataPresenter<Int>(
2729         mainContext = EmptyCoroutineContext,
2730         cachedPagingData = cachedPagingData
2731     ) {
2732     private val _localLoadStates = mutableListOf<CombinedLoadStates>()
2733 
2734     val nonNullLoadStateFlow = loadStateFlow.filterNotNull()
2735 
newCombinedLoadStatesnull2736     fun newCombinedLoadStates(): List<CombinedLoadStates?> {
2737         val newCombinedLoadStates = _localLoadStates.toList()
2738         _localLoadStates.clear()
2739         return newCombinedLoadStates
2740     }
2741 
collectLoadStatesnull2742     suspend fun collectLoadStates() {
2743         nonNullLoadStateFlow.collect { combinedLoadStates ->
2744             _localLoadStates.add(combinedLoadStates)
2745         }
2746     }
2747 
2748     private val _pagingDataEvents = mutableListOf<PagingDataEvent<Int>>()
2749 
newEventsnull2750     fun newEvents(): List<PagingDataEvent<Int>> {
2751         val newEvents = _pagingDataEvents.toList()
2752         _pagingDataEvents.clear()
2753         return newEvents
2754     }
2755 
presentPagingDataEventnull2756     override suspend fun presentPagingDataEvent(event: PagingDataEvent<Int>) {
2757         _pagingDataEvents.add(event)
2758     }
2759 }
2760 
2761 internal val dummyUiReceiver =
2762     object : UiReceiver {
retrynull2763         override fun retry() {}
2764 
refreshnull2765         override fun refresh() {}
2766     }
2767 
2768 internal val dummyHintReceiver =
2769     object : HintReceiver {
accessHintnull2770         override fun accessHint(viewportHint: ViewportHint) {}
2771     }
2772