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({ pull(controller) { if (index < chunks.length) { controller.enqueue(encoder.encode(chunks[index])); index += 1; } else { controller.close(); } }, }); }; const createFailingReadableStream = (errorMessage: string) => { return new ReadableStream({ 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((r) => { resolvePull = r; }); const encoder = new TextEncoder(); const stream = new ReadableStream({ 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((r) => { resolvePull = r; }); const encoder = new TextEncoder(); const stream = new ReadableStream({ 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((r) => { resolvePull = r; }); const encoder = new TextEncoder(); const stream = new ReadableStream({ 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); }); }); }); });