diff --git a/packages/editor-ui/src/utils/__tests__/apiUtils.spec.ts b/packages/editor-ui/src/utils/__tests__/apiUtils.spec.ts new file mode 100644 index 0000000000000..cefb6f3389bb7 --- /dev/null +++ b/packages/editor-ui/src/utils/__tests__/apiUtils.spec.ts @@ -0,0 +1,112 @@ +import { STREAM_SEPERATOR, streamRequest } from '../apiUtils'; + +describe('streamRequest', () => { + it('should stream data from the API endpoint', async () => { + const encoder = new TextEncoder(); + const mockResponse = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 1 })}${STREAM_SEPERATOR}`)); + controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 2 })}${STREAM_SEPERATOR}`)); + controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 3 })}${STREAM_SEPERATOR}`)); + controller.close(); + }, + }); + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + body: mockResponse, + }); + + global.fetch = mockFetch; + + const onChunkMock = vi.fn(); + const onDoneMock = vi.fn(); + const onErrorMock = vi.fn(); + + await streamRequest( + { + baseUrl: 'https://api.example.com', + pushRef: '', + }, + '/data', + { key: 'value' }, + onChunkMock, + onDoneMock, + onErrorMock, + ); + + expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', { + method: 'POST', + body: JSON.stringify({ key: 'value' }), + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'browser-id': expect.stringContaining('-'), + }, + }); + + expect(onChunkMock).toHaveBeenCalledTimes(3); + expect(onChunkMock).toHaveBeenNthCalledWith(1, { chunk: 1 }); + expect(onChunkMock).toHaveBeenNthCalledWith(2, { chunk: 2 }); + expect(onChunkMock).toHaveBeenNthCalledWith(3, { chunk: 3 }); + + expect(onDoneMock).toHaveBeenCalledTimes(1); + expect(onErrorMock).not.toHaveBeenCalled(); + }); + + it('should handle broken stream data', async () => { + const encoder = new TextEncoder(); + const mockResponse = new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode(`${JSON.stringify({ chunk: 1 })}${STREAM_SEPERATOR}{"chunk": `), + ); + controller.enqueue(encoder.encode(`2}${STREAM_SEPERATOR}{"ch`)); + controller.enqueue(encoder.encode('unk":')); + controller.enqueue(encoder.encode(`3}${STREAM_SEPERATOR}`)); + controller.close(); + }, + }); + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + body: mockResponse, + }); + + global.fetch = mockFetch; + + const onChunkMock = vi.fn(); + const onDoneMock = vi.fn(); + const onErrorMock = vi.fn(); + + await streamRequest( + { + baseUrl: 'https://api.example.com', + pushRef: '', + }, + '/data', + { key: 'value' }, + onChunkMock, + onDoneMock, + onErrorMock, + ); + + expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', { + method: 'POST', + body: JSON.stringify({ key: 'value' }), + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'browser-id': expect.stringContaining('-'), + }, + }); + + expect(onChunkMock).toHaveBeenCalledTimes(3); + expect(onChunkMock).toHaveBeenNthCalledWith(1, { chunk: 1 }); + expect(onChunkMock).toHaveBeenNthCalledWith(2, { chunk: 2 }); + expect(onChunkMock).toHaveBeenNthCalledWith(3, { chunk: 3 }); + + expect(onDoneMock).toHaveBeenCalledTimes(1); + expect(onErrorMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/editor-ui/src/utils/apiUtils.ts b/packages/editor-ui/src/utils/apiUtils.ts index adfff50be67b3..b07f8910b90c8 100644 --- a/packages/editor-ui/src/utils/apiUtils.ts +++ b/packages/editor-ui/src/utils/apiUtils.ts @@ -13,6 +13,7 @@ if (!browserId && 'randomUUID' in crypto) { } export const NO_NETWORK_ERROR_CODE = 999; +export const STREAM_SEPERATOR = '⧉⇋⇋➽⌑⧉§§\n'; export class ResponseError extends ApplicationError { // The HTTP status code of response @@ -200,7 +201,7 @@ export async function streamRequest( onChunk?: (chunk: T) => void, onDone?: () => void, onError?: (e: Error) => void, - separator = '⧉⇋⇋➽⌑⧉§§\n', + separator = STREAM_SEPERATOR, ): Promise { const headers: Record = { 'Content-Type': 'application/json',