import { getJson } from '../common'; /* * Can be replaced by async timer API available in jest v29.5.0 * See https://github.com/jestjs/jest/issues/13435 */ async function flushMicrotasks() { await new Promise(resolve => jest.requireActual('timers').setImmediate(resolve)); } async function flushMicrotasksAndRunAllTimersTimes(times: number) { for (let i = 0; i < times; i++) { // eslint-disable-next-line no-await-in-loop await flushMicrotasks(); jest.runAllTimers(); } } describe('[web-components] common', () => { describe(`${getJson.name}`, () => { const mockUrl = 'fake://url'; const fetchResponseJson = { data: 'test' }; let spyFetch: jest.SpyInstance; let fetchResponse: any; let getJsonOptions: NonNullable[1]>; beforeEach(() => { jest.useFakeTimers(); jest.resetAllMocks(); spyFetch = jest.spyOn(global, 'fetch').mockImplementation(() => fetchResponse); getJsonOptions = {}; fetchResponse = { ok: true, json: () => Promise.resolve(fetchResponseJson), }; }); function subject() { return getJson(mockUrl, getJsonOptions); } test('returns a response after performing 1 fetch', async () => { expect(await subject()).toEqual({ data: fetchResponseJson, headers: undefined, url: mockUrl, }); expect(spyFetch).toHaveBeenCalledTimes(1); }); describe('when onRequestError is provided', () => { const onRequestError = jest.fn(); beforeEach(() => (getJsonOptions = { onRequestError })); test('does not call onRequestError', async () => { await subject(); expect(onRequestError).not.toHaveBeenCalled(); }); }); describe('when the json response contains a url', () => { const mockFetchResponseUrl = 'foo://bar'; beforeEach(() => (fetchResponse.url = mockFetchResponseUrl)); test('returns a response with the url from the json response', async () => { expect(await subject()).toEqual( expect.objectContaining({ url: mockFetchResponseUrl }) ); }); }); describe("when the fetch response's JSON fails to serialize", () => { beforeEach(() => (fetchResponse.json = () => Promise.reject())); test('returns a response with an empty data object', async () => { expect(await subject()).toEqual(expect.objectContaining({ data: {} })); }); }); describe('when fetch responses are not ok', () => { const fetchResponseText = 'test'; beforeEach(() => { Object.assign(fetchResponse, { ok: false, status: 444, text: () => Promise.resolve(fetchResponseText), }); }); test('throws an error', async () => { await expect(subject()).rejects.toThrow( expect.objectContaining({ name: 'NetworkError', message: `Requesting "${mockUrl}" failed with status code ${fetchResponse.status}. Details: ${fetchResponseText}.`, }) ); }); describe('when onRequestError is provided', () => { const onRequestError = jest.fn(); function expectRequestError() { expect(onRequestError).toHaveBeenCalledWith({ status: fetchResponse.status, json: fetchResponseJson, body: fetchResponseText, }); } beforeEach(() => { getJsonOptions = { onRequestError }; }); test('calls onRequestError', async () => { await expect(subject()).rejects.toThrow(); expectRequestError(); }); describe('when the first fetch request fails', () => { beforeEach(() => spyFetch.mockRejectedValueOnce({}).mockResolvedValueOnce(fetchResponse) ); test('calls onRequestError', async () => { const result = subject(); await flushMicrotasksAndRunAllTimersTimes(1); await expect(result).rejects.toThrow(); expectRequestError(); }); }); describe('when response.text() fails', () => { beforeEach(() => (fetchResponse.text = () => Promise.reject())); test("calls onRequestError with a body value of ''", async () => { await expect(subject()).rejects.toThrow(); expect(onRequestError).toHaveBeenCalledWith( expect.objectContaining({ body: '' }) ); }); }); }); }); describe('when the fetch request fails', () => { beforeEach(() => spyFetch.mockRejectedValue({})); describe('when retries is provided', () => { beforeEach(() => (getJsonOptions.retries = 10)); test('retries the specified number of times before throwing error', async () => { const result = subject(); await flushMicrotasksAndRunAllTimersTimes(getJsonOptions.retries!); await expect(result).rejects.toThrow( expect.objectContaining({ name: 'NetworkError', message: `Failed to fetch: "${mockUrl}".`, }) ); const expectedTimesCalled = getJsonOptions.retries! + 1; expect(spyFetch).toHaveBeenCalledTimes(expectedTimesCalled); }); }); describe('when a signal is provided and aborted', () => { let abortController: AbortController; beforeEach(() => { abortController = new AbortController(); getJsonOptions.signal = abortController.signal; getJsonOptions.retries = 2; }); test('does not retry the request when the signal is aborted before the first retry', async () => { const result = subject(); await flushMicrotasks(); abortController.abort(); await expect(result).rejects.toThrow(/The operation was aborted/); await flushMicrotasksAndRunAllTimersTimes(getJsonOptions.retries!); expect(spyFetch).toHaveBeenCalledTimes(1); }); test('does not retry the request after the signal is aborted after the first retry', async () => { const result = subject(); await flushMicrotasksAndRunAllTimersTimes(1); abortController.abort(); await expect(result).rejects.toThrow(/The operation was aborted/); await flushMicrotasksAndRunAllTimersTimes(getJsonOptions.retries!); expect(spyFetch).toHaveBeenCalledTimes(2); }); }); }); }); });