@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
365 lines (318 loc) • 12.9 kB
text/typescript
import { waitFor } from "@testing-library/dom";
import { createHttpMockingServer, sleep } from "@hyper-fetch/testing";
import { getErrorMessage } from "adapter";
import { Client } from "client";
import { xhrExtra } from "http-adapter";
const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();
describe("Request [ Sending ]", () => {
const fixture = { test: 1, data: [1, 2, 3] };
let client = new Client({ url: "shared-base-url" });
let request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" });
beforeAll(() => {
startServer();
});
beforeEach(() => {
client = new Client({ url: "shared-base-url" });
request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" });
resetMocks();
vi.resetAllMocks();
mockRequest(request, { data: fixture, delay: 40 });
});
afterAll(() => {
stopServer();
});
describe("When using request's exec method", () => {
it("should return adapter response", async () => {
const requestExecution = request.exec({});
await sleep(5);
expect(client.fetchDispatcher.getAllRunningRequests()).toHaveLength(0);
const { responseTimestamp, requestTimestamp, ...response } = await requestExecution;
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
});
it("should return mapped adapter response", async () => {
const requestExecution = request
.setResponseMapper((res) => ({ ...res, data: { nested: res.data } }) as typeof res)
.exec({});
await sleep(5);
expect(client.fetchDispatcher.getAllRunningRequests()).toHaveLength(0);
const { responseTimestamp, requestTimestamp, ...response } = await requestExecution;
expect(response).toStrictEqual({
data: { nested: fixture },
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
});
});
describe("When using request's send method", () => {
it("should return adapter response", async () => {
const { responseTimestamp, requestTimestamp, ...response } = await request.send({});
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
});
it("should return mapped adapter response", async () => {
const { responseTimestamp, requestTimestamp, ...response } = await request
.setResponseMapper((res) => ({ ...res, data: { nested: res.data } }) as typeof res)
.send({});
expect(response).toStrictEqual({
data: { nested: fixture },
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
});
it("should return async mapped adapter response", async () => {
const { responseTimestamp, requestTimestamp, ...response } = await request
.setResponseMapper(async (res) => Promise.resolve({ ...res, data: { nested: res.data } } as typeof res))
.send({});
expect(response).toStrictEqual({
data: { nested: fixture },
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
});
it("should wait to resolve request in online mode", async () => {
const spy = vi.fn();
mockRequest(request, { delay: 10, status: 400 });
const requestExecution = request.send({});
await sleep(5);
client.appManager.setOnline(false);
const unmount = client.requestManager.events.onResponseByCache(request.cacheKey, () => {
spy();
mockRequest(request, { data: fixture, delay: 40 });
client.appManager.setOnline(true);
unmount();
});
const { responseTimestamp, requestTimestamp, ...response } = await requestExecution;
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
expect(spy).toHaveBeenCalledTimes(1);
});
it("should wait to resolve request retries", async () => {
const spy = vi.fn();
mockRequest(request, { delay: 10, status: 400 });
const requestExecution = request.setRetry(1).setRetryTime(30).send({});
await sleep(5);
const unmount = client.requestManager.events.onResponseByCache(request.cacheKey, () => {
spy();
mockRequest(request, { data: fixture, delay: 40 });
unmount();
});
const { responseTimestamp, requestTimestamp, ...response } = await requestExecution;
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
expect(spy).toHaveBeenCalledTimes(1);
});
it("should return error once request got removed", async () => {
mockRequest(request, { delay: 10, status: 400 });
const requestExecution = request.send({});
await sleep(2);
const runningRequests = client.fetchDispatcher.getAllRunningRequests();
client.fetchDispatcher.delete(request.queryKey, runningRequests[0].requestId, request.abortKey);
const { responseTimestamp, requestTimestamp, ...response } = await requestExecution;
expect(response).toStrictEqual({
data: null,
error: getErrorMessage("deleted"),
status: null,
success: false,
extra: xhrExtra,
});
});
it("should call remove error", async () => {
const spy = vi.fn();
mockRequest(request, { delay: 10, status: 400 });
const requestExecution = request.send({ onRemove: spy });
await sleep(2);
const runningRequests = client.fetchDispatcher.getAllRunningRequests();
client.fetchDispatcher.delete(request.queryKey, runningRequests[0].requestId, request.abortKey);
await requestExecution;
expect(spy).toHaveBeenCalledTimes(1);
});
it("should return cancel error", async () => {
const newRequest = client
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setCancelable(true);
const mock = mockRequest(newRequest);
const [
{ requestTimestamp: time1, responseTimestamp: time2, ...res1 },
{ requestTimestamp: time3, responseTimestamp: time4, ...res2 },
{ requestTimestamp: time5, responseTimestamp: time6, ...res3 },
] = await Promise.all([newRequest.send({}), newRequest.send({}), newRequest.send({})]);
expect(res1).toStrictEqual({
data: null,
error: getErrorMessage("abort"),
status: 0,
success: false,
extra: xhrExtra,
});
expect(res2).toStrictEqual({
data: null,
error: getErrorMessage("abort"),
status: 0,
success: false,
extra: xhrExtra,
});
expect(res3).toStrictEqual({
data: mock,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "2" } },
});
});
/**
* Solved #40 https://github.com/BetterTyped/hyper-fetch/issues/40
*/
it("should return cancel error for cancelable requests", async () => {
const getUsers = client.createRequest()({
method: "GET",
endpoint: "/users",
cancelable: true,
});
const mock = mockRequest(getUsers);
const [
{ requestTimestamp: time1, responseTimestamp: time2, ...res1 },
{ requestTimestamp: time3, responseTimestamp: time4, ...res2 },
{ requestTimestamp: time5, responseTimestamp: time6, ...res3 },
] = await Promise.all([getUsers.send({}), getUsers.send({}), getUsers.send({})]);
expect(res1).toStrictEqual({
data: null,
error: getErrorMessage("abort"),
status: 0,
success: false,
extra: xhrExtra,
});
expect(res2).toStrictEqual({
data: null,
error: getErrorMessage("abort"),
status: 0,
success: false,
extra: xhrExtra,
});
expect(res3).toStrictEqual({
data: mock,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "2" } },
});
});
it("should allow to call the request callbacks", async () => {
const spy1 = vi.fn();
const spy2 = vi.fn();
const spy3 = vi.fn();
const spy4 = vi.fn();
const spy5 = vi.fn();
const spy6 = vi.fn();
await request.send({
onBeforeSent: spy1,
onRequestStart: spy2,
onResponseStart: spy3,
onUploadProgress: spy4,
onDownloadProgress: spy5,
onResponse: spy6,
});
expect(spy1).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledTimes(1);
expect(spy3).toHaveBeenCalledTimes(1);
expect(spy4.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(spy5.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(spy6).toHaveBeenCalledTimes(1);
});
describe("cachePolicy", () => {
it("cache-first uses read() and does not call adapter when cache is primed", async () => {
const fetchSpy = vi.fn(() => ({ data: fixture, status: 200 }));
const r = request.setMock(fetchSpy);
await r.send({});
fetchSpy.mockClear();
const { responseTimestamp, requestTimestamp, ...response } = await r.send({ cachePolicy: "cache-first" });
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: xhrExtra,
});
expect(fetchSpy).not.toHaveBeenCalled();
});
it("cache-first fetches when cache is empty", async () => {
const fetchSpy = vi.fn(() => ({ data: fixture, status: 200 }));
const r = request.setMock(fetchSpy);
client.cache.clear();
await r.send({ cachePolicy: "cache-first" });
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
it("revalidate resolves with cache immediately and refreshes in the background", async () => {
const fetchSpy = vi.fn(() => ({ data: fixture, status: 200 }));
const r = request.setMock(fetchSpy);
await r.send({});
fetchSpy.mockClear();
fetchSpy.mockImplementation(() => ({ data: { refreshed: true } as any, status: 200 }));
const { responseTimestamp, requestTimestamp, ...immediate } = await r.send({ cachePolicy: "revalidate" });
expect(immediate.data).toStrictEqual(fixture);
await waitFor(() => {
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
expect(r.read()?.data).toStrictEqual({ refreshed: true });
});
});
it("revalidate awaits network when cache is empty", async () => {
const fetchSpy = vi.fn(() => ({ data: fixture, status: 200 }));
const r = request.setMock(fetchSpy);
client.cache.clear();
const { responseTimestamp, requestTimestamp, ...response } = await r.send({ cachePolicy: "revalidate" });
expect(response.data).toStrictEqual(fixture);
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
it("network-only matches default send", async () => {
const { responseTimestamp, requestTimestamp, ...response } = await request.send({
cachePolicy: "network-only",
});
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "25" } },
});
});
it("cache-first applies response mapper without calling adapter", async () => {
const fetchSpy = vi.fn(() => ({ data: fixture, status: 200 }));
const r = request
.setResponseMapper((res) => ({ ...res, data: { nested: res.data } }) as typeof res)
.setMock(fetchSpy);
await r.send({});
fetchSpy.mockClear();
const { responseTimestamp, requestTimestamp, ...response } = await r.send({ cachePolicy: "cache-first" });
expect(response.data).toStrictEqual({ nested: fixture });
expect(fetchSpy).not.toHaveBeenCalled();
});
});
});
});