From bd20aeb64c4eed117831556c54b40ff4aee9bfd1 Mon Sep 17 00:00:00 2001 From: Martin Hanzel Date: Thu, 5 Sep 2019 12:56:17 +0000 Subject: Add helpers to wait for axios requests Add two methods to the axios_utils Jest mock: - `waitFor(url)`, which returns a Promise that resolves when the next request to `url` finishes. - `waitForAll()`, which returns a Promise that resolves when all pending requests finish. --- app/assets/javascripts/lib/utils/axios_utils.js | 11 ++--- spec/frontend/lib/utils/axios_utils_spec.js | 45 ++++++++++++++++++ spec/frontend/mocks/ce/lib/utils/axios_utils.js | 62 +++++++++++++++++++++++++ spec/frontend/notes/old_notes_spec.js | 9 +--- 4 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 spec/frontend/lib/utils/axios_utils_spec.js diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 69159e2d741..37721cd030c 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -10,21 +10,18 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.interceptors.request.use(config => { window.activeVueResources = window.activeVueResources || 0; window.activeVueResources += 1; - return config; }); // Remove the global counter axios.interceptors.response.use( - config => { + response => { window.activeVueResources -= 1; - - return config; + return response; }, - e => { + err => { window.activeVueResources -= 1; - - return Promise.reject(e); + return Promise.reject(err); }, ); diff --git a/spec/frontend/lib/utils/axios_utils_spec.js b/spec/frontend/lib/utils/axios_utils_spec.js new file mode 100644 index 00000000000..d5c39567f06 --- /dev/null +++ b/spec/frontend/lib/utils/axios_utils_spec.js @@ -0,0 +1,45 @@ +/* eslint-disable promise/catch-or-return */ + +import AxiosMockAdapter from 'axios-mock-adapter'; + +import axios from '~/lib/utils/axios_utils'; + +describe('axios_utils', () => { + let mock; + + beforeEach(() => { + mock = new AxiosMockAdapter(axios); + mock.onAny('/ok').reply(200); + mock.onAny('/err').reply(500); + expect(axios.countActiveRequests()).toBe(0); + }); + + afterEach(() => axios.waitForAll().finally(() => mock.restore())); + + describe('waitForAll', () => { + it('resolves if there are no requests', () => axios.waitForAll()); + + it('waits for all requests to finish', () => { + const handler = jest.fn(); + axios.get('/ok').then(handler); + axios.get('/err').catch(handler); + + return axios.waitForAll().finally(() => { + expect(handler).toHaveBeenCalledTimes(2); + expect(handler.mock.calls[0][0].status).toBe(200); + expect(handler.mock.calls[1][0].response.status).toBe(500); + }); + }); + }); + + describe('waitFor', () => { + it('waits for requests on a specific URL', () => { + const handler = jest.fn(); + axios.get('/ok').finally(handler); + axios.waitFor('/err').finally(() => { + throw new Error('waitFor on /err should not be called'); + }); + return axios.waitFor('/ok'); + }); + }); +}); diff --git a/spec/frontend/mocks/ce/lib/utils/axios_utils.js b/spec/frontend/mocks/ce/lib/utils/axios_utils.js index a3783b91f95..85fad231d28 100644 --- a/spec/frontend/mocks/ce/lib/utils/axios_utils.js +++ b/spec/frontend/mocks/ce/lib/utils/axios_utils.js @@ -1,3 +1,5 @@ +import EventEmitter from 'events'; + const axios = jest.requireActual('~/lib/utils/axios_utils').default; axios.isMock = true; @@ -13,4 +15,64 @@ axios.defaults.adapter = config => { throw error; }; +// Count active requests and provide a way to wait for them +let activeRequests = 0; +const events = new EventEmitter(); +const onRequest = () => { + activeRequests += 1; +}; + +// Use setImmediate to alloow the response interceptor to finish +const onResponse = config => { + activeRequests -= 1; + setImmediate(() => { + events.emit('response', config); + }); +}; + +const subscribeToResponse = (predicate = () => true) => + new Promise(resolve => { + const listener = (config = {}) => { + if (predicate(config)) { + events.off('response', listener); + resolve(config); + } + }; + + events.on('response', listener); + + // If a request has been made synchronously, setImmediate waits for it to be + // processed and the counter incremented. + setImmediate(listener); + }); + +/** + * Registers a callback function to be run after a request to the given URL finishes. + */ +axios.waitFor = url => subscribeToResponse(({ url: configUrl }) => configUrl === url); + +/** + * Registers a callback function to be run after all requests have finished. If there are no requests waiting, the callback is executed immediately. + */ +axios.waitForAll = () => subscribeToResponse(() => activeRequests === 0); + +axios.countActiveRequests = () => activeRequests; + +axios.interceptors.request.use(config => { + onRequest(); + return config; +}); + +// Remove the global counter +axios.interceptors.response.use( + response => { + onResponse(response.config); + return response; + }, + err => { + onResponse(err.config); + return Promise.reject(err); + }, +); + export default axios; diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/old_notes_spec.js index b57041cf4d1..96133c601aa 100644 --- a/spec/frontend/notes/old_notes_spec.js +++ b/spec/frontend/notes/old_notes_spec.js @@ -49,17 +49,12 @@ describe('Old Notes (~/notes.js)', () => { setTestTimeoutOnce(4000); }); - afterEach(done => { + afterEach(() => { // The Notes component sets a polling interval. Clear it after every run. // Make sure to use jest.runOnlyPendingTimers() instead of runAllTimers(). jest.clearAllTimers(); - setImmediate(() => { - // Wait for any requests to resolve, otherwise we get failures about - // unmocked requests. - mockAxios.restore(); - done(); - }); + return axios.waitForAll().finally(() => mockAxios.restore()); }); it('loads the Notes class into the DOM', () => { -- cgit v1.2.1