Skip to content

Commit

Permalink
add tests for full coverage of useLazyLoad
Browse files Browse the repository at this point in the history
  • Loading branch information
konstantin-lukas committed Sep 6, 2024
1 parent c05ea66 commit 3614f45
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 20 deletions.
10 changes: 7 additions & 3 deletions src/hooks/useLazyLoad.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";

export interface LazyLoadState<T> {
/** Calling this function triggers fetching more items an updating the hook state. Calling this function while
Expand Down Expand Up @@ -87,7 +87,7 @@ function useLazyLoad<T>(
const [isFetching, setIsFetching] = useState(false);
const [reachedEnd, setReachedEnd] = useState(false);

const loadMore = useCallback(async () => {
const loadMore = async () => {
if (isFetching || reachedEnd) return;
setIsFetching(true);
let newElements: T[] = [];
Expand All @@ -112,7 +112,11 @@ function useLazyLoad<T>(
});
fetchCount.current += 1;
setIsFetching(false);
}, [elementLimit, batchSize, truncate]);
};

useEffect(() => {
setReachedEnd(elementLimit <= elements.length);
}, [elementLimit]);

return {
loadMore,
Expand Down
119 changes: 102 additions & 17 deletions tests/useLazyLoad.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Animal {
const animalsMockData = [
{ name: "bear", call: "roar" },
{ name: "mouse", call: "squeak" },
{ name: "bear", call: "roar" },
{ name: "lion", call: "roar" },
{ name: "bee", call: "buzz" },
{ name: "tiger", call: "roar" },
{ name: "capybara", call: "squeak" },
Expand All @@ -35,13 +35,18 @@ global.fetch = jest.fn(async (url: string) => {
});
}) as jest.Mock;

async function fetchFunction(fetchIndex: number) {
const data = await fetch(`https://api.org/animals?length=3&offset=${fetchIndex * 3}`);
return await data.json();
}

async function renderUseLazyLoad({
initialLimit,
initialFunc,
initialOptions,
}: {
initialLimit?: number,
initialFunc?: () => Promise<Animal[]>,
initialLimit?: number | (() => number),
initialFunc?: (n: number) => Promise<Animal[]>,
initialOptions?: LazyLoadOptions,
initialLength?: number,
initialOffset?: number,
Expand All @@ -52,10 +57,7 @@ async function renderUseLazyLoad({
} = await act(async () => {
return renderHook(({ func, limit, options }) => useLazyLoad<Animal>(limit, func, options), {
initialProps: {
func: initialFunc ?? (async (fetchIndex: number) => {
const data = await fetch(`https://api.org/animals?length=3&offset=${fetchIndex * 3}`);
return await data.json();
}),
func: initialFunc ?? fetchFunction,
limit: initialLimit ?? 15,
options: initialOptions ?? {},
},
Expand All @@ -81,34 +83,117 @@ test("should fetch data when triggered to do so and stop when reaching the end",
});

test("should truncate the results by default if the final fetch returns more results than expected", async () => {
/* TODO */
const { result } = await renderUseLazyLoad({ initialLimit: 5 });

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 3));
expect(result.current.reachedEnd).toBe(false);

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 5));
expect(result.current.reachedEnd).toBe(true);
});

test("should not truncate the results if the final fetch returns more results than expected and configured to do so", async () => {
/* TODO */
const { result } = await renderUseLazyLoad({ initialLimit: 5, initialOptions: { truncate: false }});

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 3));
expect(result.current.reachedEnd).toBe(false);

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 6));
expect(result.current.reachedEnd).toBe(true);
});

test("should reach the end if a batch size is specified and a fetch result returns a shorter result", async () => {
/* TODO */
const { result } = await renderUseLazyLoad({ initialOptions: { batchSize: 10 }});

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 3));
expect(result.current.reachedEnd).toBe(true);
});

test("should reach the end by default if the fetch function throws an error", async () => {
/* TODO */
const { result } = await renderUseLazyLoad({ initialFunc: () => { throw new Error(); } });

await act(result.current.loadMore);
expect(result.current.elements).toEqual([]);
expect(result.current.reachedEnd).toBe(true);
});

test("should not reach the end by if the fetch function throws an error and configured to do so", async () => {
/* TODO */
const { result } = await renderUseLazyLoad({
initialFunc: () => { throw new Error(); },
initialOptions: { continueOnError: true },
});

await act(result.current.loadMore);
expect(result.current.elements).toEqual([]);
expect(result.current.reachedEnd).toBe(false);
});

test("should clear its state when the clear() function is called", async () => {
/* TODO */
const { result } = await renderUseLazyLoad({});

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 3));
expect(result.current.reachedEnd).toBe(false);

await act(async () => result.current.clear());
expect(result.current.elements).toEqual([]);
expect(result.current.reachedEnd).toBe(false);
});

test("should clear its state when the fetch function changes", async () => {
/* TODO */
test("should allow changing the fetch function", async () => {
const options = { };
const { result, rerender } = await renderUseLazyLoad({ initialLimit: 5, initialFunc: fetchFunction, initialOptions: options });

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 3));
expect(result.current.reachedEnd).toBe(false);

rerender({ limit: 5, func: () => { return fetchFunction(2); }, options: options });
await act(result.current.loadMore);
expect(result.current.elements).toEqual([...animalsMockData.slice(0, 3), ...animalsMockData.slice(6, 8)]);
expect(result.current.reachedEnd).toBe(true);
});

test("should allow fetching again if the maxElements prop changes after reaching the end", async () => {
/* TODO */
test("should allow fetching again if the maxElements prop changes to a larger number after reaching the end", async () => {
const options = { };
const { result, rerender } = await renderUseLazyLoad({ initialLimit: 3, initialFunc: fetchFunction, initialOptions: options });

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 3));
expect(result.current.reachedEnd).toBe(true);

rerender({ limit: 5, func: fetchFunction, options: options });
expect(result.current.reachedEnd).toBe(false);

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 5));
expect(result.current.reachedEnd).toBe(true);
});

test("should allow passing a callback function to calculate the element limit", async () => {
const { result } = await renderUseLazyLoad({ initialLimit: () => 1 + 1 });

await act(result.current.loadMore);
expect(result.current.elements).toEqual(animalsMockData.slice(0, 2));
expect(result.current.reachedEnd).toBe(true);

});


test("should do nothing if currently fetching or having reached the end", async () => {
const f = jest.fn(() => { throw new Error(); });
const { result } = await renderUseLazyLoad({ initialFunc: f });

await act(result.current.loadMore);
expect(result.current.reachedEnd).toBe(true);
expect(f).toHaveBeenCalledTimes(1);

await act(result.current.loadMore);
expect(f).toHaveBeenCalledTimes(1);
});

0 comments on commit 3614f45

Please sign in to comment.