From 39a3fbced333dc093e45a655d8d417b359ce92c4 Mon Sep 17 00:00:00 2001 From: Steve Clay Date: Tue, 26 Apr 2016 13:50:24 -0400 Subject: [PATCH] feature(js): elgg/Ajax users get more access to underlying resources All success/fail/complete callbacks and Promise handlers now receive the standard jQuery arguments, including `jqXHR`, `textStatus`, and `errorThrown`. In successful responses, `jqXHR.AjaxData` contains the wrapper object passed through the `ajax_response` hooks, and `jqXHR.AjaxData.status` is set to 0 or -1 (indicating `register_error()` was called on the server). The default error handler is improved so that server-encoded messages (sent from `register_error`/`system_message`) are displayed even on HTTP error responses. Note the API to send this type of responses has not yet been implemented, but is planned. Updates ElggAjaxTest to use jquery-mockjax for better coverage and tests new features. Makes developer/ajax_demo a little more like an integration test. Fixes #9767 --- docs/guides/ajax.rst | 46 ++- js/tests/ElggAjaxTest.js | 266 +++++++++++++----- js/tests/karma.conf.js | 2 + js/tests/requirejs.config.js | 4 +- .../actions/developers/ajax_demo.php | 5 +- .../views/default/developers/ajax_demo.js | 29 +- package.json | 3 +- views/default/elgg/Ajax.js | 159 +++++++---- 8 files changed, 362 insertions(+), 152 deletions(-) diff --git a/docs/guides/ajax.rst b/docs/guides/ajax.rst index ac13889e756..88dfd9301c4 100644 --- a/docs/guides/ajax.rst +++ b/docs/guides/ajax.rst @@ -16,11 +16,12 @@ All the ajax methods perform the following: #. Client-side, the ``data`` option (if given as an object) is filtered by the hook ``ajax_request_data``. #. The request is made to the server, either rendering a view or a form, calling an action, or loading a path. -#. Echoed JSON is turned into a response object (``Elgg\Services\AjaxResponse``). +#. The method returns a ``jqXHR`` object, which can be used as a Promise. +#. Server-echoed content is turned into a response object (``Elgg\Services\AjaxResponse``) containing a string (or a JSON-parsed value). #. The response object is filtered by the hook ``ajax_response``. #. The response object is used to create the HTTP response. #. Client-side, the response data is filtered by the hook ``ajax_response_data``. -#. The method returns a ``jqXHR`` object, which can be used as a Promise. +#. The ``jqXHR`` promise is resolved and any ``success`` callbacks are called. More notes: @@ -70,7 +71,10 @@ To execute it, use ``ajax.action('', options)``: arg1: 1, arg2: 2 }, - }).done(function (output) { + }).done(function (output, statusText, jqXHR) { + if (jqXHR.AjaxData.status == -1) { + return; + } alert(output.sum); alert(output.product); }); @@ -117,7 +121,10 @@ To fetch its output, use ``ajax.path('', options)``. var Ajax = require('elgg/Ajax'); var ajax = new Ajax(); - ajax.path('myplugin_time').done(function (output) { + ajax.path('myplugin_time').done(function (output, statusText, jqXHR) { + if (jqXHR.AjaxData.status == -1) { + return; + } alert(output.rfc2822); alert(output.day); }); @@ -169,7 +176,10 @@ To fetch the view, use ``ajax.view('', options)``: data: { guid: 123 // querystring }, - }).done(function (output) { + }).done(function (output, statusText, jqXHR) { + if (jqXHR.AjaxData.status == -1) { + return; + } $('.myplugin-link').html(output); }); @@ -203,7 +213,10 @@ To fetch this using ``ajax.form('', options)``. var Ajax = require('elgg/Ajax'); var ajax = new Ajax(); - ajax.form('myplugin/add').done(function (output) { + ajax.form('myplugin/add').done(function (output, statusText, jqXHR) { + if (jqXHR.AjaxData.status == -1) { + return; + } $('.myplugin-form-container').html(output); }); @@ -300,6 +313,27 @@ To capture the metadata send back to the client, we use the client-side ``ajax_r .. note:: Elgg uses these same hooks to deliver system messages over ``elgg/Ajax`` responses. +Handling errors +--------------- + +Responses basically fall into three categories: + +1. HTTP success (200) with status ``0``. No ``register_error()`` calls were made on the server. +2. HTTP success (200) with status ``-1``. ``register_error()`` was called. +3. HTTP error (4xx/5xx). E.g. calling an action with stale tokens, or a server exception. In this case the ``done`` and ``success`` callbacks are not called. + +You may need only worry about the 2nd case. We can do this by looking at ``jqXHR.AjaxData.status``: + +.. code-block:: js + + ajax.action('entity/delete?guid=123').done(function (value, statusText, jqXHR) { + if (jqXHR.AjaxData.status == -1) { + // a server error was already displayed + return; + } + + // remove element from the page + }); Requiring AMD modules --------------------- diff --git a/js/tests/ElggAjaxTest.js b/js/tests/ElggAjaxTest.js index 536c9bd273a..e15799c5640 100644 --- a/js/tests/ElggAjaxTest.js +++ b/js/tests/ElggAjaxTest.js @@ -1,35 +1,24 @@ define(function(require) { var elgg = require('elgg'); + var $ = require('jquery'); + require('jquery-mockjax'); var Ajax = require('elgg/Ajax'); var ajax = new Ajax(); - + + $.mockjaxSettings.responseTime = 10; + describe("elgg/ajax", function() { - var ajax_tmp, - captured_options, - captured_hook, - response_object, + var captured_hook, root = elgg.get_site_url(); beforeEach(function() { - ajax_tmp = $.ajax; - response_object = {value: null}; captured_hook = null; - - $.ajax = function(options) { - captured_options = options; - var def = $.Deferred(); - setTimeout(function () { - if ($.isFunction(options.success)) { - options.success(response_object); - } - def.resolve(response_object); - }, 1); - return def; - }; + + $.mockjaxSettings.logging = false; + $.mockjax.clear(); elgg.config.hooks = {}; - Ajax._init_hooks(); // note, "all" type > always higher priority than specific types elgg.register_hook_handler(Ajax.REQUEST_DATA_HOOK, 'all', function (h, t, p, v) { @@ -40,15 +29,15 @@ define(function(require) { }; }); }); - - afterEach(function() { - $.ajax = ajax_tmp; - }); it("passes unwrapped value to both success and deferred", function(done) { - response_object = { - value: 1 - }; + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("foo"), + responseText: { + value: 1 + } + }); var def1 = $.Deferred(), def2 = $.Deferred(); @@ -67,16 +56,24 @@ define(function(require) { }); it("allows filtering response wrapper by hook, called only once", function(done) { - response_object = { - value: 1, - foo: 2 - }; + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("foo"), + responseText: { + value: 1, + foo: 2 + } + }); var hook_calls = 0; elgg.register_hook_handler(Ajax.RESPONSE_DATA_HOOK, 'path:foo', function (h, t, p, v) { hook_calls++; - expect(v).toEqual({value: 1, foo: 2}); + expect(v).toEqual({ + value: 1, + foo: 2, + status: 0 + }); expect(p.options.url).toBe('foo'); v.value = 3; return v; @@ -104,99 +101,109 @@ define(function(require) { $.each(['path', 'action', 'form', 'view'], function (i, method) { it("method " + method + "() sends special header", function() { ajax[method]('foo'); - expect(captured_options.headers).toEqual({ 'X-Elgg-Ajax-API' : '2' }); + expect(ajax._ajax_options.headers).toEqual({ 'X-Elgg-Ajax-API' : '2' }); }); it("method " + method + "() uses dataType json", function() { ajax[method]('foo'); - expect(captured_options.dataType).toEqual('json'); + expect(ajax._ajax_options.dataType).toEqual('json'); }); }); it("action() defaults to POST", function() { ajax.action('foo'); - expect(captured_options.method).toEqual('POST'); + expect(ajax._ajax_options.method).toEqual('POST'); }); $.each(['path', 'form', 'view'], function (i, method) { it(method + "() defaults to GET", function() { ajax[method]('foo'); - expect(captured_options.method).toEqual('GET'); + expect(ajax._ajax_options.method).toEqual('GET'); }); it(method + "(): non-empty object data changes default to POST", function() { ajax[method]('foo', { data: {bar: 'bar'} }); - expect(captured_options.method).toEqual('POST'); + expect(ajax._ajax_options.method).toEqual('POST'); }); it(method + "(): non-empty string data changes default to POST", function() { ajax[method]('foo', { data: '?bar=bar' }); - expect(captured_options.method).toEqual('POST'); + expect(ajax._ajax_options.method).toEqual('POST'); }); it(method + "(): empty string data leaves default as GET", function() { ajax[method]('foo', { data: '' }); - expect(captured_options.method).toEqual('GET'); + expect(ajax._ajax_options.method).toEqual('GET'); }); it(method + "(): empty object data leaves default as GET", function() { ajax[method]('foo', { data: {} }); - expect(captured_options.method).toEqual('GET'); + expect(ajax._ajax_options.method).toEqual('GET'); }); }); - it("allows altering value via hook", function() { + it("allows altering value via hook", function(done) { elgg.register_hook_handler(Ajax.REQUEST_DATA_HOOK, 'path:foo/bar', function (h, t, p, v) { v.arg3 = 3; return v; }, 900); + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("foo/bar/?arg1=1"), + responseText: { + value: 1, + foo: 2 + } + }); + ajax.path('/foo/bar/?arg1=1#target', { data: {arg2: 2} + }).done(function () { + expect(captured_hook.v).toEqual({arg2: 2, arg3: 3}); + expect(captured_hook.p.options.data).toEqual({arg2: 2, arg3: 3}); + done(); }); - expect(captured_options.data).toEqual({ + expect(ajax._ajax_options.data).toEqual({ arg2: 2, arg3: 3 }); - - expect(captured_hook.v).toEqual({arg2: 2, arg3: 3}); - expect(captured_hook.p.options.data).toEqual({arg2: 2, arg3: 3}); }); it("normalizes argument paths/URLs", function() { ajax.path('/foo/bar/?arg1=1#target'); - expect(captured_hook.t).toEqual('path:foo/bar'); - expect(captured_options.url).toEqual(root + 'foo/bar/?arg1=1'); + expect(ajax._fetch_args.hook_type).toEqual('path:foo/bar'); + expect(ajax._fetch_args.options.url).toEqual(root + 'foo/bar/?arg1=1'); ajax.path(root + 'foo/bar/?arg1=1#target'); - expect(captured_hook.t).toEqual('path:foo/bar'); - expect(captured_options.url).toEqual(root + 'foo/bar/?arg1=1'); + expect(ajax._fetch_args.hook_type).toEqual('path:foo/bar'); + expect(ajax._fetch_args.options.url).toEqual(root + 'foo/bar/?arg1=1'); ajax.action('/foo/bar/?arg1=1#target'); - expect(captured_hook.t).toEqual('action:foo/bar'); - expect(captured_options.url).toEqual(root + 'action/foo/bar/?arg1=1'); + expect(ajax._fetch_args.hook_type).toEqual('action:foo/bar'); + expect(ajax._fetch_args.options.url).toEqual(root + 'action/foo/bar/?arg1=1'); ajax.action(root + 'action/foo/bar/?arg1=1#target'); - expect(captured_hook.t).toEqual('action:foo/bar'); - expect(captured_options.url).toEqual(root + 'action/foo/bar/?arg1=1'); + expect(ajax._fetch_args.hook_type).toEqual('action:foo/bar'); + expect(ajax._fetch_args.options.url).toEqual(root + 'action/foo/bar/?arg1=1'); ajax.view('foo/bar?arg1=1'); - expect(captured_hook.t).toEqual('view:foo/bar'); - expect(captured_options.url).toEqual(root + 'ajax/view/foo/bar?arg1=1'); + expect(ajax._fetch_args.hook_type).toEqual('view:foo/bar'); + expect(ajax._fetch_args.options.url).toEqual(root + 'ajax/view/foo/bar?arg1=1'); ajax.form('/foo/bar/?arg1=1#target'); - expect(captured_hook.t).toEqual('form:foo/bar'); - expect(captured_options.url).toEqual(root + 'ajax/form/foo/bar/?arg1=1'); + expect(ajax._fetch_args.hook_type).toEqual('form:foo/bar'); + expect(ajax._fetch_args.options.url).toEqual(root + 'ajax/form/foo/bar/?arg1=1'); }); it("refuses to accept external URLs", function() { @@ -223,12 +230,12 @@ define(function(require) { var ts = elgg.security.token.__elgg_ts; ajax.action('foo'); - expect(captured_options.data.__elgg_ts).toBe(ts); + expect(ajax._ajax_options.data.__elgg_ts).toBe(ts); ajax.action('foo', { data: "?arg1=1" }); - expect(captured_options.data).toContain('__elgg_ts=' + ts); + expect(ajax._ajax_options.data).toContain('__elgg_ts=' + ts); }); it("does not add tokens if already in action URL", function() { @@ -237,7 +244,7 @@ define(function(require) { var url = elgg.security.addToken(root + 'action/foo'); ajax.action(url); - expect(captured_options.data.__elgg_ts).toBe(undefined); + expect(ajax._ajax_options.data.__elgg_ts).toBe(undefined); }); it("path() accepts empty argument for fetching home page", function() { @@ -268,16 +275,65 @@ define(function(require) { captured.deps = arg; }; - response_object = { - value: 1, - _elgg_msgs: { + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("foo"), + responseText: { + value: 1, + _elgg_msgs: { + error: ['fail'], + success: ['yay'] + }, + _elgg_deps: ['foo'] + } + }); + + ajax.path('foo').done(function () { + expect(captured).toEqual({ + msg: ['yay'], error: ['fail'], - success: ['yay'] - }, - _elgg_deps: ['foo'] + deps: ['foo'] + }); + + elgg.system_message = tmp_system_message; + elgg.register_error = tmp_register_error; + Ajax._require = tmp_require; + + done(); + }); + }); + + it("error handler still handles server-sent messages and dependencies", function (done) { + var tmp_system_message = elgg.system_message; + var tmp_register_error = elgg.register_error; + var tmp_require = Ajax._require; + var captured = {}; + + elgg.system_message = function (arg) { + captured.msg = arg; + }; + elgg.register_error = function (arg) { + captured.error = arg; + }; + Ajax._require = function (arg) { + captured.deps = arg; }; - ajax.path('foo').done(function () { + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("foo"), + status: 500, + responseText: { + value: null, + _elgg_msgs: { + error: ['fail'], + success: ['yay'] + }, + _elgg_deps: ['foo'] + } + }); + + ajax.path('foo').fail(function () { expect(captured).toEqual({ msg: ['yay'], error: ['fail'], @@ -292,6 +348,82 @@ define(function(require) { }); }); + it("outputs the generic error if no server-sent message", function (done) { + var tmp_register_error = elgg.register_error; + var captured = {}; + + elgg.register_error = function (arg) { + captured.error = arg; + }; + + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("foo"), + status: 500, + responseText: { + error: 'not seen by user' + } + }); + + ajax.path('foo').fail(function () { + expect(captured).toEqual({ + error: elgg.echo('ajax:error') + }); + + elgg.register_error = tmp_register_error; + + done(); + }); + }); + + it("copies data to jqXHR.AjaxData", function (done) { + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("foo"), + responseText: { + value: 1, + other: 2 + } + }); + + ajax.path('foo').done(function (value, textStatus, jqXHR) { + expect(jqXHR.AjaxData).toEqual({ + value: 1, + other: 2, + status: 0 + }); + done(); + }); + }); + + it("sets jqXHR.AjaxData.status to 0 or -1 depending on presence of server error", function (done) { + //$.mockjaxSettings.logging = true; + $.mockjax({ + url: elgg.normalize_url("good"), + responseText: { + value: 1 + } + }); + $.mockjax({ + url: elgg.normalize_url("bad"), + responseText: { + value: 1, + _elgg_msgs: { + error: ['fail'] + } + } + }); + + ajax.path('good').done(function (value, textStatus, jqXHR) { + expect(jqXHR.AjaxData.status).toBe(0); + + ajax.path('bad').done(function (value, textStatus, jqXHR) { + expect(jqXHR.AjaxData.status).toBe(-1); + done(); + }); + }); + }); + describe("ajax.objectify", function() { /** diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index ac9f0f80a32..06b6e72c59a 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -24,6 +24,8 @@ module.exports = function(config) { {pattern:'js/tests/*Test.js',included: false}, {pattern:'views/default/**/*.js',included:false}, + {pattern:'vendor/bower-asset/**/*.js',included:false}, + {pattern:'node_modules/**/*.js',included:false}, 'js/tests/requirejs.config.js' ], diff --git a/js/tests/requirejs.config.js b/js/tests/requirejs.config.js index 39cc2a52893..1f1e6c7d967 100644 --- a/js/tests/requirejs.config.js +++ b/js/tests/requirejs.config.js @@ -9,7 +9,9 @@ requirejs.config({ // Karma serves files from '/base' baseUrl: '/base/views/default/', paths: { - 'sprintf': '/base/vendor/bower-asset/sprintf/src/sprintf', + 'vendor': '../../vendor', + 'node_modules': '../../node_modules', + 'jquery-mockjax': '../../node_modules/jquery-mockjax/dist/jquery.mockjax', }, // ask Require.js to load these files (all our tests) diff --git a/mod/developers/actions/developers/ajax_demo.php b/mod/developers/actions/developers/ajax_demo.php index 7eaa111b79b..ecd5f4a544a 100644 --- a/mod/developers/actions/developers/ajax_demo.php +++ b/mod/developers/actions/developers/ajax_demo.php @@ -9,11 +9,10 @@ function developers_ajax_demo_alter($hook, $type, Elgg\Services\AjaxResponse $v, $v->getData()->server_response_altered = 2; } - register_error('Error from ajax demo response hook'); + register_error('Hello from ajax_response hook'); } elgg_register_plugin_hook_handler('ajax_response', 'action:developers/ajax_demo', 'developers_ajax_demo_alter'); - // typical ajax action: elgg_ajax_gatekeeper(); @@ -21,7 +20,7 @@ function developers_ajax_demo_alter($hook, $type, Elgg\Services\AjaxResponse $v, $arg1 = (int)get_input('arg1'); $arg2 = (int)get_input('arg2'); -system_message('Success message from ajax demo'); +system_message('Hello from action'); echo json_encode([ 'sum' => $arg1 + $arg2, diff --git a/mod/developers/views/default/developers/ajax_demo.js b/mod/developers/views/default/developers/ajax_demo.js index 6c7264abe5f..241e43599aa 100644 --- a/mod/developers/views/default/developers/ajax_demo.js +++ b/mod/developers/views/default/developers/ajax_demo.js @@ -24,8 +24,9 @@ define(function(require) { } ); - var got_metadata_from_server = false, - num_hook_calls = 0; + var got_metadata_from_server = false; + + log("Expecting 6 passes..."); // alter request data response for the action elgg.register_hook_handler( @@ -40,8 +41,6 @@ define(function(require) { // alter the return value data.value.altered_value = true; - num_hook_calls++; - return data; } ); @@ -51,21 +50,21 @@ define(function(require) { ajax.path('developers_ajax_demo') .then(function (html_page) { if (html_page.indexOf('path demo') != -1) { - log("path() successful!"); + log("PASS path()"); return ajax.view('developers/ajax_demo.html'); } }) .then(function (div) { if (div.indexOf('view demo') != -1) { - log("view() successful!"); + log("PASS view()"); return ajax.form('developers/ajax_demo'); } }) .then(function (form) { if (form.indexOf('form demo') != -1) { - log("form() successful!"); + log("PASS form()"); return ajax.action('developers/ajax_demo', { data: {arg1: 2, arg2: 3}, @@ -76,12 +75,16 @@ define(function(require) { } }) .then(function (obj) { - if (obj.sum === 5 - && got_metadata_from_server - && obj.altered_value - && num_hook_calls == 1) { - log("action() successful!"); - alert('Success!'); + if (obj.sum === 5) { + log("PASS action()"); + } + if (got_metadata_from_server) { + log("PASS got metadata from server response hook"); } + if (obj.altered_value) { + log("PASS client response hook altered value"); + } + + alert('Success!'); }); }); \ No newline at end of file diff --git a/package.json b/package.json index 97272e9786c..8c4606d3f09 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,10 @@ "grunt-contrib-watch": "~0.6.1", "grunt-exec": "~0.4.6", "grunt-open": "~0.2.3", + "jquery-mockjax": "^2.1.1", "karma": "^0.12.32", - "karma-jasmine": "~0.2.0", "karma-chrome-launcher": "^0.2.3", + "karma-jasmine": "~0.2.0", "karma-phantomjs-launcher": "~0.1", "karma-requirejs": "~0.2", "load-grunt-config": "~0.16.0", diff --git a/views/default/elgg/Ajax.js b/views/default/elgg/Ajax.js index f7274e73e63..f467893dd21 100644 --- a/views/default/elgg/Ajax.js +++ b/views/default/elgg/Ajax.js @@ -21,23 +21,20 @@ define(function (require) { use_spinner = elgg.isNullOrUndefined(use_spinner) ? true : !!use_spinner; + var that = this; + /** * Fetch a value from an Ajax endpoint. * - * Note that this function does not support the array form of "success". + * @param {Object} options See {@link jQuery#ajax}. The default method is "GET" (or "POST" for actions). * - * To request the response be cached, set options.method to "GET" and options.data.elgg_response_ttl - * to a number of seconds. + * data: {Object} Data to send to the server (optional). If set to a string (e.g. $.serialize) + * then the request hook will not be called. * - * To bypass downloading system messages with the response, set options.data.elgg_fetch_messages = 0. + * elgg_response_ttl: {Number} Sets response max-age. To be effective, you must also + * set option.method to "GET". * - * @param {Object} options See {@link jQuery#ajax}. The default method is "GET" (or "POST" for actions). - * - * url : {String} Path of the Ajax API endpoint (required) - * error : {Function} Error handler. Default is elgg.ajax.handleAjaxError. To cancel this altogether, - * pass in function(){}. - * data : {Object} Data to send to the server (optional). If set to a string (e.g. $.serialize) - * then the request hook will not be called. + * elgg_fetch_message: {Number} Set to 0 to bypass downloading server messages * * @param {String} hook_type Type of the plugin hooks. If missing, the hooks will not trigger. * @@ -46,24 +43,46 @@ define(function (require) { function fetch(options, hook_type) { var orig_options, params, - unwrapped = false, - result; - - function unwrap_data(data) { - // between the deferred and a success function, make sure this runs only once. - if (!unwrapped) { - var params = { - options: orig_options - }; - if (hook_type) { - data = elgg.trigger_hook(Ajax.RESPONSE_DATA_HOOK, hook_type, params, data); + jqXHR, + metadata_extracted = false, + error_displayed = false; + + /** + * Show messages and require dependencies + * + * @param {Object} data + */ + function extract_metadata(data) { + if (!metadata_extracted) { + var m = data._elgg_msgs; + if (m && m.error) { + elgg.register_error(m.error); + error_displayed = true; + data.status = -1; + } else { + data.status = 0; } - result = data.value; - unwrapped = true; + m && m.success && elgg.system_message(m.success); + delete data._elgg_msgs; + + var deps = data._elgg_deps; + deps && deps.length && Ajax._require(deps); + delete data._elgg_deps; + + metadata_extracted = true; } - return result; } + /** + * For unit testing + * @type {{options: Object, hook_type: String}} + * @private + */ + that._fetch_args = { + options: options, + hook_type: hook_type + }; + hook_type = hook_type || ''; if (!$.isPlainObject(options)) { @@ -106,38 +125,77 @@ define(function (require) { } } - if ($.isArray(options.success)) { - throw new Error('The array form of options.success is not supported'); - } - - if (elgg.isFunction(options.success)) { - options.success = function (data) { - data = unwrap_data(data); - orig_options.success(data); - }; - } - if (use_spinner) { options.beforeSend = function () { - orig_options.beforeSend && orig_options.beforeSend(); + orig_options.beforeSend && orig_options.beforeSend.apply(null, arguments); spinner.start(); }; options.complete = function () { spinner.stop(); - orig_options.complete && orig_options.complete(); + orig_options.complete && orig_options.complete.apply(null, arguments); }; } if (!options.error) { - options.error = elgg.ajax.handleAjaxError; + options.error = function (jqXHR, textStatus, errorThrown) { + if (!jqXHR.getAllResponseHeaders()) { + // user aborts (like refresh or navigate) do not have headers + return; + } + + try { + var data = $.parseJSON(jqXHR.responseText); + if ($.isPlainObject(data)) { + extract_metadata(data); + } + } catch (e) { + if (window.console) { + console.warn(e.message); + } + } + + if (!error_displayed) { + elgg.register_error(elgg.echo('ajax:error')); + } + }; } + options.dataFilter = function (data, type) { + if (type !== 'json') { + return data; + } + + data = $.parseJSON(data); + + extract_metadata(data); + + var params = { + options: orig_options + }; + if (hook_type) { + data = elgg.trigger_hook(Ajax.RESPONSE_DATA_HOOK, hook_type, params, data); + } + + jqXHR.AjaxData = data; + + return JSON.stringify(data.value); + }; + options.url = elgg.normalize_url(options.url); options.headers = { 'X-Elgg-Ajax-API': '2' }; - return $.ajax(options).then(unwrap_data); + /** + * For unit testing + * @type {Object} + * @private + */ + that._ajax_options = options; + + jqXHR = $.ajax(options); + + return jqXHR; } /** @@ -318,27 +376,6 @@ define(function (require) { */ Ajax.RESPONSE_DATA_HOOK = 'ajax_response_data'; - /** - * Sets up response hook for all responses - * @private For testing - */ - Ajax._init_hooks = function () { - elgg.register_hook_handler(Ajax.RESPONSE_DATA_HOOK, 'all', function (name, type, params, data) { - var m = data._elgg_msgs; - m && m.error && elgg.register_error(m.error); - m && m.success && elgg.system_message(m.success); - delete data._elgg_msgs; - - var deps = data._elgg_deps; - deps && deps.length && Ajax._require(deps); - delete data._elgg_deps; - - return data; - }); - }; - - Ajax._init_hooks(); - /** * @private For testing */