From 10064520b57151cd2e71406a00a4585477018e7f Mon Sep 17 00:00:00 2001 From: Bartek Fabiszewski Date: Sat, 16 Nov 2019 12:44:15 +0100 Subject: [PATCH] Add ajax class --- js/src/ajax.js | 111 ++++++++++++++++++++++++++++++ js/test/ajax.test.js | 157 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 js/src/ajax.js create mode 100644 js/test/ajax.test.js diff --git a/js/src/ajax.js b/js/src/ajax.js new file mode 100644 index 0000000..8c615fe --- /dev/null +++ b/js/src/ajax.js @@ -0,0 +1,111 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +export default class uAjax { + + /** + * Perform POST HTTP request + * @alias ajax + */ + static post(url, data, options) { + const params = options || {}; + params.method = 'POST'; + return this.ajax(url, data, params); + } + + /** + * Perform GET HTTP request + * @alias ajax + */ + static get(url, data, options) { + const params = options || {}; + params.method = 'GET'; + return this.ajax(url, data, params); + } + + /** + * Perform ajax HTTP request + * @param {string} url Request URL + * @param {Object|HTMLFormElement} [data] Optional request parameters: key/value pairs or form element + * @param {Object} [options] Optional options + * @param {string} [options.method='GET'] Optional query method, default 'GET' + * @return {Promise} + */ + static ajax(url, data, options) { + const params = []; + data = data || {}; + options = options || {}; + const method = options.method || 'GET'; + const xhr = new XMLHttpRequest(); + return new Promise((resolve, reject) => { + xhr.onreadystatechange = function () { + if (xhr.readyState !== XMLHttpRequest.DONE) { return; } + let message = ''; + let error = true; + if (xhr.status === 200) { + try { + const obj = JSON.parse(xhr.responseText); + if (obj) { + if (!obj.error) { + if (resolve && typeof resolve === 'function') { + resolve(obj); + } + error = false; + } else if (obj.message) { + message = obj.message; + } + } + } catch (err) { + message = err.message; + } + } else { + message = `HTTP error ${xhr.status}`; + } + if (error && reject && typeof reject === 'function') { + reject(message); + } + }; + let body = null; + if (data instanceof HTMLFormElement) { + if (method === 'POST') { + body = new FormData(data); + } else { + body = new URLSearchParams(new FormData(data)).toString(); + } + } else { + for (const key in data) { + if (data.hasOwnProperty(key)) { + params.push(key + '=' + encodeURIComponent(data[key])); + } + } + body = params.join('&'); + body = body.replace(/%20/g, '+'); + } + if (method === 'GET' && body.length) { + url += '?' + body; + body = null; + } + xhr.open(method, url, true); + if (method === 'POST' && !(data instanceof HTMLFormElement)) { + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + xhr.send(body); + }); + } +} diff --git a/js/test/ajax.test.js b/js/test/ajax.test.js new file mode 100644 index 0000000..17f7450 --- /dev/null +++ b/js/test/ajax.test.js @@ -0,0 +1,157 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import uAjax from '../src/ajax.js'; + +describe('Ajax tests', () => { + + const url = 'http://ulogger.test/'; + const validResponse = { id: 1 }; + const invalidResponse = 'invalid'; + const errorResponse = { error: true, message: 'response error' }; + const form = document.createElement('form'); + const input = document.createElement('input'); + input.type = 'text'; + input.name = 'p1'; + input.value = 'test'; + form.appendChild(input); + + beforeEach(() => { + spyOn(XMLHttpRequest.prototype, 'open').and.callThrough(); + spyOn(XMLHttpRequest.prototype, 'setRequestHeader').and.callThrough(); + spyOn(XMLHttpRequest.prototype, 'send'); + spyOnProperty(XMLHttpRequest.prototype, 'readyState').and.returnValue(XMLHttpRequest.DONE); + }); + + it('should make POST request', () => { + // when + uAjax.post(url); + // then + expect(XMLHttpRequest.prototype.setRequestHeader).toHaveBeenCalledWith('Content-type', 'application/x-www-form-urlencoded'); + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('POST', url, true); + }); + + it('should make GET request', () => { + // when + uAjax.get(url); + // then + expect(XMLHttpRequest.prototype.setRequestHeader).not.toHaveBeenCalled(); + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', url, true); + }); + + it('should make GET request with parameters', () => { + // when + uAjax.get(url, { p1: 1, p2: 'test' }); + // then + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', `${url}?p1=1&p2=test`, true); + expect(XMLHttpRequest.prototype.send).toHaveBeenCalledWith(null); + }); + + it('should make POST request with parameters', () => { + // when + uAjax.post(url, { p1: 1, p2: 'test' }); + // then + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('POST', url, true); + expect(XMLHttpRequest.prototype.send).toHaveBeenCalledWith('p1=1&p2=test'); + }); + + it('should make POST request with form data', () => { + // when + uAjax.post(url, form); + // then + expect(XMLHttpRequest.prototype.setRequestHeader).not.toHaveBeenCalled(); + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('POST', url, true); + expect(XMLHttpRequest.prototype.send).toHaveBeenCalledWith(new FormData(form)); + }); + + it('should make GET request with form data', () => { + // when + uAjax.get(url, form); + // then + expect(XMLHttpRequest.prototype.setRequestHeader).not.toHaveBeenCalled(); + expect(XMLHttpRequest.prototype.open).toHaveBeenCalledWith('GET', `${url}?p1=test`, true); + expect(XMLHttpRequest.prototype.send).toHaveBeenCalledWith(null); + }); + + it('should make successful request and return value', (done) => { + // when + spyOnProperty(XMLHttpRequest.prototype, 'status').and.returnValue(200); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify(validResponse)); + // then + uAjax.get(url) + .then((result) => { + expect(result).toEqual(validResponse); + done(); + }) + .catch(() => done.fail('reject callback called')); + }); + + it('should make successful request and return error with message', (done) => { + // when + spyOnProperty(XMLHttpRequest.prototype, 'status').and.returnValue(200); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify(errorResponse)); + // then + uAjax.get(url) + .then(() => done.fail('resolve callback called')) + .catch((message) => { + expect(message).toBe(errorResponse.message); + done(); + }); + }); + + it('should make successful request and return error without message', (done) => { + // when + spyOnProperty(XMLHttpRequest.prototype, 'status').and.returnValue(200); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(JSON.stringify({ error: true })); + // then + uAjax.get(url) + .then(() => done.fail('resolve callback called')) + .catch((message) => { + expect(message).toBe(''); + done(); + }); + }); + + it('should make request and fail with HTTP error code', (done) => { + // when + const status = 401; + spyOnProperty(XMLHttpRequest.prototype, 'status').and.returnValue(status); + // then + uAjax.get(url) + .then(() => done.fail('resolve callback called')) + .catch((message) => { + expect(message).toBe(`HTTP error ${status}`); + done(); + }); + }); + + it('should make request and fail with JSON parse error', (done) => { + // when + spyOnProperty(XMLHttpRequest.prototype, 'status').and.returnValue(200); + spyOnProperty(XMLHttpRequest.prototype, 'responseText').and.returnValue(invalidResponse); + // then + uAjax.get(url) + .then(() => done.fail('resolve callback called')) + .catch((message) => { + expect(message).toContain('JSON'); + done(); + }); + }); + +});