1 /*
2  * Copyright 2022 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.testing
18 
19 import androidx.kruth.assertThat
20 import androidx.paging.PagingConfig
21 import androidx.paging.PagingSource.LoadResult
22 import androidx.paging.PagingState
23 import androidx.paging.TestPagingSource
24 import kotlin.test.Test
25 import kotlin.test.assertFailsWith
26 import kotlin.test.assertTrue
27 import kotlinx.coroutines.ExperimentalCoroutinesApi
28 import kotlinx.coroutines.delay
29 import kotlinx.coroutines.launch
30 import kotlinx.coroutines.test.advanceUntilIdle
31 import kotlinx.coroutines.test.runTest
32 
33 @OptIn(ExperimentalCoroutinesApi::class)
34 class TestPagerTest {
35 
36     @Test
refresh_nullKeynull37     fun refresh_nullKey() {
38         val source = TestPagingSource()
39         val pager = TestPager(CONFIG, source)
40 
41         runTest {
42             val result = pager.refresh(null) as LoadResult.Page
43 
44             assertThat(result.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
45         }
46     }
47 
48     @Test
refresh_withInitialKeynull49     fun refresh_withInitialKey() {
50         val source = TestPagingSource()
51         val pager = TestPager(CONFIG, source)
52 
53         runTest {
54             val result = pager.refresh(50) as LoadResult.Page
55 
56             assertThat(result.data).containsExactlyElementsIn(listOf(50, 51, 52, 53, 54)).inOrder()
57         }
58     }
59 
60     @Test
refresh_returnErrornull61     fun refresh_returnError() {
62         val source = TestPagingSource()
63         val pager = TestPager(CONFIG, source)
64 
65         runTest {
66             source.errorNextLoad = true
67             val result = pager.refresh()
68             assertTrue(result is LoadResult.Error)
69 
70             val page = pager.getLastLoadedPage()
71             assertThat(page).isNull()
72         }
73     }
74 
75     @Test
refresh_returnInvalidnull76     fun refresh_returnInvalid() {
77         val source = TestPagingSource()
78         val pager = TestPager(CONFIG, source)
79 
80         runTest {
81             source.nextLoadResult = LoadResult.Invalid()
82             val result = pager.refresh()
83             assertTrue(result is LoadResult.Invalid)
84 
85             val page = pager.getLastLoadedPage()
86             assertThat(page).isNull()
87         }
88     }
89 
90     @Test
refresh_invalidPagingSourcenull91     fun refresh_invalidPagingSource() {
92         val source = TestPagingSource()
93         val pager = TestPager(CONFIG, source)
94 
95         runTest {
96             source.invalidate()
97             assertTrue(source.invalid)
98             // simulate a PagingSource that returns LoadResult.Invalid when it's invalidated
99             source.nextLoadResult = LoadResult.Invalid()
100 
101             assertThat(pager.refresh()).isInstanceOf<LoadResult.Invalid<Int, Int>>()
102         }
103     }
104 
105     @Test
refresh_getLastLoadedPagenull106     fun refresh_getLastLoadedPage() {
107         val source = TestPagingSource()
108         val pager = TestPager(CONFIG, source)
109 
110         runTest {
111             val page: LoadResult.Page<Int, Int>? =
112                 pager.run {
113                     refresh()
114                     getLastLoadedPage()
115                 }
116             assertThat(page).isNotNull()
117             assertThat(page?.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
118         }
119     }
120 
121     @Test
getLastLoadedPage_afterInvalidPagingSourcenull122     fun getLastLoadedPage_afterInvalidPagingSource() {
123         val source = TestPagingSource()
124         val pager = TestPager(CONFIG, source)
125 
126         runTest {
127             val page =
128                 pager.run {
129                     refresh()
130                     append() // page should be this appended page
131                     source.invalidate()
132                     assertTrue(source.invalid)
133                     getLastLoadedPage()
134                 }
135             assertThat(page).isNotNull()
136             assertThat(page?.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
137         }
138     }
139 
140     @Test
refresh_getPagesnull141     fun refresh_getPages() {
142         val source = TestPagingSource()
143         val pager = TestPager(CONFIG, source)
144 
145         runTest {
146             val pages =
147                 pager.run {
148                     refresh()
149                     getPages()
150                 }
151             assertThat(pages).hasSize(1)
152             assertThat(pages)
153                 .containsExactlyElementsIn(listOf(listOf(0, 1, 2, 3, 4).asPage()))
154                 .inOrder()
155         }
156     }
157 
158     @Test
<lambda>null159     fun getPages_multiplePages() = runTest {
160         val source = TestPagingSource()
161         val pager = TestPager(CONFIG, source)
162 
163         pager.run {
164             refresh(20)
165             prepend()
166         }
167         assertThat(pager.getPages())
168             .containsExactlyElementsIn(
169                 listOf(
170                     // prepend
171                     listOf(17, 18, 19).asPage(),
172                     // refresh
173                     listOf(20, 21, 22, 23, 24).asPage(),
174                 )
175             )
176             .inOrder()
177     }
178 
179     @Test
<lambda>null180     fun getPages_fromEmptyList() = runTest {
181         val source = TestPagingSource()
182         val pager = TestPager(CONFIG, source)
183         val pages = pager.getPages()
184         assertThat(pages).isEmpty()
185     }
186 
187     @Test
getPages_afterInvalidPagingSourcenull188     fun getPages_afterInvalidPagingSource() {
189         val source = TestPagingSource()
190         val pager = TestPager(CONFIG, source)
191 
192         runTest {
193             val pages =
194                 pager.run {
195                     refresh()
196                     append()
197                     source.invalidate()
198                     assertTrue(source.invalid)
199                     getPages()
200                 }
201             assertThat(pages)
202                 .containsExactlyElementsIn(
203                     listOf(listOf(0, 1, 2, 3, 4).asPage(), listOf(5, 6, 7).asPage())
204                 )
205                 .inOrder()
206         }
207     }
208 
209     @Test
<lambda>null210     fun getPages_multiThread() = runTest {
211         val source = TestPagingSource()
212         val pager = TestPager(CONFIG, source)
213 
214         var pages: List<LoadResult.Page<Int, Int>>? = null
215         val job = launch {
216             pager.run {
217                 refresh(20) // first
218                 pages = getPages() // third
219                 prepend() // fifth
220             }
221         }
222         job.start()
223         assertTrue(job.isActive)
224         val pages2 =
225             pager.run {
226                 delay(200) // let launch start first
227                 append() // second
228                 prepend() // fourth
229                 getPages() // sixth
230             }
231 
232         advanceUntilIdle()
233         assertThat(pages)
234             .containsExactlyElementsIn(
235                 listOf(
236                     // should contain first and second load
237                     listOf(20, 21, 22, 23, 24).asPage(), // refresh
238                     listOf(25, 26, 27).asPage(), // append
239                 )
240             )
241             .inOrder()
242         assertThat(pages2)
243             .containsExactlyElementsIn(
244                 // should contain all loads
245                 listOf(
246                     listOf(14, 15, 16).asPage(),
247                     listOf(17, 18, 19).asPage(),
248                     listOf(20, 21, 22, 23, 24).asPage(),
249                     listOf(25, 26, 27).asPage(),
250                 )
251             )
252             .inOrder()
253     }
254 
255     @Test
multipleRefresh_onSinglePager_throwsnull256     fun multipleRefresh_onSinglePager_throws() {
257         val source = TestPagingSource()
258         val pager = TestPager(CONFIG, source)
259 
260         runTest {
261             pager.run {
262                 // second refresh should throw since testPager is not mult-generational
263                 assertFailsWith<IllegalStateException> {
264                     refresh()
265                     refresh()
266                 }
267             }
268             assertTrue(source.invalid)
269             // the first refresh should still have succeeded
270             assertThat(pager.getPages()).hasSize(1)
271         }
272     }
273 
274     @Test
multipleRefresh_onMultiplePagersnull275     fun multipleRefresh_onMultiplePagers() = runTest {
276         val source1 = TestPagingSource()
277         val pager1 = TestPager(CONFIG, source1)
278 
279         // first gen
280         val result1 = pager1.run { refresh() } as LoadResult.Page
281 
282         assertThat(result1.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
283 
284         // second gen
285         val source2 = TestPagingSource()
286         val pager2 = TestPager(CONFIG, source2)
287 
288         val result2 = pager2.run { refresh() } as LoadResult.Page
289 
290         assertThat(result2.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
291     }
292 
293     @Test
<lambda>null294     fun simpleAppend() = runTest {
295         val source = TestPagingSource()
296         val pager = TestPager(CONFIG, source)
297 
298         val result =
299             pager.run {
300                 refresh(null)
301                 append()
302             } as LoadResult.Page
303 
304         assertThat(result.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
305         assertThat(pager.getPages())
306             .containsExactlyElementsIn(
307                 listOf(listOf(0, 1, 2, 3, 4).asPage(), listOf(5, 6, 7).asPage())
308             )
309             .inOrder()
310     }
311 
312     @Test
<lambda>null313     fun simplePrepend() = runTest {
314         val source = TestPagingSource()
315         val pager = TestPager(CONFIG, source)
316 
317         val result =
318             pager.run {
319                 refresh(30)
320                 prepend()
321             } as LoadResult.Page
322 
323         assertThat(result.data).containsExactlyElementsIn(listOf(27, 28, 29)).inOrder()
324         // prepended pages should be inserted before refresh
325         assertThat(pager.getPages())
326             .containsExactlyElementsIn(
327                 listOf(
328                     // prepend
329                     listOf(27, 28, 29).asPage(),
330                     // refresh
331                     listOf(30, 31, 32, 33, 34).asPage()
332                 )
333             )
334             .inOrder()
335     }
336 
337     @Test
<lambda>null338     fun append_beforeRefresh_throws() = runTest {
339         val source = TestPagingSource()
340         val pager = TestPager(CONFIG, source)
341         assertFailsWith<IllegalStateException> { pager.append() }
342     }
343 
344     @Test
prepend_beforeRefresh_throwsnull345     fun prepend_beforeRefresh_throws() = runTest {
346         val source = TestPagingSource()
347         val pager = TestPager(CONFIG, source)
348         assertFailsWith<IllegalStateException> { pager.prepend() }
349     }
350 
351     @Test
<lambda>null352     fun append_invalidPagingSource() = runTest {
353         val source = TestPagingSource()
354         val pager = TestPager(CONFIG, source)
355 
356         val result =
357             pager.run {
358                 refresh()
359                 source.invalidate()
360                 assertThat(source.invalid).isTrue()
361                 // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
362                 source.nextLoadResult = LoadResult.Invalid()
363                 append()
364             }
365         assertThat(result).isInstanceOf<LoadResult.Invalid<Int, Int>>()
366     }
367 
368     @Test
<lambda>null369     fun prepend_invalidPagingSource() = runTest {
370         val source = TestPagingSource()
371         val pager = TestPager(CONFIG, source)
372 
373         val result =
374             pager.run {
375                 refresh(initialKey = 20)
376                 source.invalidate()
377                 assertThat(source.invalid).isTrue()
378                 // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
379                 source.nextLoadResult = LoadResult.Invalid()
380                 prepend()
381             }
382         assertThat(result).isInstanceOf<LoadResult.Invalid<Int, Int>>()
383     }
384 
385     @Test
<lambda>null386     fun consecutive_append() = runTest {
387         val source = TestPagingSource()
388         val pager = TestPager(CONFIG, source)
389 
390         pager.run {
391             refresh(20)
392             append()
393             append()
394         } as LoadResult.Page
395 
396         assertThat(pager.getPages())
397             .containsExactlyElementsIn(
398                 listOf(
399                     listOf(20, 21, 22, 23, 24).asPage(),
400                     listOf(25, 26, 27).asPage(),
401                     listOf(28, 29, 30).asPage()
402                 )
403             )
404             .inOrder()
405     }
406 
407     @Test
<lambda>null408     fun consecutive_prepend() = runTest {
409         val source = TestPagingSource()
410         val pager = TestPager(CONFIG, source)
411 
412         pager.run {
413             refresh(20)
414             prepend()
415             prepend()
416         } as LoadResult.Page
417 
418         // prepended pages should be ordered before the refresh
419         assertThat(pager.getPages())
420             .containsExactlyElementsIn(
421                 listOf(
422                     // 2nd prepend
423                     listOf(14, 15, 16).asPage(),
424                     // 1st prepend
425                     listOf(17, 18, 19).asPage(),
426                     // refresh
427                     listOf(20, 21, 22, 23, 24).asPage(),
428                 )
429             )
430             .inOrder()
431     }
432 
433     @Test
<lambda>null434     fun append_then_prepend() = runTest {
435         val source = TestPagingSource()
436         val pager = TestPager(CONFIG, source)
437 
438         pager.run {
439             refresh(20)
440             append()
441             prepend()
442         } as LoadResult.Page
443 
444         assertThat(pager.getPages())
445             .containsExactlyElementsIn(
446                 listOf(
447                     // prepend
448                     listOf(17, 18, 19).asPage(),
449                     // refresh
450                     listOf(20, 21, 22, 23, 24).asPage(),
451                     // append
452                     listOf(25, 26, 27).asPage(),
453                 )
454             )
455             .inOrder()
456     }
457 
458     @Test
<lambda>null459     fun prepend_then_append() = runTest {
460         val source = TestPagingSource()
461         val pager = TestPager(CONFIG, source)
462 
463         pager.run {
464             refresh(20)
465             prepend()
466             append()
467         } as LoadResult.Page
468 
469         assertThat(pager.getPages())
470             .containsExactlyElementsIn(
471                 listOf(
472                     // prepend
473                     listOf(17, 18, 19).asPage(),
474                     // refresh
475                     listOf(20, 21, 22, 23, 24).asPage(),
476                     // append
477                     listOf(25, 26, 27).asPage(),
478                 )
479             )
480             .inOrder()
481     }
482 
483     @Test
<lambda>null484     fun multiThread_loads() = runTest {
485         val source = TestPagingSource()
486         val pager = TestPager(CONFIG, source)
487         // load operations upon completion add an int to the list.
488         // after all loads complete, we assert the order that the ints were added.
489         val loadOrder = mutableListOf<Int>()
490 
491         val job = launch {
492             pager.run {
493                 refresh(20).also { loadOrder.add(1) } // first load
494                 prepend().also { loadOrder.add(3) } // third load
495                 append().also { loadOrder.add(5) } // fifth load
496             }
497         }
498         job.start()
499         assertTrue(job.isActive)
500 
501         pager.run {
502             // give some time for job to start
503             delay(200)
504             append().also { loadOrder.add(2) } // second load
505             prepend().also { loadOrder.add(4) } // fourth load
506         }
507 
508         advanceUntilIdle()
509         assertThat(loadOrder).containsExactlyElementsIn(listOf(1, 2, 3, 4, 5)).inOrder()
510         assertThat(pager.getPages())
511             .containsExactlyElementsIn(
512                 listOf(
513                     listOf(14, 15, 16).asPage(),
514                     listOf(17, 18, 19).asPage(),
515                     listOf(20, 21, 22, 23, 24).asPage(),
516                     listOf(25, 26, 27).asPage(),
517                     listOf(28, 29, 30).asPage(),
518                 )
519             )
520             .inOrder()
521     }
522 
523     @Test
<lambda>null524     fun multiThread_operations() = runTest {
525         val source = TestPagingSource()
526         val pager = TestPager(CONFIG, source)
527         // operations upon completion add an int to the list.
528         // after all operations complete, we assert the order that the ints were added.
529         val loadOrder = mutableListOf<Int>()
530 
531         var lastLoadedPage: LoadResult.Page<Int, Int>? = null
532         val job = launch {
533             pager.run {
534                 refresh(20).also { loadOrder.add(1) } // first operation
535                 // third operation, should return first appended page
536                 lastLoadedPage = getLastLoadedPage().also { loadOrder.add(3) }
537                 append().also { loadOrder.add(5) } // fifth operation
538                 prepend().also { loadOrder.add(7) } // last operation
539             }
540         }
541         job.start()
542         assertTrue(job.isActive)
543 
544         val pages =
545             pager.run {
546                 // give some time for job to start first
547                 delay(200)
548                 append().also { loadOrder.add(2) } // second operation
549                 prepend().also { loadOrder.add(4) } // fourth operation
550                 // sixth operation, should return 4 pages
551                 getPages().also { loadOrder.add(6) }
552             }
553 
554         advanceUntilIdle()
555         assertThat(loadOrder).containsExactlyElementsIn(listOf(1, 2, 3, 4, 5, 6, 7)).inOrder()
556         assertThat(lastLoadedPage)
557             .isEqualTo(
558                 listOf(25, 26, 27).asPage(),
559             )
560         // should not contain the second prepend, with a total of 4 pages
561         assertThat(pages)
562             .containsExactlyElementsIn(
563                 listOf(
564                     listOf(17, 18, 19).asPage(), // first prepend
565                     listOf(20, 21, 22, 23, 24).asPage(), // refresh
566                     listOf(25, 26, 27).asPage(), // first append
567                     listOf(28, 29, 30).asPage(), // second append
568                 )
569             )
570             .inOrder()
571     }
572 
573     @Test
<lambda>null574     fun getPagingStateWithAnchorPosition_placeHoldersEnabled() = runTest {
575         val source = TestPagingSource()
576         val pager = TestPager(CONFIG, source)
577 
578         val state =
579             pager.run {
580                 refresh(20)
581                 prepend()
582                 append()
583                 getPagingState(7)
584             }
585         // in this case anchorPos is a placeholder at index 7
586         assertThat(state)
587             .isEqualTo(
588                 PagingState(
589                     pages =
590                         listOf(
591                             listOf(17, 18, 19).asPage(),
592                             // refresh
593                             listOf(20, 21, 22, 23, 24).asPage(),
594                             // append
595                             listOf(25, 26, 27).asPage(),
596                         ),
597                     anchorPosition = 7,
598                     config = CONFIG,
599                     leadingPlaceholderCount = 17
600                 )
601             )
602         val source2 = TestPagingSource()
603         val pager2 = TestPager(CONFIG, source)
604         val page = pager2.run { refresh(source2.getRefreshKey(state)) }
605         assertThat(page).isEqualTo(listOf(7, 8, 9, 10, 11).asPage())
606     }
607 
608     @Test
<lambda>null609     fun getPagingStateWithAnchorPosition_placeHoldersDisabled() = runTest {
610         val source = TestPagingSource(placeholdersEnabled = false)
611         val config = PagingConfig(pageSize = 3, initialLoadSize = 5, enablePlaceholders = false)
612         val pager = TestPager(config, source)
613 
614         val state =
615             pager.run {
616                 refresh(20)
617                 prepend()
618                 append()
619                 getPagingState(7)
620             }
621         assertThat(state)
622             .isEqualTo(
623                 PagingState(
624                     pages =
625                         listOf(
626                             listOf(17, 18, 19).asPage(placeholdersEnabled = false),
627                             // refresh
628                             listOf(20, 21, 22, 23, 24).asPage(placeholdersEnabled = false),
629                             // append
630                             listOf(25, 26, 27).asPage(placeholdersEnabled = false),
631                         ),
632                     anchorPosition = 7,
633                     config = config,
634                     leadingPlaceholderCount = 0
635                 )
636             )
637         val source2 = TestPagingSource()
638         val pager2 = TestPager(CONFIG, source)
639         val page = pager2.run { refresh(source2.getRefreshKey(state)) }
640         // without placeholders, Paging currently has no way to translate item[7] within loaded
641         // pages into its absolute position within available data. Hence anchorPosition 7 will
642         // reference item[7] within available data.
643         assertThat(page).isEqualTo(listOf(7, 8, 9, 10, 11).asPage(placeholdersEnabled = false))
644     }
645 
646     @Test
<lambda>null647     fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithPlaceholders() = runTest {
648         val source = TestPagingSource()
649         val pager = TestPager(CONFIG, source)
650 
651         val msg =
652             assertFailsWith<IllegalStateException> {
653                     pager.run {
654                         refresh()
655                         append()
656                         getPagingState(-1)
657                     }
658                 }
659                 .message
660         assertThat(msg)
661             .isEqualTo(
662                 "anchorPosition -1 is out of bounds between [0..${ITEM_COUNT - 1}]. Please " +
663                     "provide a valid anchorPosition."
664             )
665 
666         val msg2 =
667             assertFailsWith<IllegalStateException> { pager.getPagingState(ITEM_COUNT) }.message
668         assertThat(msg2)
669             .isEqualTo(
670                 "anchorPosition $ITEM_COUNT is out of bounds between [0..${ITEM_COUNT - 1}]. " +
671                     "Please provide a valid anchorPosition."
672             )
673     }
674 
675     @Test
<lambda>null676     fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithoutPlaceholders() = runTest {
677         val source = TestPagingSource()
678         val pager =
679             TestPager(
680                 PagingConfig(pageSize = 3, initialLoadSize = 5, enablePlaceholders = false),
681                 source
682             )
683 
684         val msg =
685             assertFailsWith<IllegalStateException> {
686                     pager.run {
687                         refresh()
688                         append()
689                         getPagingState(-1)
690                     }
691                 }
692                 .message
693         assertThat(msg)
694             .isEqualTo(
695                 "anchorPosition -1 is out of bounds between [0..7]. Please " +
696                     "provide a valid anchorPosition."
697             )
698 
699         // total loaded items = 8, anchorPos with index 8 should be out of bounds
700         val msg2 = assertFailsWith<IllegalStateException> { pager.getPagingState(8) }.message
701         assertThat(msg2)
702             .isEqualTo(
703                 "anchorPosition 8 is out of bounds between [0..7]. Please " +
704                     "provide a valid anchorPosition."
705             )
706     }
707 
708     @Test
<lambda>null709     fun getPagingStateWithAnchorLookup_placeHoldersEnabled() = runTest {
710         val source = TestPagingSource()
711         val pager = TestPager(CONFIG, source)
712 
713         val state =
714             pager.run {
715                 refresh(20)
716                 prepend()
717                 append()
718                 getPagingState { it == TestPagingSource.ITEMS[22] }
719             }
720         assertThat(state)
721             .isEqualTo(
722                 PagingState(
723                     pages =
724                         listOf(
725                             listOf(17, 18, 19).asPage(),
726                             // refresh
727                             listOf(20, 21, 22, 23, 24).asPage(),
728                             // append
729                             listOf(25, 26, 27).asPage(),
730                         ),
731                     anchorPosition = 22,
732                     config = CONFIG,
733                     leadingPlaceholderCount = 17
734                 )
735             )
736         // use state to getRefreshKey
737         val source2 = TestPagingSource()
738         val pager2 = TestPager(CONFIG, source)
739         val page = pager2.run { refresh(source2.getRefreshKey(state)) }
740         assertThat(page).isEqualTo(listOf(22, 23, 24, 25, 26).asPage())
741     }
742 
743     @Test
<lambda>null744     fun getPagingStateWithAnchorLookup_placeHoldersDisabled() = runTest {
745         val source = TestPagingSource(placeholdersEnabled = false)
746         val config = PagingConfig(pageSize = 3, initialLoadSize = 5, enablePlaceholders = false)
747         val pager = TestPager(config, source)
748 
749         val state =
750             pager.run {
751                 refresh(20)
752                 prepend()
753                 append()
754                 getPagingState { it == TestPagingSource.ITEMS[22] } // item 22 in this case
755             }
756         assertThat(state)
757             .isEqualTo(
758                 PagingState(
759                     pages =
760                         listOf(
761                             listOf(17, 18, 19).asPage(placeholdersEnabled = false),
762                             // refresh
763                             listOf(20, 21, 22, 23, 24).asPage(placeholdersEnabled = false),
764                             // append
765                             listOf(25, 26, 27).asPage(placeholdersEnabled = false),
766                         ),
767                     anchorPosition = 5,
768                     config = config,
769                     leadingPlaceholderCount = 0
770                 )
771             )
772         // use state to getRefreshKey
773         val source2 = TestPagingSource()
774         val pager2 = TestPager(CONFIG, source)
775         val page = pager2.run { refresh(source2.getRefreshKey(state)) }
776         // without placeholders, Paging currently has no way to translate item[5] within loaded
777         // pages into its absolute position within available data. anchorPosition 5 will reference
778         // item[5] within available data.
779         assertThat(page).isEqualTo(listOf(5, 6, 7, 8, 9).asPage(placeholdersEnabled = false))
780     }
781 
782     @Test
<lambda>null783     fun getPagingStateWithAnchorLookup_itemNotFoundThrows() = runTest {
784         val source = TestPagingSource(placeholdersEnabled = false)
785         val config = PagingConfig(pageSize = 3, initialLoadSize = 5, enablePlaceholders = false)
786         val pager = TestPager(config, source)
787 
788         val msg =
789             assertFailsWith<IllegalArgumentException> {
790                     pager.run {
791                         refresh(20)
792                         prepend()
793                         append()
794                         getPagingState { it == TestPagingSource.ITEMS[10] }
795                     }
796                 }
797                 .message
798         assertThat(msg)
799             .isEqualTo(
800                 "The given predicate has returned false for every loaded item. To generate a" +
801                     "PagingState anchored to an item, the expected item must have already " +
802                     "been loaded."
803             )
804     }
805 
806     @Test
<lambda>null807     fun dropPrependedPage() = runTest {
808         val source = TestPagingSource()
809         val config =
810             PagingConfig(
811                 pageSize = 3,
812                 initialLoadSize = 5,
813                 enablePlaceholders = false,
814                 maxSize = 10
815             )
816         val pager = TestPager(config, source)
817         pager.run {
818             refresh(20)
819             prepend()
820         }
821         assertThat(pager.getPages())
822             .containsExactlyElementsIn(
823                 listOf(
824                     listOf(17, 18, 19).asPage(),
825                     // refresh
826                     listOf(20, 21, 22, 23, 24).asPage(),
827                 )
828             )
829 
830         // this append should trigger paging to drop the prepended page
831         pager.append()
832         assertThat(pager.getPages())
833             .containsExactlyElementsIn(
834                 listOf(
835                     listOf(20, 21, 22, 23, 24).asPage(),
836                     listOf(25, 26, 27).asPage(),
837                 )
838             )
839     }
840 
841     @Test
<lambda>null842     fun dropAppendedPage() = runTest {
843         val source = TestPagingSource()
844         val config =
845             PagingConfig(
846                 pageSize = 3,
847                 initialLoadSize = 5,
848                 enablePlaceholders = false,
849                 maxSize = 10
850             )
851         val pager = TestPager(config, source)
852         pager.run {
853             refresh(20)
854             append()
855         }
856         assertThat(pager.getPages())
857             .containsExactlyElementsIn(
858                 listOf(
859                     listOf(20, 21, 22, 23, 24).asPage(),
860                     listOf(25, 26, 27).asPage(),
861                 )
862             )
863 
864         // this prepend should trigger paging to drop the prepended page
865         pager.prepend()
866         assertThat(pager.getPages())
867             .containsExactlyElementsIn(
868                 listOf(
869                     listOf(17, 18, 19).asPage(),
870                     listOf(20, 21, 22, 23, 24).asPage(),
871                 )
872             )
873     }
874 
875     @Test
<lambda>null876     fun dropInitialRefreshedPage() = runTest {
877         val source = TestPagingSource()
878         val config =
879             PagingConfig(
880                 pageSize = 3,
881                 initialLoadSize = 5,
882                 enablePlaceholders = false,
883                 maxSize = 10
884             )
885         val pager = TestPager(config, source)
886         pager.run {
887             refresh(20)
888             append()
889         }
890         assertThat(pager.getPages())
891             .containsExactlyElementsIn(
892                 listOf(
893                     listOf(20, 21, 22, 23, 24).asPage(),
894                     listOf(25, 26, 27).asPage(),
895                 )
896             )
897 
898         // this append should trigger paging to drop the first page which is the initial refresh
899         pager.append()
900         assertThat(pager.getPages())
901             .containsExactlyElementsIn(
902                 listOf(
903                     listOf(25, 26, 27).asPage(),
904                     listOf(28, 29, 30).asPage(),
905                 )
906             )
907     }
908 
909     @Test
<lambda>null910     fun dropRespectsPrefetchDistance_InDroppedDirection() = runTest {
911         val source = TestPagingSource()
912         val config =
913             PagingConfig(
914                 pageSize = 1,
915                 initialLoadSize = 10,
916                 enablePlaceholders = false,
917                 maxSize = 5,
918                 prefetchDistance = 2
919             )
920         val pager = TestPager(config, source)
921         pager.refresh(20)
922         assertThat(pager.getPages())
923             .containsExactlyElementsIn(
924                 listOf(
925                     listOf(20, 21, 22, 23, 24, 25, 26, 27, 28, 29).asPage(),
926                 )
927             )
928 
929         // these appends should would normally trigger paging to drop first page, but it won't
930         // in this case due to prefetchDistance
931         pager.run {
932             append()
933             append()
934         }
935         assertThat(pager.getPages())
936             .containsExactlyElementsIn(
937                 listOf(
938                     // second page counted towards prefetch distance
939                     listOf(20, 21, 22, 23, 24, 25, 26, 27, 28, 29).asPage(),
940                     // first page counted towards prefetch distance
941                     listOf(30).asPage(),
942                     listOf(31).asPage()
943                 )
944             )
945     }
946 
947     @Test
<lambda>null948     fun drop_noOpUnderTwoPages() = runTest {
949         val source = TestPagingSource()
950         val config =
951             PagingConfig(
952                 pageSize = 1,
953                 initialLoadSize = 5,
954                 enablePlaceholders = false,
955                 maxSize = 3,
956                 prefetchDistance = 1
957             )
958         val pager = TestPager(config, source)
959         val result = pager.refresh() as LoadResult.Page
960         assertThat(result.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4))
961 
962         pager.append()
963         // data size exceeds maxSize but no data should be dropped
964         assertThat(pager.getPages().flatten()).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4, 5))
965     }
966 
967     private val CONFIG =
968         PagingConfig(
969             pageSize = 3,
970             initialLoadSize = 5,
971         )
972 
asPagenull973     private fun List<Int>.asPage(placeholdersEnabled: Boolean = true): LoadResult.Page<Int, Int> {
974         val itemsBefore =
975             if (placeholdersEnabled) {
976                 if (first() == 0) 0 else first()
977             } else {
978                 Int.MIN_VALUE
979             }
980         val itemsAfter =
981             if (placeholdersEnabled) {
982                 if (last() == ITEM_COUNT - 1) 0 else ITEM_COUNT - 1 - last()
983             } else {
984                 Int.MIN_VALUE
985             }
986         return LoadResult.Page(
987             data = this,
988             prevKey = if (first() == 0) null else first() - 1,
989             nextKey = if (last() == ITEM_COUNT - 1) null else last() + 1,
990             itemsBefore = itemsBefore,
991             itemsAfter = itemsAfter
992         )
993     }
994 
995     private val ITEM_COUNT = 100
996 }
997