import axios, { AxiosInterceptorManager, AxiosResponse } from 'axios'; import { initAjaxHandlers } from '../ajax-handlers'; import { initAjaxHandlersCommon } from '../ajax-handlers-common'; import { initAjaxHandlersUnauthorized } from '../ajax-handlers-unauthorized'; import { Status } from '../common'; import { loader } from '../loader'; jest.mock('axios', () => ({ interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, isCancel: jest.fn(), })); jest.mock('../ajax-handlers-common', () => ({ initAjaxHandlersCommon: jest.fn() })); jest.mock('../ajax-handlers-unauthorized', () => ({ initAjaxHandlersUnauthorized: jest.fn() })); jest.mock('../loader', () => ({ loader: { show: jest.fn(), hide: jest.fn() } })); interface Handlers { request: { onFulfilled?: Parameters['use']>[0]; onRejected?: Parameters['use']>[1]; }; response: { onFulfilled?: Parameters['use']>[0]; onRejected?: Parameters['use']>[1]; }; } describe(`[ajax-handlers] ${initAjaxHandlers.name}`, () => { const errorFilteredUrls = ['http://foo.com']; const notifier = { log: jest.fn(), show: jest.fn() }; let unauthorizedHandler: NonNullable< Parameters[0] >['unauthorizedHandler']; let handlers: Handlers; beforeEach(() => { jest.clearAllMocks(); unauthorizedHandler = undefined; }); const subject = () => { initAjaxHandlers({ notifier, errorFilteredUrls, unauthorizedHandler }); const { interceptors } = axios; const [config, requestError] = jest.mocked(interceptors.request.use).mock.calls[0]; const [response, responseError] = jest.mocked(interceptors.response.use).mock.calls[0]; handlers = { request: { onFulfilled: config, onRejected: requestError }, response: { onFulfilled: response, onRejected: responseError }, }; }; test('initializes common handlers', () => { subject(); expect(initAjaxHandlersCommon).toHaveBeenCalled(); }); describe('with unauthorizedHandler', () => { beforeEach(() => { unauthorizedHandler = true; }); function itCallsInitAjaxHandlersUnauthorized(options?: typeof unauthorizedHandler) { test('initializes unauthorized handlers', () => { subject(); expect(initAjaxHandlersUnauthorized).toHaveBeenCalledWith(options); }); } itCallsInitAjaxHandlersUnauthorized(); describe('when unauthorizedHandler is an object', () => { const handler = { isUnauthorized: () => true }; beforeEach(() => { unauthorizedHandler = handler; }); itCallsInitAjaxHandlersUnauthorized(handler); }); }); describe('request handlers', () => { beforeEach(() => subject()); describe('config', () => { let config: any; beforeEach(() => (config = {})); const subject = () => handlers.request.onFulfilled?.(config); test('shows loader', () => { expect(subject()).toBe(config); expect(loader.show).toHaveBeenCalled(); }); describe('when loader is hidden', () => { beforeEach(() => (config.hideGlobalLoader = true)); test('does not show loader', () => { subject(); expect(loader.show).not.toHaveBeenCalled(); }); }); }); describe('error', () => { let error: any; beforeEach(() => { error = { config: { url: 'http://example.com/foo' }, request: { message: 'foo', readyState: 'ready' }, }; }); const subject = () => handlers.request.onRejected?.(error); const setup = () => expect(subject()).rejects.toBe(error); test('hides loader', () => { setup(); expect(loader.hide).toHaveBeenCalled(); }); describe('when loader is hidden', () => { beforeEach(() => (error.config.hideGlobalLoader = true)); test('does not hide loader', () => { setup(); expect(loader.hide).not.toHaveBeenCalled(); }); }); describe('when request.status is zero', () => { beforeEach(() => (error.request.status = 0)); test('logs error', () => { const { message, status, readyState } = error.request; setup(); expect(notifier.log).toHaveBeenCalledWith({ exception: message, category: 'NetworkConnection', message: 'Network Connection Error', data: { readyState, requestUrl: error.config.url, status }, }); }); }); }); }); describe('response handlers', () => { beforeEach(() => subject()); describe('response', () => { let response: any; beforeEach(() => (response = { config: {} })); const subject = () => handlers.response.onFulfilled?.(response); test('hides loader', () => { expect(subject()).toBe(response); expect(loader.hide).toHaveBeenCalled(); }); describe('when loader is hidden', () => { beforeEach(() => (response.config.hideGlobalLoader = true)); test('does not hide loader', () => { subject(); expect(loader.hide).not.toHaveBeenCalled(); }); }); }); describe('error', () => { let error: any; beforeEach(() => { error = { config: { baseURL: 'http://example.com', url: 'http://example.com/foo' }, request: { message: 'foo', readyStatus: 'ready' }, response: { status: 404, statusText: 'statusText' }, }; }); const subject = () => handlers.response.onRejected?.(error); const setup = () => expect(subject()).rejects.toBe(error); function itDoesNotShowError() { test('does not show error', () => { setup(); expect(notifier.show).not.toHaveBeenCalled(); }); } function composeMessage(error: any) { return error.response.status ? error.response.statusText : [ error.response.statusText, `URL: ${error.config.url}`, `Status Code: ${error.response.status}`, ].join('\r\n'); } test('hides loader', () => { setup(); expect(loader.hide).toHaveBeenCalled(); }); describe('when loader is hidden', () => { beforeEach(() => (error.config.hideGlobalLoader = true)); test('does not hide loader', () => { setup(); expect(loader.hide).not.toHaveBeenCalled(); }); }); describe('when request.status is zero', () => { beforeEach(() => (error.request.status = 0)); test('logs error', () => { setup(); expect(notifier.log).toHaveBeenCalled(); }); }); describe('when request was cancelled', () => { beforeEach(() => jest.mocked(axios.isCancel).mockReturnValueOnce(true)); test('rejects the error', async () => { await expect(subject()).rejects.toBe(error); }); }); test('shows the error', () => { setup(); expect(notifier.show).toHaveBeenCalledWith({ status: Status.Warning, title: 'Action cancelled', message: composeMessage(error), detail: { isConnectionError: false, isHardError: false, baseUrl: error.config.baseURL, url: error.config.url, error, }, }); }); describe('when response.data contains error message', () => { beforeEach(() => { error.response.data = { Error: { Message: 'responseDataErrorMessage' } }; }); test('uses response.data error message', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining(error.response.data.Error.Message), }) ); }); }); describe('when response header contains message', () => { beforeEach(() => { error.response.headers = { 'x-message': 'responseHeaderMessage' }; }); test('uses response header message', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining(error.response.headers['x-message']), }) ); }); }); describe('when response contains no message', () => { beforeEach(() => delete error.response.statusText); test('uses message "Error"', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringMatching(/^Error/), }) ); }); }); // TODO: Fix exception when error is a string describe.skip('when error is a string', () => { beforeEach(() => (error = 'Oops!')); test('uses error as message', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining(error) }) ); }); }); describe('with a connection error', () => { beforeEach(() => (error.response.status = undefined)); test('shows connection error', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ detail: expect.objectContaining({ isConnectionError: true }), message: composeMessage(error), }) ); }); describe('when error.config is undefined', () => { beforeEach(() => (error.config = undefined)); test('shows connection error with empty URL in message', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining('URL: '), }) ); }); }); }); describe('with a hard error', () => { beforeEach(() => { error.response.headers = { harderror: true, errorlogentryid: 'foo' }; }); test('shows hard error', () => { setup(); expect(notifier.show).toHaveBeenCalledWith({ status: Status.Error, title: 'Application error', duration: 0, action: { label: 'Show details', onClick: expect.any(Function) }, detail: { isHardError: true, isReportable: false, baseUrl: error.config.baseURL, url: error.config.url, error, }, }); }); test('onClick() action dispatches event', () => { const dispatchEventSpy = jest.spyOn(window, 'dispatchEvent'); setup(); const { action } = notifier.show.mock.calls[0][0]; action.onClick(); expect(dispatchEventSpy).toHaveBeenCalled(); }); describe('when error is reportable', () => { beforeEach(() => (error.response.headers.isreportable = 'true')); test('shows reportable error', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ detail: expect.objectContaining({ isReportable: true }), }) ); }); }); describe('when error.config is undefined', () => { beforeEach(() => (error.config = undefined)); test('shows hard error with empty baseUrl and url', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ detail: expect.objectContaining({ isHardError: true, baseUrl: '', url: '', }), }) ); }); }); }); describe('when error.config is undefined', () => { beforeEach(() => (error.config = undefined)); test('shows error with empty baseUrl and url', () => { setup(); expect(notifier.show).toHaveBeenCalledWith( expect.objectContaining({ detail: expect.objectContaining({ baseUrl: '', url: '', error, }), }) ); }); }); describe('when error status is suppressed', () => { beforeEach(() => (error.config.suppressShowError = [error.response.status])); itDoesNotShowError(); }); describe('when suppressShowError includes 0 and response.status is undefined', () => { beforeEach(() => { error.response.status = undefined; error.config.suppressShowError = [0]; }); test('shows error (undefined status is not treated as 0)', () => { setup(); expect(notifier.show).toHaveBeenCalled(); }); }); describe('when error is suppressed by callback', () => { beforeEach(() => (error.config.suppressShowError = () => true)); itDoesNotShowError(); }); describe('when error is filtered', () => { beforeEach(() => (error.config.url = `${errorFilteredUrls[0]}/foo`)); itDoesNotShowError(); }); describe('when error is coded exception', () => { beforeEach(() => (error.response.data = { Error: { Code: 'Foo' } })); itDoesNotShowError(); }); describe('when error is "abort"', () => { beforeEach(() => (error = 'abort')); itDoesNotShowError(); }); }); }); });