@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
310 lines (270 loc) • 11.2 kB
text/typescript
import { waitFor } from "@testing-library/dom";
import { createHttpMockingServer, sleep } from "@hyper-fetch/testing";
import type { AdapterInstance, ResponseType } from "adapter";
import { getErrorMessage } from "adapter";
import type { ResponseDetailsType } from "managers";
import { createDispatcher, createAdapter } from "../../utils";
import { Client } from "client";
import { xhrExtra } from "http-adapter";
const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();
describe("Dispatcher [ Events ]", () => {
const adapterSpy = vi.fn();
let adapter = createAdapter({ callback: adapterSpy });
let client = new Client({ url: "shared-base-url" }).setAdapter(adapter);
let request = client.createRequest()({ endpoint: "shared-base-endpoint" });
let dispatcher = createDispatcher(client);
beforeAll(() => {
startServer();
});
beforeEach(() => {
resetMocks();
vi.resetAllMocks();
adapter = createAdapter({ callback: adapterSpy });
client = new Client({ url: "shared-base-url" }).setAdapter(adapter);
request = client.createRequest()({ endpoint: "shared-base-endpoint" });
dispatcher = createDispatcher(client);
mockRequest(request, { status: 200, delay: 0 });
});
afterAll(() => {
stopServer();
});
describe("When using dispatcher events", () => {
it("should emit loading event", async () => {
const spy = vi.fn();
const unmount = client.requestManager.events.onLoadingByQueue(request.queryKey, spy);
dispatcher.add(request);
expect(spy).toHaveBeenCalledTimes(1);
unmount();
dispatcher.add(request);
expect(spy).toHaveBeenCalledTimes(1);
});
it("should emit drained event", async () => {
const spy = vi.fn();
const unmount = dispatcher.events.onDrainedByKey(request.queryKey, spy);
const requestId = dispatcher.add(request.setQueued(true));
dispatcher.delete(request.queryKey, requestId, request.abortKey);
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
unmount();
const requestId2 = dispatcher.add(request.setQueued(true));
dispatcher.delete(request.queryKey, requestId2, request.abortKey);
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
});
it("should emit queue status change event", async () => {
const spy = vi.fn();
const unmount = dispatcher.events.onQueueStatusChangeByKey(request.queryKey, spy);
dispatcher.stop(request.queryKey);
expect(spy).toHaveBeenCalledTimes(1);
unmount();
dispatcher.stop(request.queryKey);
expect(spy).toHaveBeenCalledTimes(1);
});
it("should emit queue change event", async () => {
const spy = vi.fn();
const unmount = dispatcher.events.onQueueChangeByKey(request.queryKey, spy);
dispatcher.add(request);
expect(spy).toHaveBeenCalledTimes(1);
unmount();
dispatcher.add(request);
expect(spy).toHaveBeenCalledTimes(1);
});
it("should handle listen change events correctly", async () => {
const spy = vi.fn();
const unmount = dispatcher.emitter.onListener(request.queryKey, spy);
const listener = () => {};
// Should be called when a new listener is added
dispatcher.emitter.on(request.queryKey, listener);
expect(dispatcher.emitter.listeners(request.queryKey).length).toBe(1);
expect(spy).toHaveBeenCalledWith(1);
dispatcher.emitter.off(request.queryKey, listener);
expect(dispatcher.emitter.listeners(request.queryKey).length).toBe(0);
expect(spy).toHaveBeenCalledWith(0);
// Should be called when a listener is removed
unmount();
dispatcher.emitter.addListener(request.queryKey, listener);
expect(dispatcher.emitter.listeners(request.queryKey).length).toBe(1);
expect(spy).toHaveBeenCalledTimes(2);
});
it("should emit proper data response", async () => {
let response: [ResponseType<unknown, unknown, AdapterInstance>, ResponseDetailsType];
const mock = mockRequest(request);
client.requestManager.events.onResponseByCache(request.cacheKey, (data) => {
response = [data.response, data.details];
});
dispatcher.add(request);
const adapterResponse: ResponseType<unknown, unknown, AdapterInstance> = {
data: mock,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "2" } },
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
};
const responseDetails: Omit<ResponseDetailsType, "timestamp"> = {
retries: 0,
isCanceled: false,
isOffline: false,
willRetry: false,
triggerTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
addedTimestamp: expect.toBeNumber(),
};
await waitFor(() => {
expect(response).toEqual([adapterResponse, responseDetails]);
});
});
it("should emit proper failed response", async () => {
let response: [ResponseType<unknown, unknown, AdapterInstance>, ResponseDetailsType];
const mock = mockRequest(request, { status: 400 });
client.requestManager.events.onResponseByCache(request.cacheKey, (data) => {
response = [data.response, data.details];
});
dispatcher.add(request);
const adapterResponse: ResponseType<unknown, unknown, AdapterInstance> = {
data: null,
error: mock,
status: 400,
success: false,
extra: { headers: { "content-type": "application/json", "content-length": "19" } },
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
};
const responseDetails: Omit<ResponseDetailsType, "timestamp"> = {
retries: 0,
isCanceled: false,
isOffline: false,
willRetry: false,
triggerTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
addedTimestamp: expect.toBeNumber(),
};
await waitFor(() => {
expect(response).toEqual([adapterResponse, responseDetails]);
});
});
it("should emit proper retry response", async () => {
let response: [ResponseType<unknown, unknown, AdapterInstance>, ResponseDetailsType];
const requestWithRetry = request.setRetry(1).setRetryTime(50);
mockRequest(requestWithRetry, { status: 400, delay: 0 });
client.requestManager.events.onResponseByCache(requestWithRetry.cacheKey, (data) => {
response = [data.response, data.details];
});
dispatcher.add(requestWithRetry);
await waitFor(() => {
expect(response).toBeDefined();
});
const mock = mockRequest(requestWithRetry);
const adapterResponse: ResponseType<unknown, unknown, AdapterInstance> = {
data: mock,
error: null,
status: 200,
success: true,
extra: { headers: { "content-type": "application/json", "content-length": "2" } },
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
};
const responseDetails: Omit<ResponseDetailsType, "timestamp"> = {
retries: 1,
isCanceled: false,
isOffline: false,
willRetry: false,
triggerTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
addedTimestamp: expect.toBeNumber(),
};
await waitFor(() => {
expect(response).toEqual([adapterResponse, responseDetails]);
});
});
it("should emit proper cancel response", async () => {
let response: [ResponseType<unknown, unknown, AdapterInstance>, ResponseDetailsType];
mockRequest(request, { status: 400 });
client.requestManager.events.onResponseByCache(request.cacheKey, (data) => {
response = [data.response, data.details];
});
dispatcher.add(request);
await sleep(1);
client.requestManager.abortAll();
const adapterResponse: ResponseType<unknown, unknown, AdapterInstance> = {
data: null,
error: getErrorMessage("abort"),
status: 0,
success: false,
extra: xhrExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
};
const responseDetails: Omit<ResponseDetailsType, "timestamp"> = {
retries: 0,
isCanceled: true,
isOffline: false,
willRetry: false,
addedTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
triggerTimestamp: expect.toBeNumber(),
};
await waitFor(() => {
expect(response).toEqual([adapterResponse, responseDetails]);
});
});
it("should emit and unsubscribe from global drained event", async () => {
const spy = vi.fn();
const unmount = dispatcher.events.onDrained(spy);
// Add and complete multiple requests to trigger drain
const request1 = dispatcher.add(request.setQueued(true));
const request2 = dispatcher.add(request.setQueued(true));
dispatcher.delete(request.queryKey, request1, request.abortKey);
dispatcher.delete(request.queryKey, request2, request.abortKey);
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
// Test unsubscribe
unmount();
const request3 = dispatcher.add(request.setQueued(true));
dispatcher.delete(request.queryKey, request3, request.abortKey);
await sleep(10); // Give time for potential unwanted events
expect(spy).toHaveBeenCalledTimes(1); // Should not be called again
});
it("should emit and unsubscribe from global queue status change event", async () => {
const spy = vi.fn();
const unmount = dispatcher.events.onQueueStatusChange(spy);
// Trigger queue status change
dispatcher.stop(request.queryKey);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({ stopped: true, requests: [], queryKey: request.queryKey }),
);
// Test unsubscribe
unmount();
// Should not trigger spy after unmount
dispatcher.stop(request.queryKey);
expect(spy).toHaveBeenCalledTimes(1);
});
it("should emit and unsubscribe from global queue change event", async () => {
const spy = vi.fn();
const unmount = dispatcher.events.onQueueChange(spy);
// Add request to trigger queue change
dispatcher.add(request);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
requests: expect.any(Array),
queryKey: request.queryKey,
}),
);
// Test unsubscribe
unmount();
// Should not trigger spy after unmount
dispatcher.add(request);
expect(spy).toHaveBeenCalledTimes(1);
});
});
});