@@ -6,6 +6,7 @@ import { ContentList } from "../../src/components/ContentList";
66import type { ContentItem , TrashedContentItem } from "../../src/lib/api" ;
77
88const NO_RESULTS_PATTERN = / N o r e s u l t s f o r / ;
9+ const HAS_MORE_ITEMS_PATTERN = / 2 1 \+ i t e m s / ;
910
1011// ---------------------------------------------------------------------------
1112// Constants
@@ -304,16 +305,67 @@ describe("ContentList", () => {
304305 expect ( screen . getByRole ( "button" , { name : "Load More" } ) . query ( ) ) . toBeNull ( ) ;
305306 } ) ;
306307
308+ it ( "auto-fetches when user navigates to the last client-side page" , async ( ) => {
309+ const onLoadMore = vi . fn ( ) ;
310+ // 21 items = 2 pages of 20; user starts on page 0 (not the last page)
311+ const items = Array . from ( { length : 21 } , ( _ , i ) => makeItem ( { id : `item_${ i } ` } ) ) ;
312+ const screen = await render (
313+ < ContentList { ...defaultProps } items = { items } hasMore = { true } onLoadMore = { onLoadMore } /> ,
314+ ) ;
315+
316+ // On mount, page 0 is not the last page — no fetch yet
317+ expect ( onLoadMore ) . not . toHaveBeenCalled ( ) ;
318+
319+ // Navigate to page 2 (the last page)
320+ await screen . getByRole ( "button" , { name : "Next page" } ) . click ( ) ;
321+
322+ expect ( onLoadMore ) . toHaveBeenCalledOnce ( ) ;
323+ } ) ;
324+
325+ it ( "does not auto-fetch when a search query is active" , async ( ) => {
326+ const onLoadMore = vi . fn ( ) ;
327+ // 21 items so pagination exists, but search will collapse to 1 result / 1 page
328+ const items = [
329+ ...Array . from ( { length : 20 } , ( _ , i ) =>
330+ makeItem ( { id : `item_${ i } ` , data : { title : `Post ${ i } ` } } ) ,
331+ ) ,
332+ makeItem ( { id : "unique" , data : { title : "Unique Title" } } ) ,
333+ ] ;
334+ const screen = await render (
335+ < ContentList { ...defaultProps } items = { items } hasMore = { true } onLoadMore = { onLoadMore } /> ,
336+ ) ;
337+
338+ // No fetch on mount (page 0 is not the last page with 21 items)
339+ expect ( onLoadMore ) . not . toHaveBeenCalled ( ) ;
340+
341+ // Search collapses results to 1 item — totalPages becomes 1, but should NOT fetch
342+ await screen . getByRole ( "searchbox" ) . fill ( "Unique Title" ) ;
343+
344+ expect ( onLoadMore ) . not . toHaveBeenCalled ( ) ;
345+ } ) ;
346+
347+ it ( "shows '+' suffix on item count when hasMore is true and no search is active" , async ( ) => {
348+ const items = Array . from ( { length : 21 } , ( _ , i ) => makeItem ( { id : `item_${ i } ` } ) ) ;
349+ const screen = await render ( < ContentList { ...defaultProps } items = { items } hasMore = { true } /> ) ;
350+
351+ await expect . element ( screen . getByText ( HAS_MORE_ITEMS_PATTERN ) ) . toBeInTheDocument ( ) ;
352+ } ) ;
353+
307354 it ( "calls onLoadMore when Load More is clicked" , async ( ) => {
308355 const onLoadMore = vi . fn ( ) ;
309356 const items = [ makeItem ( ) ] ;
310357 const screen = await render (
311358 < ContentList { ...defaultProps } items = { items } hasMore = { true } onLoadMore = { onLoadMore } /> ,
312359 ) ;
313360
361+ // With 1 item and hasMore=true, the auto-fetch effect fires on mount
362+ // because page 0 is already the last client-side page.
363+ // The button click adds a second call on top of that.
364+ expect ( onLoadMore ) . toHaveBeenCalledOnce ( ) ;
365+
314366 await screen . getByRole ( "button" , { name : "Load More" } ) . click ( ) ;
315367
316- expect ( onLoadMore ) . toHaveBeenCalledOnce ( ) ;
368+ expect ( onLoadMore ) . toHaveBeenCalledTimes ( 2 ) ;
317369 } ) ;
318370 } ) ;
319371
0 commit comments