diff --git a/packages/shared/src/hooks/useInfinitePages.test.ts b/packages/shared/src/hooks/useInfinitePages.test.ts index 5db014f..3da0ee9 100644 --- a/packages/shared/src/hooks/useInfinitePages.test.ts +++ b/packages/shared/src/hooks/useInfinitePages.test.ts @@ -365,4 +365,90 @@ describe("useInfinitePages", () => { expect(result.current.error?.message).toBe("String error"); }); + + it("does not update state when reset is called before fetch resolves", async () => { + let resolvePromise!: (value: PageResponse<{ id: number }>) => void; + mockFetchPage.mockImplementation( + () => + new Promise((resolve) => { + resolvePromise = resolve; + }) + ); + + const { result } = renderHook(() => + useInfinitePages({ + fetchPage: mockFetchPage, + pageSize: 20, + initialPage: 0, + }) + ); + + act(() => { + result.current.loadPage(0); + }); + + await waitFor(() => { + expect(result.current.loadingPages.has(0)).toBe(true); + }); + + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.loadingPages.size).toBe(0); + }); + + act(() => { + resolvePromise({ items: [{ id: 1 }], total: 100, hasMore: true }); + }); + + await waitFor(() => { + expect(result.current.pages.size).toBe(0); + expect(result.current.total).toBe(0); + }); + }); + + it("does not set error when reset is called before fetch rejects", async () => { + let rejectPromise!: (reason?: unknown) => void; + mockFetchPage.mockImplementation( + () => + new Promise((_, reject) => { + rejectPromise = reject; + }) + ); + + const { result } = renderHook(() => + useInfinitePages({ + fetchPage: mockFetchPage, + pageSize: 20, + initialPage: 0, + }) + ); + + act(() => { + result.current.loadPage(0); + }); + + await waitFor(() => { + expect(result.current.loadingPages.has(0)).toBe(true); + }); + + act(() => { + result.current.reset(); + }); + + await waitFor(() => { + expect(result.current.loadingPages.size).toBe(0); + }); + + act(() => { + rejectPromise(new Error("network error")); + }); + + await waitFor(() => { + expect(result.current.error).toBeNull(); + expect(result.current.pages.size).toBe(0); + }); + }); }); diff --git a/packages/shared/src/utils/canLoadPage.test.ts b/packages/shared/src/utils/canLoadPage.test.ts new file mode 100644 index 0000000..b070e94 --- /dev/null +++ b/packages/shared/src/utils/canLoadPage.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; +import { canLoadPage } from "./canLoadPage"; + +describe("canLoadPage", () => { + const emptyPages = new Map(); + const emptyLoading = new Set(); + + it("returns true for first page in empty state", () => { + expect(canLoadPage(0, emptyPages, emptyLoading, 0, 20, true)).toBe(true); + }); + + it("returns false when page is already loaded", () => { + const pages = new Map([[0, [1, 2, 3]]]); + expect(canLoadPage(0, pages, emptyLoading, 100, 20, true)).toBe(false); + }); + + it("returns false when page is currently loading", () => { + const loadingPages = new Set([2]); + expect(canLoadPage(2, emptyPages, loadingPages, 100, 20, true)).toBe(false); + }); + + it("returns false when page is both loaded and loading", () => { + const pages = new Map([[1, [1]]]); + const loadingPages = new Set([1]); + expect(canLoadPage(1, pages, loadingPages, 100, 20, true)).toBe(false); + }); + + it("returns false when page * pageSize >= total", () => { + // total=50, pageSize=20: page 3 → 3*20=60 >= 50 + expect(canLoadPage(3, emptyPages, emptyLoading, 50, 20, true)).toBe(false); + }); + + it("returns false when page * pageSize exactly equals total", () => { + // total=40, pageSize=20: page 2 → 2*20=40 >= 40 + expect(canLoadPage(2, emptyPages, emptyLoading, 40, 20, true)).toBe(false); + }); + + it("returns true when page * pageSize is within total", () => { + // total=50, pageSize=20: page 2 → 2*20=40 < 50 + expect(canLoadPage(2, emptyPages, emptyLoading, 50, 20, true)).toBe(true); + }); + + it("skips total check when total is 0", () => { + expect(canLoadPage(0, emptyPages, emptyLoading, 0, 20, true)).toBe(true); + }); + + it("returns false when !hasMore and page exceeds last page index (total=0)", () => { + // total=0, hasMore=false: Math.floor(0/20)=0, page=1 > 0 → false + expect(canLoadPage(1, emptyPages, emptyLoading, 0, 20, false)).toBe(false); + }); + + it("returns true for page 0 when !hasMore and total is 0", () => { + // Math.floor(0/20)=0, page=0 is NOT > 0 + expect(canLoadPage(0, emptyPages, emptyLoading, 0, 20, false)).toBe(true); + }); + + it("returns true when hasMore is true even if page is beyond floor(total/pageSize)", () => { + // total=50, pageSize=20: floor(50/20)=2, page=2 → NOT > 2, but line 11 catches it (2*20=40<50 → ok) + // Use total=0 so line 11 is skipped: page=5, hasMore=true → line 13 not triggered → true + expect(canLoadPage(5, emptyPages, emptyLoading, 0, 20, true)).toBe(true); + }); +}); diff --git a/packages/shared/src/utils/findMissingPages.test.ts b/packages/shared/src/utils/findMissingPages.test.ts new file mode 100644 index 0000000..ba545ad --- /dev/null +++ b/packages/shared/src/utils/findMissingPages.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect } from "vitest"; +import { findMissingPages } from "./findMissingPages"; + +describe("findMissingPages", () => { + const emptyPages = new Map(); + const emptyLoading = new Set(); + + it("returns all pages in range when nothing is loaded or loading", () => { + const result = findMissingPages(0, 2, emptyPages, emptyLoading); + expect(result).toEqual([0, 1, 2]); + }); + + it("returns empty array when start > end", () => { + const result = findMissingPages(5, 3, emptyPages, emptyLoading); + expect(result).toEqual([]); + }); + + it("returns empty array when start === end and page is loaded", () => { + const pages = new Map([[1, [1, 2]]]); + const result = findMissingPages(1, 1, pages, emptyLoading); + expect(result).toEqual([]); + }); + + it("excludes pages that are already loaded", () => { + const pages = new Map([[1, [1, 2]]]); + const result = findMissingPages(0, 3, pages, emptyLoading); + expect(result).toEqual([0, 2, 3]); + }); + + it("excludes pages that are currently loading", () => { + const loadingPages = new Set([2]); + const result = findMissingPages(0, 3, emptyPages, loadingPages); + expect(result).toEqual([0, 1, 3]); + }); + + it("excludes pages that are both loaded and loading", () => { + const pages = new Map([[0, [1]]]); + const loadingPages = new Set([2]); + const result = findMissingPages(0, 3, pages, loadingPages); + expect(result).toEqual([1, 3]); + }); + + it("returns empty array when all pages are loaded", () => { + const pages = new Map([ + [0, [1]], + [1, [2]], + [2, [3]], + ]); + const result = findMissingPages(0, 2, pages, emptyLoading); + expect(result).toEqual([]); + }); + + it("returns empty array when all pages are loading", () => { + const loadingPages = new Set([0, 1, 2]); + const result = findMissingPages(0, 2, emptyPages, loadingPages); + expect(result).toEqual([]); + }); + + it("handles single-page range that is missing", () => { + const result = findMissingPages(3, 3, emptyPages, emptyLoading); + expect(result).toEqual([3]); + }); + + it("handles large range with sparse loaded pages", () => { + const pages = new Map([ + [2, []], + [5, []], + ]); + const result = findMissingPages(0, 6, pages, emptyLoading); + expect(result).toEqual([0, 1, 3, 4, 6]); + }); +}); diff --git a/packages/shared/vitest.config.ts b/packages/shared/vitest.config.ts index bb3f96d..45d1d8f 100644 --- a/packages/shared/vitest.config.ts +++ b/packages/shared/vitest.config.ts @@ -7,6 +7,14 @@ export default defineConfig({ coverage: { provider: "v8", reporter: ["text", "json", "html", "json-summary"], + exclude: [ + "node_modules/", + "dist/", + "**/*.d.ts", + "**/*.config.*", + "**/index.ts", + "**/*.test.{ts,tsx}", + ], }, }, });