44 buildPaginationQuery ,
55 paginate ,
66} from '../src/pagination' ;
7+ import type { CursorPage } from '../src/pagination' ;
78
89describe ( 'pagination helper (issue #61)' , ( ) => {
910 describe ( 'parseCursorPage' , ( ) => {
@@ -57,8 +58,17 @@ describe('pagination helper (issue #61)', () => {
5758 } ) ;
5859
5960 describe ( 'paginate' , ( ) => {
60- it ( 'yields all items from a single page' , async ( ) => {
61- const fetchPage = vi . fn ( ) . mockResolvedValue ( [ 1 , 2 , 3 ] ) ;
61+ const page = (
62+ items : number [ ] ,
63+ nextCursor : string | null ,
64+ ) : CursorPage < number > => ( {
65+ items,
66+ nextCursor,
67+ hasMore : nextCursor != null && nextCursor !== '' ,
68+ } ) ;
69+
70+ it ( 'yields all items from a single cursor-less page' , async ( ) => {
71+ const fetchPage = vi . fn ( ) . mockResolvedValue ( page ( [ 1 , 2 , 3 ] , null ) ) ;
6272 const results : number [ ] = [ ] ;
6373 for await ( const item of paginate < number > ( fetchPage ) ) {
6474 results . push ( item ) ;
@@ -67,24 +77,72 @@ describe('pagination helper (issue #61)', () => {
6777 expect ( fetchPage ) . toHaveBeenCalledTimes ( 1 ) ;
6878 } ) ;
6979
70- it ( 'stops at maxPages when limit is set' , async ( ) => {
71- const fetchPage = vi . fn ( ) . mockResolvedValue ( [ 1 , 2 ] ) ;
80+ it ( 'advances on cursor metadata across a two-page dataset (issue #403)' , async ( ) => {
81+ const fetchPage = vi
82+ . fn ( )
83+ . mockResolvedValueOnce ( page ( [ 1 , 2 ] , 'c1' ) )
84+ . mockResolvedValueOnce ( page ( [ 3 , 4 ] , null ) ) ;
85+ const results : number [ ] = [ ] ;
86+ for await ( const item of paginate < number > ( fetchPage , { limit : 2 } ) ) {
87+ results . push ( item ) ;
88+ }
89+ expect ( results ) . toEqual ( [ 1 , 2 , 3 , 4 ] ) ;
90+ expect ( fetchPage ) . toHaveBeenCalledTimes ( 2 ) ;
91+ // First call carries only the limit, second call feeds the cursor back.
92+ expect ( fetchPage ) . toHaveBeenNthCalledWith ( 1 , { limit : 2 } ) ;
93+ expect ( fetchPage ) . toHaveBeenNthCalledWith ( 2 , {
94+ limit : 2 ,
95+ cursor : 'c1' ,
96+ } ) ;
97+ } ) ;
98+
99+ it ( 'never re-fetches a page whose length equals limit (issue #403)' , async ( ) => {
100+ // Old bug: a full page with no further cursor was re-requested forever,
101+ // yielding duplicate items.
102+ const fetchPage = vi
103+ . fn ( )
104+ . mockResolvedValueOnce ( page ( [ 1 , 2 ] , null ) )
105+ . mockResolvedValue ( page ( [ 1 , 2 ] , null ) ) ;
106+ const results : number [ ] = [ ] ;
107+ for await ( const item of paginate < number > ( fetchPage , { limit : 2 } ) ) {
108+ results . push ( item ) ;
109+ }
110+ expect ( results ) . toEqual ( [ 1 , 2 ] ) ;
111+ expect ( fetchPage ) . toHaveBeenCalledTimes ( 1 ) ;
112+ } ) ;
113+
114+ it ( 'stops at maxPages while the server keeps returning cursors' , async ( ) => {
115+ const fetchPage = vi . fn ( ) . mockResolvedValue ( page ( [ 1 , 2 ] , 'c-next' ) ) ;
72116 const results : number [ ] = [ ] ;
73117 for await ( const item of paginate < number > ( fetchPage , {
74118 limit : 2 ,
75119 maxPages : 3 ,
76120 } ) ) {
77121 results . push ( item ) ;
78122 }
79- expect ( results . length ) . toBe ( 6 ) ; // 3 pages * 2 items
123+ expect ( results ) . toEqual ( [ 1 , 2 , 1 , 2 , 1 , 2 ] ) ;
80124 expect ( fetchPage ) . toHaveBeenCalledTimes ( 3 ) ;
125+ expect ( fetchPage ) . toHaveBeenLastCalledWith ( {
126+ limit : 2 ,
127+ cursor : 'c-next' ,
128+ } ) ;
129+ } ) ;
130+
131+ it ( 'stops on an empty page' , async ( ) => {
132+ const fetchPage = vi . fn ( ) . mockResolvedValue ( page ( [ ] , null ) ) ;
133+ const results : number [ ] = [ ] ;
134+ for await ( const item of paginate < number > ( fetchPage , { limit : 10 } ) ) {
135+ results . push ( item ) ;
136+ }
137+ expect ( results ) . toEqual ( [ ] ) ;
138+ expect ( fetchPage ) . toHaveBeenCalledTimes ( 1 ) ;
81139 } ) ;
82140
83- it ( 'stops when page returns fewer items than limit' , async ( ) => {
141+ it ( 'stops when a page is shorter than the limit' , async ( ) => {
84142 const fetchPage = vi
85143 . fn ( )
86- . mockResolvedValueOnce ( [ 1 , 2 ] )
87- . mockResolvedValueOnce ( [ 3 ] ) ;
144+ . mockResolvedValueOnce ( page ( [ 1 , 2 ] , 'c1' ) )
145+ . mockResolvedValueOnce ( page ( [ 3 ] , 'c2' ) ) ;
88146 const results : number [ ] = [ ] ;
89147 for await ( const item of paginate < number > ( fetchPage , { limit : 2 } ) ) {
90148 results . push ( item ) ;
@@ -93,13 +151,17 @@ describe('pagination helper (issue #61)', () => {
93151 expect ( fetchPage ) . toHaveBeenCalledTimes ( 2 ) ;
94152 } ) ;
95153
96- it ( 'stops on empty page' , async ( ) => {
97- const fetchPage = vi . fn ( ) . mockResolvedValue ( [ ] ) ;
154+ it ( 'stops when hasMore is false even if a cursor leaks back' , async ( ) => {
155+ const fetchPage = vi . fn ( ) . mockResolvedValue ( {
156+ items : [ 1 ] ,
157+ nextCursor : 'c1' ,
158+ hasMore : false ,
159+ } ) ;
98160 const results : number [ ] = [ ] ;
99- for await ( const item of paginate < number > ( fetchPage , { limit : 10 } ) ) {
161+ for await ( const item of paginate < number > ( fetchPage ) ) {
100162 results . push ( item ) ;
101163 }
102- expect ( results ) . toEqual ( [ ] ) ;
164+ expect ( results ) . toEqual ( [ 1 ] ) ;
103165 expect ( fetchPage ) . toHaveBeenCalledTimes ( 1 ) ;
104166 } ) ;
105167 } ) ;
0 commit comments