@hyper-fetch/react
Version:
React hooks and utils for the hyper-fetch
373 lines (316 loc) • 11.6 kB
text/typescript
import { act, waitFor } from "@testing-library/react";
import type { AdapterInstance, ResponseType } from "@hyper-fetch/core";
import { xhrExtra } from "@hyper-fetch/core";
import { createHttpMockingServer, sleep } from "@hyper-fetch/testing";
import { createRequest, renderUseFetch, createCacheData, client } from "../../utils";
import { testSuccessState, testErrorState, testInitialState, testCacheState, testClientIsolation } from "../../shared";
const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();
describe("useFetch [ Base ]", () => {
let request = createRequest();
beforeAll(() => {
startServer();
});
afterEach(() => {
resetMocks();
});
afterAll(() => {
stopServer();
});
beforeEach(() => {
vi.resetModules();
request = createRequest();
client.clear();
});
describe("when hook is initialized", () => {
it("should initialize in loading state", async () => {
await testClientIsolation(client);
mockRequest(request);
const view = renderUseFetch(request);
testInitialState(view);
});
it("should load cached data", async () => {
await testClientIsolation(client);
const mock = mockRequest(request);
const [cache] = createCacheData(request, {
data: {
data: mock,
error: null,
status: 200,
success: true,
extra: xhrExtra,
responseTimestamp: +new Date(),
requestTimestamp: +new Date(),
},
details: {
retries: 2,
},
});
const view = renderUseFetch(request);
await testCacheState(cache, view);
});
it("should show stale cache data while revalidating", async () => {
await testClientIsolation(client);
const timestamp = +new Date() - 11;
const mock = mockRequest(request, { delay: 50 });
act(() => {
createCacheData(request, {
data: {
data: mock,
error: null,
status: 200,
success: true,
extra: xhrExtra,
responseTimestamp: timestamp,
requestTimestamp: timestamp,
},
details: {
triggerTimestamp: timestamp,
responseTimestamp: timestamp,
requestTimestamp: timestamp,
retries: 3,
},
});
});
const view = renderUseFetch(request.setStaleTime(10));
await testCacheState(
{
data: mock,
error: null,
status: 200,
success: true,
extra: xhrExtra,
responseTimestamp: timestamp,
requestTimestamp: timestamp,
},
view,
);
});
it("should allow to use initial data", async () => {
await testClientIsolation(client);
const initialResponse: ResponseType<unknown, Error, AdapterInstance> = {
data: { test: [1, 2, 3] },
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json" } },
responseTimestamp: +new Date(),
requestTimestamp: +new Date(),
};
const view = renderUseFetch(request, { disabled: true, initialResponse });
expect(view.result.current.data).toEqual(initialResponse.data);
await testSuccessState(initialResponse.data, view);
});
it("should allow to use initial data while requesting", async () => {
await testClientIsolation(client);
mockRequest(request);
const initialResponse: ResponseType<unknown, Error, AdapterInstance> = {
data: { test: [1, 2, 3] },
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json" } },
responseTimestamp: +new Date(),
requestTimestamp: +new Date(),
};
const view = renderUseFetch(request, { initialResponse });
expect(view.result.current.data).toEqual(initialResponse.data);
});
it("should prefer cache data over initial data", async () => {
// Todo
});
it("should make only one request", async () => {
const spy = vi.spyOn(client.adapter, "fetch");
await testClientIsolation(client);
const mock = mockRequest(request);
const view = renderUseFetch(request);
await testSuccessState(mock, view);
await act(async () => {
await sleep(50);
});
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("when hook get success response", () => {
it("should set state with success data", async () => {
await testClientIsolation(client);
const mock = mockRequest(request);
const view = renderUseFetch(request);
await testSuccessState(mock, view);
});
it("should map the data on deps change", async () => {
mockRequest(request);
const request1 = request
.setCacheKey("request1")
.setResponseMapper((response) => ({ ...response, data: 1 }) as typeof response);
const request2 = request
.setCacheKey("request2")
.setResponseMapper((response) => ({ ...response, data: 2 }) as typeof response);
const { result, rerender } = renderUseFetch(request1);
act(() => {
rerender({ request: request2 });
rerender({ request: request1 });
result.current.refetch();
});
await waitFor(() => {
expect(result.current.data).toBe(1);
expect(result.current.error).toBe(null);
});
act(() => {
rerender({ request: request2 });
result.current.refetch();
});
await waitFor(() => {
expect(result.current.data).toBe(2);
expect(result.current.error).toBe(null);
});
});
it("should clear previous error state once success response is returned", async () => {
await testClientIsolation(client);
const errorMock = mockRequest(request, { status: 400 });
const view = renderUseFetch(request);
await testErrorState(errorMock, view);
const mock = mockRequest(request);
act(() => {
view.result.current.refetch();
});
await testSuccessState(mock, view);
});
it("should change loading to false on success", async () => {
// Todo
});
it("should map response data", async () => {
const mappedData = { test: 1, test2: 2, test3: 3 };
const mappedRequest = request.setResponseMapper(
(response) =>
({
...response,
data: mappedData,
}) as typeof response,
);
mockRequest(mappedRequest);
const view = renderUseFetch(mappedRequest);
await testSuccessState(mappedData, view);
});
it("should map async response data", async () => {
const spy = vi.fn();
const mappedData = { test: 1, test2: 2, test3: 3 };
const mappedRequest = request.setResponseMapper(
async (response) =>
({
...response,
data: mappedData,
}) as typeof response,
);
mockRequest(mappedRequest);
const view = renderUseFetch(mappedRequest, { disabled: true });
act(() => {
view.result.current.onSuccess(() => {
expect(view.result.current.data).toEqual(null);
spy();
});
view.rerender({ disabled: false });
});
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
await testSuccessState(mappedData, view);
});
});
describe("when hook get error response", () => {
it("should set state with error data", async () => {
await testClientIsolation(client);
const mock = mockRequest(request, { status: 400 });
const view = renderUseFetch(request);
await testErrorState(mock, view);
});
it("should keep previous success state once error response is returned", async () => {
await testClientIsolation(client);
const mock = mockRequest(request);
const view = renderUseFetch(request);
await testSuccessState(mock, view);
const errorMock = mockRequest(request, { status: 400 });
act(() => {
view.result.current.refetch();
});
await testErrorState(errorMock, view, mock);
});
it("should change loading to false on error", async () => {
// Todo
});
});
describe("when dependencies change", () => {
// Solves Issue #22
it("should fetch data when disabled prop changes", async () => {
const spy = vi.fn();
await testClientIsolation(client);
const mock = mockRequest(request);
const view = renderUseFetch(request, { disabled: true });
act(() => {
view.result.current.onRequestStart(spy);
});
await sleep(20);
expect(spy).toHaveBeenCalledTimes(0);
view.rerender({ disabled: false });
await testSuccessState(mock, view);
expect(spy).toHaveBeenCalledTimes(1);
});
it("should fetch when dependencies change", async () => {
// Todo
});
});
// GitHub Issue #126
describe("when query parameters change", () => {
it("should reset loading to true when query params change and no cache exists for new key", async () => {
await testClientIsolation(client);
const paginatedRequest = createRequest({ endpoint: "/items" });
const page1Request = paginatedRequest.setQueryParams({ page: "1" } as any);
mockRequest(page1Request, { data: { items: [1, 2, 3] } });
const view = renderUseFetch(page1Request);
await waitFor(() => {
expect(view.result.current.data).toStrictEqual({ items: [1, 2, 3] });
expect(view.result.current.loading).toBe(false);
});
const page2Request = paginatedRequest.setQueryParams({ page: "2" } as any);
mockRequest(page2Request, { data: { items: [4, 5, 6] }, delay: 100 });
view.rerender({ request: page2Request });
await waitFor(() => {
expect(view.result.current.loading).toBe(true);
});
await waitFor(() => {
expect(view.result.current.data).toStrictEqual({ items: [4, 5, 6] });
expect(view.result.current.loading).toBe(false);
});
});
it("should reset loading to true on each subsequent query param change", async () => {
await testClientIsolation(client);
const paginatedRequest = createRequest({ endpoint: "/posts" });
const page1 = paginatedRequest.setQueryParams({ page: "1" } as any);
mockRequest(page1, { data: { page: 1 } });
const view = renderUseFetch(page1);
await waitFor(() => {
expect(view.result.current.data).toStrictEqual({ page: 1 });
expect(view.result.current.loading).toBe(false);
});
const page2 = paginatedRequest.setQueryParams({ page: "2" } as any);
mockRequest(page2, { data: { page: 2 }, delay: 100 });
view.rerender({ request: page2 });
await waitFor(() => {
expect(view.result.current.loading).toBe(true);
});
await waitFor(() => {
expect(view.result.current.data).toStrictEqual({ page: 2 });
expect(view.result.current.loading).toBe(false);
});
const page3 = paginatedRequest.setQueryParams({ page: "3" } as any);
mockRequest(page3, { data: { page: 3 }, delay: 100 });
view.rerender({ request: page3 });
await waitFor(() => {
expect(view.result.current.loading).toBe(true);
});
await waitFor(() => {
expect(view.result.current.data).toStrictEqual({ page: 3 });
expect(view.result.current.loading).toBe(false);
});
});
});
});