@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
1,026 lines (951 loc) • 37.3 kB
text/typescript
import { createHttpMockingServer, sleep } from "@hyper-fetch/testing";
import { Plugin } from "plugin";
import type { ResponseType, ResponseErrorType } from "adapter";
import { getAdapterBindings, getErrorMessage } from "adapter";
import { testProgressSpy } from "../../shared";
import { Client } from "client";
import type { RequestInstance } from "request";
import type { FetchAdapterOptionsType, HttpAdapterType } from "http-adapter";
import { xhrExtra } from "http-adapter";
const { resetMocks, startServer, stopServer } = createHttpMockingServer();
describe("Adapter [ Bindings ]", () => {
const url = "http://localhost:9000";
const endpoint = "/endpoint";
const requestId = "test";
const queryParams = "?query=params";
const data = { value: 1 };
const successResponse: Omit<
ResponseType<unknown, unknown, HttpAdapterType>,
"requestTimestamp" | "responseTimestamp"
> = {
data,
error: null,
success: true,
status: 200,
extra: xhrExtra,
};
const errorResponse: Omit<
ResponseType<unknown, unknown, HttpAdapterType>,
"requestTimestamp" | "responseTimestamp"
> = {
data: null,
error: data,
success: false,
status: 400,
extra: xhrExtra,
};
const requestConfig: FetchAdapterOptionsType = { timeout: 1000 };
const onTriggerSpy = vi.fn();
const onErrorSpy = vi.fn();
const onFinishedSpy = vi.fn();
const onStartSpy = vi.fn();
const onSuccessSpy = vi.fn();
const initializeSetup = () => {
const effect = new Plugin({
name: "test",
})
.onRequestTrigger(onTriggerSpy)
.onRequestError(onErrorSpy)
.onRequestFinished(onFinishedSpy)
.onRequestStart(onStartSpy)
.onRequestSuccess(onSuccessSpy);
const client = new Client({ url }).addPlugin(effect);
const request = client
.createRequest<{
response: any;
payload: { value: number };
queryParams: string;
}>()({ endpoint, options: requestConfig })
.setPayload(data)
.setQueryParams(queryParams);
return { request, client, effect };
};
let setup = initializeSetup();
const { client, request } = setup;
beforeAll(() => {
startServer();
});
beforeEach(() => {
setup = initializeSetup();
resetMocks();
vi.resetAllMocks();
});
afterAll(() => {
stopServer();
});
describe("when getAdapterBindings get initialized", () => {
it("should create correct fullUrl", async () => {
const {
url: u,
endpoint: e,
queryParams: q,
} = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(u).toBe(url);
expect(e).toBe(endpoint);
expect(q).toBe(queryParams);
});
it("should create correct headers", async () => {
const { headers } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(headers).toEqual({ "Content-Type": "application/json" });
});
it("should create correct payload", async () => {
const { payload } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(payload).toEqual(JSON.stringify(data));
});
it("should create AbortController", async () => {
request.client.requestManager.addAbortController(request.abortKey, requestId);
const { getAbortController } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(getAbortController()).toBeDefined();
});
});
describe("given bindings abort methods being used", () => {
describe("when using AbortController methods", () => {
it("should create listener using createAbortListener", async () => {
const resolveSpy = vi.fn();
const { createAbortListener, getAbortController } = await getAdapterBindings({
request,
requestId,
resolve: resolveSpy,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
request.client.requestManager.addAbortController(request.abortKey, requestId);
const controller = getAbortController();
const unmount = createAbortListener({ status: 0, extra: xhrExtra, onAbort: spy });
expect(spy).toHaveBeenCalledTimes(0);
expect(resolveSpy).toHaveBeenCalledTimes(0);
controller?.abort();
await sleep(1);
expect(spy).toHaveBeenCalledTimes(1);
expect(resolveSpy).toHaveBeenCalledTimes(1);
unmount();
});
it("should unmount listener using createAbortListener", async () => {
request.client.requestManager.addAbortController(request.abortKey, requestId);
const { createAbortListener, getAbortController } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const resolveSpy = vi.fn();
const controller = getAbortController();
const unmount = createAbortListener({ status: 0, extra: xhrExtra, onAbort: spy });
expect(spy).toHaveBeenCalledTimes(0);
expect(resolveSpy).toHaveBeenCalledTimes(0);
unmount();
controller?.abort();
expect(spy).toHaveBeenCalledTimes(0);
expect(resolveSpy).toHaveBeenCalledTimes(0);
});
it("should throw createAbortListener when there is no controller", async () => {
const { createAbortListener } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
client.requestManager.abortControllers.clear();
expect(() => createAbortListener({ status: 0, extra: xhrExtra, onAbort: () => null })).toThrow();
});
it("should call onAbort when signal is aborted", async () => {
request.client.requestManager.addAbortController(request.abortKey, requestId);
const abortController = request.client.requestManager.getAbortController(request.abortKey, requestId);
abortController?.abort();
const spy = vi.fn();
const { createAbortListener } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
createAbortListener({ status: 0, extra: xhrExtra, onAbort: spy });
expect(spy).toHaveBeenCalledTimes(1);
});
it("should work with default onAbort handler", async () => {
request.client.requestManager.addAbortController(request.abortKey, requestId);
const { createAbortListener, getAbortController } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const resolveSpy = vi.fn();
const controller = getAbortController();
const unmount = createAbortListener({ status: 0, extra: xhrExtra }); // No onAbort provided
expect(resolveSpy).toHaveBeenCalledTimes(0);
controller?.abort();
await sleep(1);
// Should not throw and complete successfully
expect(() => unmount()).not.toThrow();
});
});
});
describe("given bindings pre-request methods being used", () => {
describe("when onBeforeRequest got executed", () => {
it("should use effect lifecycle method ", async () => {
const { onBeforeRequest } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
onBeforeRequest();
expect(onTriggerSpy).toHaveBeenCalledTimes(1);
});
});
});
// Request
describe("given bindings request methods being used", () => {
it("should allow for setting data mapper", async () => {
const req = client.createRequest<{
response: any;
payload: { role: string; userId: number };
error: Error;
queryParams: { userId: number; role: string };
}>()({
endpoint: "shared-endpoint/",
});
const spy = vi.fn();
const newData = {
userId: 11,
role: "ADMIN",
};
const requestMapped = req.setPayloadMapper<string>((value) => {
const { role, userId } = value;
spy();
return `${userId}_${role}`;
});
const requestSetData = requestMapped.setPayload(newData);
const { payload } = await getAdapterBindings({
request: requestSetData,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(spy).toHaveBeenCalledTimes(1);
expect(payload).toBe(`${newData.userId}_${newData.role}`);
});
it("should allow for setting request mapper", async () => {
const req = client.createRequest<{
response: any;
payload: Record<string, unknown>;
error: any;
queryParams: { userId: number; role: string };
}>()({
endpoint: "shared-endpoint/",
});
const spy = vi.fn();
const newData = {
userId: 11,
role: "ADMIN",
};
const requestMapped = req.setRequestMapper<RequestInstance>((r) => {
spy();
// TODO figure out something to change type inside of mapper
return (r as RequestInstance).setPayload(`${newData.userId}_${newData.role}`);
});
const requestSetData = requestMapped.setPayload(newData);
const { payload } = await getAdapterBindings({
request: requestSetData,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(spy).toHaveBeenCalledTimes(1);
expect(payload).toBe(`"${newData.userId}_${newData.role}"`);
});
describe("when onRequestStart got executed", () => {
it("should use effect lifecycle method", async () => {
const { onRequestStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
onRequestStart();
expect(onStartSpy).toHaveBeenCalledTimes(1);
});
it("should emit request start event", async () => {
const { onRequestStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const unmount = client.requestManager.events.onRequestStartByQueue(request.queryKey, spy);
onRequestStart();
unmount();
expect(spy).toHaveBeenCalledTimes(1);
});
it("should emit upload progress event", async () => {
const { onRequestStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const unmount = client.requestManager.events.onUploadProgressByQueue(request.queryKey, spy);
const startTimestamp = onRequestStart();
unmount();
testProgressSpy({ spy, request, requestId, startTimestamp });
});
it("should emit upload progress event with specified payload size", async () => {
const { onRequestStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const progress = {
total: 9999,
loaded: 0,
};
const spy = vi.fn();
const unmount = client.requestManager.events.onUploadProgressByQueue(request.queryKey, spy);
const startTimestamp = onRequestStart(progress);
unmount();
testProgressSpy({ ...progress, spy, request, requestId, startTimestamp });
});
});
describe("when onRequestProgress got executed", () => {
const progress = {
total: 9999,
loaded: 30,
};
it("should emit upload progress event", async () => {
const { onRequestStart, onRequestProgress } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const startTimestamp = onRequestStart();
await sleep(30);
const unmount = client.requestManager.events.onUploadProgressByQueue(request.queryKey, spy);
const progressTimestamp = onRequestProgress(progress);
unmount();
testProgressSpy({ ...progress, spy, request, requestId, startTimestamp, progressTimestamp });
});
it("should create start timestamp if it's not available", async () => {
const { onRequestProgress, getRequestStartTimestamp } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(getRequestStartTimestamp()).toBeNull();
const progressTimestamp = onRequestProgress(progress);
expect(progressTimestamp).toBeNumber();
});
it("should emit correct loaded value", async () => {
const { onRequestProgress } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
let value: number | undefined;
const unmount = client.requestManager.events.onUploadProgressByQueue(request.queryKey, ({ loaded }) => {
value = loaded;
});
onRequestProgress({});
expect(value).toBe(0);
unmount();
});
it("should emit correct loaded value", async () => {
const { onResponseProgress } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
let value: number | undefined;
const unmount = client.requestManager.events.onDownloadProgressByQueue(request.queryKey, ({ loaded }) => {
value = loaded;
});
onResponseProgress({});
expect(value).toBe(0);
unmount();
});
});
describe("when onRequestEnd got executed", () => {
const progress = {
total: 9999,
loaded: 9999,
};
it("should emit upload progress event", async () => {
const { onRequestStart, onRequestEnd } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const startTimestamp = onRequestStart({ total: progress.total, loaded: 0 });
await sleep(30);
const unmount = client.requestManager.events.onUploadProgressByQueue(request.queryKey, spy);
const progressTimestamp = onRequestEnd();
unmount();
testProgressSpy({ ...progress, spy, request, requestId, startTimestamp, progressTimestamp, timeLeft: 0 });
});
it("should create start timestamp if it's not available", async () => {
const { onRequestEnd, getRequestStartTimestamp } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(getRequestStartTimestamp()).toBeNull();
const progressTimestamp = onRequestEnd();
expect(progressTimestamp).toBeNumber();
});
});
describe("when handling payload mapping", () => {
it("should use adapter's unstable_payloadMapper if no request mapper exists", async () => {
const mappedPayload = "mapped_by_adapter";
const spy = vi.fn().mockReturnValue(mappedPayload);
client.adapter.unstable_payloadMapper = spy;
const { payload } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(spy).toHaveBeenCalledWith(data);
expect(payload).toBe(mappedPayload);
});
it("should prioritize request's unstable_payloadMapper over adapter's mapper", async () => {
const adapterMappedPayload = "mapped_by_adapter";
const requestMappedPayload = "mapped_by_request";
const adapterSpy = vi.fn().mockReturnValue(adapterMappedPayload);
const requestSpy = vi.fn().mockReturnValue(requestMappedPayload);
client.adapter.unstable_payloadMapper = adapterSpy;
request.unstable_payloadMapper = requestSpy;
const { payload } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(requestSpy).toHaveBeenCalledWith(data);
expect(adapterSpy).not.toHaveBeenCalled();
expect(payload).toBe(requestMappedPayload);
});
it("should use raw payload if no mappers exist", async () => {
const testData = { test: "raw_data" };
const testRequest = new Client({ url: "test" })
.createRequest<{ payload: { test: string } }>()({ endpoint: "shared-endpoint/" })
.setPayload(testData);
const { payload } = await getAdapterBindings({
request: testRequest,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(payload).toBe(JSON.stringify(testData));
});
it("should skip adapter's unstable_payloadMapper when it is undefined", async () => {
request.unstable_payloadMapper = undefined;
client.adapter.unstable_payloadMapper = undefined as any;
const { payload } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
expect(payload).toBe(payload);
});
});
});
// Response
describe("given bindings response methods being used", () => {
describe("when onResponseStart got executed", () => {
it("should emit response start event", async () => {
const { onResponseStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const unmount = client.requestManager.events.onResponseStartByQueue(request.queryKey, spy);
onResponseStart();
unmount();
expect(spy).toHaveBeenCalledTimes(1);
});
it("should emit download progress event", async () => {
const { onResponseStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const unmount = client.requestManager.events.onDownloadProgressByQueue(request.queryKey, spy);
const startTimestamp = onResponseStart();
unmount();
testProgressSpy({ spy, request, requestId, startTimestamp });
});
it("should emit download progress event with specified payload size", async () => {
const { onResponseStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const progress = {
total: 9999,
loaded: 0,
};
const spy = vi.fn();
const unmount = client.requestManager.events.onDownloadProgressByQueue(request.queryKey, spy);
const startTimestamp = onResponseStart(progress);
unmount();
testProgressSpy({ ...progress, spy, request, requestId, startTimestamp });
});
it("should emit correct total value", async () => {
const { onResponseStart } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
let value: number | undefined;
const unmount = client.requestManager.events.onDownloadProgressByQueue(request.queryKey, ({ total }) => {
value = total;
});
onResponseStart();
expect(value).toBe(1);
onResponseStart({ total: 0, loaded: 7 });
expect(value).toBe(7);
onResponseStart({ total: 10, loaded: 0 });
expect(value).toBe(10);
// If we have previous value stored it needs to take the biggest value
onResponseStart({ total: 8, loaded: 0 });
expect(value).toBe(10);
unmount();
});
});
describe("when onResponseProgress got executed", () => {
const progress = {
total: 9999,
loaded: 30,
};
it("should emit upload progress event", async () => {
const { onResponseStart, onResponseProgress } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const startTimestamp = onResponseStart();
await sleep(30);
const unmount = client.requestManager.events.onDownloadProgressByQueue(request.queryKey, spy);
const progressTimestamp = onResponseProgress(progress);
unmount();
testProgressSpy({ ...progress, spy, request, requestId, startTimestamp, progressTimestamp });
});
it("should create start timestamp if it's not available", async () => {
const { onResponseProgress, getResponseStartTimestamp } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const startTimestamp = getResponseStartTimestamp();
expect(startTimestamp).toBeNull();
const progressTimestamp = onResponseProgress(progress);
expect(progressTimestamp).toBeNumber();
});
});
describe("when onResponseEnd got executed", () => {
const progress = {
total: 9999,
loaded: 9999,
};
it("should emit upload progress event", async () => {
const { onResponseStart, onResponseEnd } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const startTimestamp = onResponseStart({ total: progress.total, loaded: 0 });
await sleep(30);
const unmount = client.requestManager.events.onDownloadProgressByQueue(request.queryKey, spy);
const progressTimestamp = onResponseEnd();
unmount();
testProgressSpy({ ...progress, spy, request, requestId, startTimestamp, progressTimestamp, timeLeft: 0 });
});
it("should create start timestamp if it's not available", async () => {
const { onResponseEnd, getResponseStartTimestamp } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const startTimestamp = getResponseStartTimestamp();
expect(startTimestamp).toBeNull();
const progressTimestamp = onResponseEnd();
expect(progressTimestamp).toBeNumber();
});
});
});
// Data handlers
describe("given bindings data handling methods being used", () => {
describe("when onSuccess got executed", () => {
it("should return success data", async () => {
const { onSuccess } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const { responseTimestamp, requestTimestamp, ...response } = await onSuccess({
data,
status: 200,
extra: xhrExtra,
});
expect(response).toEqual(expect.objectContaining(successResponse));
expect(requestTimestamp).toBeNumber();
expect(responseTimestamp).toBeGreaterThanOrEqual(requestTimestamp);
});
it("should use effect lifecycle methods", async () => {
const { onSuccess } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
await onSuccess({ data, status: 200, extra: xhrExtra });
expect(onSuccessSpy).toHaveBeenCalledTimes(1);
expect(onFinishedSpy).toHaveBeenCalledTimes(1);
expect(onSuccessSpy).toHaveBeenCalledWith({ response: expect.objectContaining(successResponse), request });
expect(onFinishedSpy).toHaveBeenCalledWith({ response: expect.objectContaining(successResponse), request });
});
it("should return data transformed by __modifyResponse", async () => {
const { onSuccess } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
client.unstable_onResponseCallbacks.push(() => ({
...successResponse,
responseTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
}));
const response = await onSuccess({ data, status: 200, extra: xhrExtra });
client.unstable_onResponseCallbacks = [];
expect(response).toEqual(expect.objectContaining(successResponse));
});
it("should return data transformed by __modifySuccessResponse", async () => {
const { onSuccess } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
client.unstable_onSuccessCallbacks.push(() => ({
...successResponse,
responseTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
}));
const response = await onSuccess({ data, status: 200, extra: xhrExtra });
client.unstable_onSuccessCallbacks = [];
expect(response).toEqual(expect.objectContaining(successResponse));
});
it("should execute __modifySuccessResponse as last modifier", async () => {
const { onSuccess } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const newData: ResponseType<unknown, unknown, HttpAdapterType> = {
data: "modified",
error: null,
success: true,
status: 222,
extra: xhrExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
};
client.unstable_onResponseCallbacks.push(() => ({
...errorResponse,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
}));
client.unstable_onSuccessCallbacks.push(() => newData);
const response = await onSuccess({ data, status: 200, extra: xhrExtra });
client.unstable_onResponseCallbacks = [];
client.unstable_onSuccessCallbacks = [];
expect(response).toEqual(expect.objectContaining(newData));
expect(onSuccessSpy).toHaveBeenCalledWith({ response: expect.objectContaining(newData), request });
expect(onFinishedSpy).toHaveBeenCalledWith({ response: expect.objectContaining(newData), request });
});
});
describe("when onError got executed", () => {
it("should return error data", async () => {
const { onError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const { requestTimestamp, responseTimestamp, ...response } = await onError({
error: data,
status: 400,
extra: xhrExtra,
});
expect(response).toEqual(errorResponse);
expect(requestTimestamp).toBeNumber();
expect(responseTimestamp).toBeGreaterThanOrEqual(requestTimestamp);
});
it("should use effect lifecycle methods", async () => {
const { onError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
await onError({ error: data, status: 400, extra: xhrExtra });
expect(onErrorSpy).toHaveBeenCalledTimes(1);
expect(onFinishedSpy).toHaveBeenCalledTimes(1);
expect(onErrorSpy).toHaveBeenCalledWith({ response: expect.objectContaining(errorResponse), request });
expect(onFinishedSpy).toHaveBeenCalledWith({ response: expect.objectContaining(errorResponse), request });
});
it("should return data transformed by __modifyResponse", async () => {
const { onError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
client.unstable_onResponseCallbacks.push(() => ({
...errorResponse,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
}));
const response = await onError({ error: data, status: 400, extra: xhrExtra });
client.unstable_onResponseCallbacks = [];
expect(response).toEqual(expect.objectContaining(errorResponse));
});
it("should return data transformed by unstable_modifyErrorResponse", async () => {
const { onError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
client.unstable_onErrorCallbacks.push(() => ({
...errorResponse,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
}));
const response = await onError({ error: data, status: 400, extra: xhrExtra });
client.unstable_onErrorCallbacks = [];
expect(response).toEqual(expect.objectContaining(errorResponse));
});
it("should execute unstable_modifyErrorResponse as last modifier", async () => {
const { onError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const newData: ResponseType<unknown, unknown, HttpAdapterType> = {
data: "modified",
error: 444,
success: false,
status: null,
extra: xhrExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
};
client.unstable_onResponseCallbacks.push(() => ({
...successResponse,
responseTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
}));
client.unstable_onErrorCallbacks.push(() => newData);
const response = await onError({ error: data, status: 400, extra: xhrExtra });
client.unstable_onErrorCallbacks = [];
client.unstable_onResponseCallbacks = [];
expect(response).toEqual(newData);
expect(onErrorSpy).toHaveBeenCalledWith({ response: newData, request });
expect(onFinishedSpy).toHaveBeenCalledWith({ response: newData, request });
});
});
describe("when errors methods got executed", () => {
it("should return correct message when onAbortError is executed", async () => {
const { onAbortError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const response = await onAbortError({ status: 0, extra: xhrExtra });
expect(response).toEqual({
data: null,
error: getErrorMessage("abort"),
status: 0,
success: false,
extra: xhrExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
} satisfies ResponseErrorType<any, HttpAdapterType>);
});
it("should return correct message when onTimeoutError is executed", async () => {
const { onTimeoutError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const response = await onTimeoutError({ status: 0, extra: xhrExtra });
expect(response).toEqual({
data: null,
error: getErrorMessage("timeout"),
status: 0,
success: false,
extra: xhrExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
} satisfies ResponseErrorType<any, HttpAdapterType>);
});
it("should return correct message when onUnexpectedError is executed", async () => {
const { onUnexpectedError } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const response = await onUnexpectedError({ status: 0, extra: xhrExtra });
expect(response).toEqual({
data: null,
error: getErrorMessage(),
status: 0,
success: false,
extra: xhrExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
} satisfies ResponseErrorType<any, HttpAdapterType>);
});
});
});
describe("when handling progress updates", () => {
it("should skip upload progress event when previousRequestTotal is 100", async () => {
const { onRequestProgress } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const unmount = client.requestManager.events.onUploadProgressByQueue(request.queryKey, spy);
// First call to set previousRequestTotal to 100
onRequestProgress({ total: 100, loaded: 100 });
expect(spy).toHaveBeenCalledTimes(1);
// Second call should not emit event
spy.mockClear();
onRequestProgress({ total: 100, loaded: 100 });
expect(spy).not.toHaveBeenCalled();
unmount();
});
it("should skip download progress event when previousResponseTotal is 100", async () => {
const { onResponseProgress } = await getAdapterBindings({
request,
requestId,
resolve: () => null,
onStartTime: () => null,
internalErrorMapping: (error) => error,
});
const spy = vi.fn();
const unmount = client.requestManager.events.onDownloadProgressByQueue(request.queryKey, spy);
// First call to set previousResponseTotal to 100
onResponseProgress({ total: 100, loaded: 100 });
expect(spy).toHaveBeenCalledTimes(1);
// Second call should not emit event
spy.mockClear();
onResponseProgress({ total: 100, loaded: 100 });
expect(spy).not.toHaveBeenCalled();
unmount();
});
});
});