From 463d2ca20e050fb5c3c9503073f37157976e6069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Sat, 28 Mar 2026 12:08:20 +0900 Subject: [PATCH 1/2] test(shared): achieve 100% unit test coverage Add test files for canLoadPage and findMissingPages utilities, and extend useInfinitePages tests with abort signal edge cases. Update vitest config to exclude barrel index.ts files from coverage. Closes #24 Co-Authored-By: Claude Sonnet 4.6 --- .../shared/src/hooks/useInfinitePages.test.ts | 86 +++++++++++++++++++ packages/shared/src/utils/canLoadPage.test.ts | 62 +++++++++++++ .../shared/src/utils/findMissingPages.test.ts | 69 +++++++++++++++ packages/shared/vitest.config.ts | 8 ++ 4 files changed, 225 insertions(+) create mode 100644 packages/shared/src/utils/canLoadPage.test.ts create mode 100644 packages/shared/src/utils/findMissingPages.test.ts 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..f93fd46 --- /dev/null +++ b/packages/shared/src/utils/findMissingPages.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "vitest"; +import { findMissingPages } from "./findMissingPages"; + +describe("findMissingPages", () => { + it("returns all pages in range when nothing is loaded or loading", () => { + const result = findMissingPages(0, 2, new Map(), new Set()); + expect(result).toEqual([0, 1, 2]); + }); + + it("returns empty array when start > end", () => { + const result = findMissingPages(5, 3, new Map(), new Set()); + 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, new Set()); + expect(result).toEqual([]); + }); + + it("excludes pages that are already loaded", () => { + const pages = new Map([[1, [1, 2]]]); + const result = findMissingPages(0, 3, pages, new Set()); + expect(result).toEqual([0, 2, 3]); + }); + + it("excludes pages that are currently loading", () => { + const loadingPages = new Set([2]); + const result = findMissingPages(0, 3, new Map(), 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, new Set()); + expect(result).toEqual([]); + }); + + it("returns empty array when all pages are loading", () => { + const loadingPages = new Set([0, 1, 2]); + const result = findMissingPages(0, 2, new Map(), loadingPages); + expect(result).toEqual([]); + }); + + it("handles single-page range that is missing", () => { + const result = findMissingPages(3, 3, new Map(), new Set()); + expect(result).toEqual([3]); + }); + + it("handles large range with sparse loaded pages", () => { + const pages = new Map([ + [2, []], + [5, []], + ]); + const result = findMissingPages(0, 6, pages, new Set()); + 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}", + ], }, }, }); From 3d37d9eafbf5cf40938bc779cb97e4f753d24179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Sat, 28 Mar 2026 12:14:58 +0900 Subject: [PATCH 2/2] fix: pattern simplify --- .../shared/src/utils/findMissingPages.test.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/shared/src/utils/findMissingPages.test.ts b/packages/shared/src/utils/findMissingPages.test.ts index f93fd46..ba545ad 100644 --- a/packages/shared/src/utils/findMissingPages.test.ts +++ b/packages/shared/src/utils/findMissingPages.test.ts @@ -2,31 +2,34 @@ 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, new Map(), new Set()); + 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, new Map(), new Set()); + 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, new Set()); + 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, new Set()); + 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, new Map(), loadingPages); + const result = findMissingPages(0, 3, emptyPages, loadingPages); expect(result).toEqual([0, 1, 3]); }); @@ -43,18 +46,18 @@ describe("findMissingPages", () => { [1, [2]], [2, [3]], ]); - const result = findMissingPages(0, 2, pages, new Set()); + 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, new Map(), loadingPages); + const result = findMissingPages(0, 2, emptyPages, loadingPages); expect(result).toEqual([]); }); it("handles single-page range that is missing", () => { - const result = findMissingPages(3, 3, new Map(), new Set()); + const result = findMissingPages(3, 3, emptyPages, emptyLoading); expect(result).toEqual([3]); }); @@ -63,7 +66,7 @@ describe("findMissingPages", () => { [2, []], [5, []], ]); - const result = findMissingPages(0, 6, pages, new Set()); + const result = findMissingPages(0, 6, pages, emptyLoading); expect(result).toEqual([0, 1, 3, 4, 6]); }); });