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, "requestTimestamp" | "responseTimestamp" > = { data, error: null, success: true, status: 200, extra: xhrExtra, }; const errorResponse: Omit< ResponseType, "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((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; error: any; queryParams: { userId: number; role: string }; }>()({ endpoint: "shared-endpoint/", }); const spy = vi.fn(); const newData = { userId: 11, role: "ADMIN", }; const requestMapped = req.setRequestMapper((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 = { 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 = { 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); }); 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); }); 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); }); }); }); 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(); }); }); });