@hyper-fetch/react
Version:
React hooks and utils for the hyper-fetch
482 lines (393 loc) • 12.7 kB
text/typescript
import { renderHook, act, waitFor } from "@testing-library/react";
import { useStream } from "hooks/use-stream";
import { client } from "../../utils";
const createStreamRequest = () => {
return client.createRequest<{ response: any }>()({
endpoint: "/stream-endpoint" as any,
method: "GET",
});
};
const createReadableStream = (chunks: string[]) => {
const encoder = new TextEncoder();
let index = 0;
return new ReadableStream<Uint8Array>({
pull(controller) {
if (index < chunks.length) {
controller.enqueue(encoder.encode(chunks[index]));
index += 1;
} else {
controller.close();
}
},
});
};
const createFailingReadableStream = (errorMessage: string) => {
return new ReadableStream<Uint8Array>({
start(controller) {
controller.error(new Error(errorMessage));
},
});
};
const renderUseStream = (request: any, options?: { autoStart?: boolean }) => {
return renderHook(() => useStream(request, options));
};
describe("useStream [ Base ]", () => {
let request = createStreamRequest();
beforeEach(() => {
vi.resetModules();
client.clear();
request = createStreamRequest();
});
describe("when hook is initialized", () => {
it("should have initial state", () => {
const { result } = renderUseStream(request);
expect(result.current.text).toBe("");
expect(result.current.chunks).toEqual([]);
expect(result.current.streaming).toBe(false);
expect(result.current.done).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.extra).toBe(null);
expect(result.current.status).toBe(null);
expect(typeof result.current.start).toBe("function");
expect(typeof result.current.abort).toBe("function");
expect(typeof result.current.reset).toBe("function");
});
});
describe("when start is called with a ReadableStream response", () => {
it("should stream text chunks and set done when complete", async () => {
const chunks = ["Hello ", "World", "!"];
const stream = createReadableStream(chunks);
const sendMock = vi.fn().mockResolvedValue({
data: stream,
error: null,
status: 200,
extra: { headers: {} },
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({
send: sendMock,
}),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
await act(async () => {
result.current.start();
});
await waitFor(() => {
expect(result.current.done).toBe(true);
});
expect(result.current.text).toBe("Hello World!");
expect(result.current.chunks).toHaveLength(3);
expect(result.current.streaming).toBe(false);
expect(result.current.status).toBe(200);
expect(result.current.extra).toEqual({ headers: {} });
expect(result.current.error).toBe(null);
});
it("should set streaming to true while consuming", async () => {
let resolvePull: (() => void) | null = null;
const pullPromise = new Promise<void>((r) => {
resolvePull = r;
});
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
async pull(controller) {
controller.enqueue(encoder.encode("data"));
await pullPromise;
controller.close();
},
});
const sendMock = vi.fn().mockResolvedValue({
data: stream,
error: null,
status: 200,
extra: null,
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
act(() => {
result.current.start();
});
await waitFor(() => {
expect(result.current.streaming).toBe(true);
});
await act(async () => {
resolvePull!();
});
await waitFor(() => {
expect(result.current.streaming).toBe(false);
expect(result.current.done).toBe(true);
});
});
});
describe("when start is called and response has an error", () => {
it("should set error state", async () => {
const errorData = { message: "Server error" };
const sendMock = vi.fn().mockResolvedValue({
data: null,
error: errorData,
status: 500,
extra: { headers: {} },
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
await act(async () => {
result.current.start();
});
await waitFor(() => {
expect(result.current.error).toEqual(errorData);
});
expect(result.current.streaming).toBe(false);
expect(result.current.status).toBe(500);
expect(result.current.extra).toEqual({ headers: {} });
});
});
describe("when response data is a plain string (non-stream fallback)", () => {
it("should set text directly from the string data", async () => {
const sendMock = vi.fn().mockResolvedValue({
data: "plain text response",
error: null,
status: 200,
extra: null,
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
await act(async () => {
result.current.start();
});
await waitFor(() => {
expect(result.current.done).toBe(true);
});
expect(result.current.text).toBe("plain text response");
expect(result.current.streaming).toBe(false);
});
});
describe("when response data is a non-string value (JSON.stringify fallback)", () => {
it("should JSON.stringify the data", async () => {
const objectData = { key: "value", num: 42 };
const sendMock = vi.fn().mockResolvedValue({
data: objectData,
error: null,
status: 200,
extra: null,
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
await act(async () => {
result.current.start();
});
await waitFor(() => {
expect(result.current.done).toBe(true);
});
expect(result.current.text).toBe(JSON.stringify(objectData));
expect(result.current.streaming).toBe(false);
});
});
describe("when stream reader throws an error", () => {
it("should set error state from the reader exception", async () => {
const stream = createFailingReadableStream("Stream read error");
const sendMock = vi.fn().mockResolvedValue({
data: stream,
error: null,
status: 200,
extra: null,
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
await act(async () => {
result.current.start();
});
await waitFor(() => {
expect(result.current.error).toBeTruthy();
});
expect(result.current.streaming).toBe(false);
});
});
describe("when abort is called", () => {
it("should stop streaming", async () => {
let resolvePull: (() => void) | null = null;
const pullPromise = new Promise<void>((r) => {
resolvePull = r;
});
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
async pull(controller) {
controller.enqueue(encoder.encode("chunk"));
await pullPromise;
controller.close();
},
});
const abortSpy = vi.fn();
const sendMock = vi.fn().mockResolvedValue({
data: stream,
error: null,
status: 200,
extra: null,
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: {
...request.client,
requestManager: {
...request.client.requestManager,
abortByKey: abortSpy,
},
},
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
act(() => {
result.current.start();
});
await waitFor(() => {
expect(result.current.streaming).toBe(true);
});
act(() => {
result.current.abort();
});
expect(result.current.streaming).toBe(false);
expect(abortSpy).toHaveBeenCalled();
await act(async () => {
resolvePull!();
});
});
});
describe("when reset is called", () => {
it("should reset all state to initial values", async () => {
const sendMock = vi.fn().mockResolvedValue({
data: "some data",
error: null,
status: 200,
extra: { headers: {} },
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
await act(async () => {
result.current.start();
});
await waitFor(() => {
expect(result.current.done).toBe(true);
});
expect(result.current.text).toBe("some data");
act(() => {
result.current.reset();
});
expect(result.current.text).toBe("");
expect(result.current.chunks).toEqual([]);
expect(result.current.streaming).toBe(false);
expect(result.current.done).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.extra).toBe(null);
expect(result.current.status).toBe(null);
});
});
describe("when autoStart is true", () => {
it("should start streaming automatically on mount", async () => {
const sendMock = vi.fn().mockResolvedValue({
data: "auto-started",
error: null,
status: 200,
extra: null,
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest, { autoStart: true });
await waitFor(() => {
expect(result.current.done).toBe(true);
});
expect(result.current.text).toBe("auto-started");
expect(sendMock).toHaveBeenCalled();
});
});
describe("when start is called while already streaming", () => {
it("should not start a second stream", async () => {
let resolvePull: (() => void) | null = null;
const pullPromise = new Promise<void>((r) => {
resolvePull = r;
});
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
async pull(controller) {
controller.enqueue(encoder.encode("data"));
await pullPromise;
controller.close();
},
});
const sendMock = vi.fn().mockResolvedValue({
data: stream,
error: null,
status: 200,
extra: null,
});
const mockRequest = {
...request,
setOptions: vi.fn().mockReturnValue({ send: sendMock }),
client: request.client,
abortKey: request.abortKey,
scope: request.scope,
};
const { result } = renderUseStream(mockRequest);
act(() => {
result.current.start();
});
await waitFor(() => {
expect(result.current.streaming).toBe(true);
});
act(() => {
result.current.start();
});
expect(sendMock).toHaveBeenCalledTimes(1);
await act(async () => {
resolvePull!();
});
await waitFor(() => {
expect(result.current.done).toBe(true);
});
});
});
});