diff --git a/core/core.libraries.yml b/core/core.libraries.yml index b26bf5fff6..d7a0461893 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -370,6 +370,7 @@ drupal.ajax: - core/drupal.progress - core/once - core/tabbable + - core/loadjs drupal.announce: version: VERSION diff --git a/core/lib/Drupal/Core/Ajax/AddJsCommand.php b/core/lib/Drupal/Core/Ajax/AddJsCommand.php new file mode 100644 index 0000000000..e94d6f4164 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/AddJsCommand.php @@ -0,0 +1,60 @@ +scripts = $scripts; + $this->selector = $selector; + } + + /** + * {@inheritdoc} + */ + public function render() { + return [ + 'command' => 'add_js', + 'selector' => $this->selector, + 'data' => $this->scripts, + ]; + } + +} diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php index 4f6acbf966..eaeed18ef2 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php @@ -174,11 +174,11 @@ protected function buildAttachmentsCommands(AjaxResponse $response, Request $req } if ($js_assets_header) { $js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header); - $resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array)); + $resource_commands[] = new AddJsCommand(array_column($js_header_render_array, '#attributes'), 'head'); } if ($js_assets_footer) { $js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer); - $resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array)); + $resource_commands[] = new AddJsCommand(array_column($js_footer_render_array, '#attributes')); } foreach (array_reverse($resource_commands) as $resource_command) { $response->addCommand($resource_command, TRUE); diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js index 08923ae52d..8024a15a30 100644 --- a/core/misc/ajax.es6.js +++ b/core/misc/ajax.es6.js @@ -11,7 +11,14 @@ * included to provide Ajax capabilities. */ -(function ($, window, Drupal, drupalSettings, { isFocusable, tabbable }) { +(function ( + $, + window, + Drupal, + drupalSettings, + loadjs, + { isFocusable, tabbable }, +) { /** * Attaches the Ajax behavior to each Ajax form element. * @@ -537,10 +544,23 @@ } } - return ajax.success(response, status); + return ( + // Ensure that the return of the success callback is a Promise. + // When the return is a Promise, using resolve will unwrap it, and + // when the return is not a Promise we make sure it can be used as + // one. This is useful for code that overrides the success method. + Promise.resolve(ajax.success(response, status)) + // Ajaxing status is back to false when all the ajax commands have + // finished executing. + .then(() => { + ajax.ajaxing = false; + }) + ); }, - complete(xmlhttprequest, status) { + error(xmlhttprequest, status, error) { ajax.ajaxing = false; + }, + complete(xmlhttprequest, status) { if (status === 'error' || status === 'parsererror') { return ajax.error(xmlhttprequest, ajax.url); } @@ -980,54 +1000,77 @@ // Track if any command is altering the focus so we can avoid changing the // focus set by the Ajax command. let focusChanged = false; - Object.keys(response || {}).forEach((i) => { - if (response[i].command && this.commands[response[i].command]) { - this.commands[response[i].command](this, response[i], status); - if ( - (response[i].command === 'invoke' && - response[i].method === 'focus') || - response[i].command === 'focusFirst' - ) { - focusChanged = true; - } - } - }); - - // If the focus hasn't be changed by the ajax commands, try to refocus the - // triggering element or one of its parents if that element does not exist - // anymore. - if ( - !focusChanged && - this.element && - !$(this.element).data('disable-refocus') - ) { - let target = false; - - for (let n = elementParents.length - 1; !target && n >= 0; n--) { - target = document.querySelector( - `[data-drupal-selector="${elementParents[n].getAttribute( - 'data-drupal-selector', - )}"]`, - ); - } - - if (target) { - $(target).trigger('focus'); - } - } - - // Reattach behaviors, if they were detached in beforeSerialize(). The - // attachBehaviors() called on the new content from processing the response - // commands is not sufficient, because behaviors from the entire form need - // to be reattached. - if (this.$form && document.body.contains(this.$form.get(0))) { - const settings = this.settings || drupalSettings; - Drupal.attachBehaviors(this.$form.get(0), settings); - } - - // Remove any response-specific settings so they don't get used on the next - // call by mistake. - this.settings = null; + return ( + Object.keys(response || {}) + .reduce( + // Add all commands to a single execution queue. + (executionQueue, key) => + executionQueue.then(() => { + const { command } = response[key]; + if (command && this.commands[command]) { + if ( + (command === 'invoke' && response[key].method === 'focus') || + response[key].command === 'focusFirst' + ) { + focusChanged = true; + } + + // When a commands returns a promise, the execution of the rest + // of the commands will stop until this promise has been + // fulfilled. Usually it is used to wait until the JavaScript + // added by the 'add_js' command has loaded before continuing + // the execution of the commands. + return this.commands[command](this, response[key], status); + } + }), + Promise.resolve(), + ) + // If the focus hasn't been changed by the ajax commands, try to refocus the + // triggering element or one of its parents if that element does not exist + // anymore. + .then(() => { + if ( + !focusChanged && + this.element && + !$(this.element).data('disable-refocus') + ) { + let target = false; + + for (let n = elementParents.length - 1; !target && n >= 0; n--) { + target = document.querySelector( + `[data-drupal-selector="${elementParents[n].getAttribute( + 'data-drupal-selector', + )}"]`, + ); + } + if (target) { + $(target).trigger('focus'); + } + } + // Reattach behaviors, if they were detached in beforeSerialize(). The + // attachBehaviors() called on the new content from processing the response + // commands is not sufficient, because behaviors from the entire form need + // to be reattached. + if (this.$form && document.body.contains(this.$form.get(0))) { + const settings = this.settings || drupalSettings; + Drupal.attachBehaviors(this.$form.get(0), settings); + } + // Remove any response-specific settings so they don't get used on the next + // call by mistake. + this.settings = null; + }) + .catch((error) => + // eslint-disable-next-line no-console + console.error( + Drupal.t( + 'An error occurred during the execution of the Ajax response: !error', + { + '!error': error, + }, + ), + ), + ) + ); }; /** @@ -1610,5 +1653,71 @@ } messages.add(response.message, response.messageOptions); }, + + /** + * Command to add JS. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {Array} response.data + * A string that contains the JS files to be added. + * @param {number} [status] + * The XMLHttpRequest status. + */ + add_js(ajax, response, status) { + const parentEl = document.querySelector(response.selector || 'body'); + const settings = ajax.settings || drupalSettings; + const allUniqueBundleIDs = response.data.map((script) => { + // loadjs requires a unique ID, AJAX instances' `instanceIndex` are + // guaranteed to be unique. + // @see Drupal.behaviors.AJAX.detach + const uniqueBundleID = script.src + ajax.instanceIndex; + loadjs(script.src, uniqueBundleID, { + // By default, dynamically added scripts are marked as async. Only + // explicitly marked async scripts should be loaded async. + async: false, + before(path, scriptEl) { + // This allows all attributes to be added, like defer, async and + // crossorigin. + Object.keys(script).forEach((attributeKey) => { + scriptEl.setAttribute(attributeKey, script[attributeKey]); + }); + + // By default, loadjs appends the script to the head. However, we + // want to add the script to the parent specified. This is just for + // consistency, because it doesn't actually matter for the script + // where it is added. Developers however expect library assets to + // show up where they declared them, so this makes things consistent + // with the assets that are not loaded with ajax. + parentEl.appendChild(scriptEl); + // Return `false` to bypass loadjs' default DOM insertion mechanism. + return false; + }, + }); + return uniqueBundleID; + }); + // Returns the promise so that the next AJAX command waits on the completion + // of this one to execute, ensuring the JS is loaded before executing. + return new Promise((resolve, reject) => { + loadjs.ready(allUniqueBundleIDs, { + success() { + Drupal.attachBehaviors(parentEl, settings); + // All JS files were loaded and new and old behaviors have + // been attached, resolve the promise and let the rest of the commands + // execute. + resolve(); + }, + error(depsNotFound) { + const message = Drupal.t( + `The following files could not be loaded: @deps`, + { '@deps': depsNotFound.join(', ') }, + ); + reject(message); + }, + }); + }); + }, }; -})(jQuery, window, Drupal, drupalSettings, window.tabbable); +})(jQuery, window, Drupal, drupalSettings, loadjs, window.tabbable); diff --git a/core/misc/ajax.js b/core/misc/ajax.js index b2f572c9d5..83341bc852 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -5,7 +5,7 @@ * @preserve **/ -(function ($, window, Drupal, drupalSettings, _ref) { +(function ($, window, Drupal, drupalSettings, loadjs, _ref) { let { isFocusable, tabbable @@ -229,12 +229,16 @@ } } - return ajax.success(response, status); + return Promise.resolve(ajax.success(response, status)).then(() => { + ajax.ajaxing = false; + }); }, - complete(xmlhttprequest, status) { + error(xmlhttprequest, status, error) { ajax.ajaxing = false; + }, + complete(xmlhttprequest, status) { if (status === 'error' || status === 'parsererror') { return ajax.error(xmlhttprequest, ajax.url); } @@ -426,34 +430,40 @@ $(this.element).prop('disabled', false); const elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray(); let focusChanged = false; - Object.keys(response || {}).forEach(i => { - if (response[i].command && this.commands[response[i].command]) { - this.commands[response[i].command](this, response[i], status); + return Object.keys(response || {}).reduce((executionQueue, key) => executionQueue.then(() => { + const { + command + } = response[key]; - if (response[i].command === 'invoke' && response[i].method === 'focus' || response[i].command === 'focusFirst') { + if (command && this.commands[command]) { + if (command === 'invoke' && response[key].method === 'focus' || response[key].command === 'focusFirst') { focusChanged = true; } + + return this.commands[command](this, response[key], status); } - }); + }), Promise.resolve()).then(() => { + if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) { + let target = false; - if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) { - let target = false; + for (let n = elementParents.length - 1; !target && n >= 0; n--) { + target = document.querySelector(`[data-drupal-selector="${elementParents[n].getAttribute('data-drupal-selector')}"]`); + } - for (let n = elementParents.length - 1; !target && n >= 0; n--) { - target = document.querySelector(`[data-drupal-selector="${elementParents[n].getAttribute('data-drupal-selector')}"]`); + if (target) { + $(target).trigger('focus'); + } } - if (target) { - $(target).trigger('focus'); + if (this.$form && document.body.contains(this.$form.get(0))) { + const settings = this.settings || drupalSettings; + Drupal.attachBehaviors(this.$form.get(0), settings); } - } - - if (this.$form && document.body.contains(this.$form.get(0))) { - const settings = this.settings || drupalSettings; - Drupal.attachBehaviors(this.$form.get(0), settings); - } - this.settings = null; + this.settings = null; + }).catch(error => console.error(Drupal.t('An error occurred during the execution of the Ajax response: !error', { + '!error': error + }))); }; Drupal.Ajax.prototype.getEffect = function (response) { @@ -664,7 +674,44 @@ } messages.add(response.message, response.messageOptions); + }, + + add_js(ajax, response, status) { + const parentEl = document.querySelector(response.selector || 'body'); + const settings = ajax.settings || drupalSettings; + const allUniqueBundleIDs = response.data.map(script => { + const uniqueBundleID = script.src + ajax.instanceIndex; + loadjs(script.src, uniqueBundleID, { + async: false, + + before(path, scriptEl) { + Object.keys(script).forEach(attributeKey => { + scriptEl.setAttribute(attributeKey, script[attributeKey]); + }); + parentEl.appendChild(scriptEl); + return false; + } + + }); + return uniqueBundleID; + }); + return new Promise((resolve, reject) => { + loadjs.ready(allUniqueBundleIDs, { + success() { + Drupal.attachBehaviors(parentEl, settings); + resolve(); + }, + + error(depsNotFound) { + const message = Drupal.t(`The following files could not be loaded: @deps`, { + '@deps': depsNotFound.join(', ') + }); + reject(message); + } + + }); + }); } }; -})(jQuery, window, Drupal, drupalSettings, window.tabbable); \ No newline at end of file +})(jQuery, window, Drupal, drupalSettings, loadjs, window.tabbable); \ No newline at end of file diff --git a/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php b/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php index da99c5c2ab..188aef4ff4 100644 --- a/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php +++ b/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php @@ -110,7 +110,7 @@ public function testCommentForm_2698811() { // Confirm that CKEditor loaded. $javascript = << 0; + return window.CKEDITOR && Object.keys(CKEDITOR.instances).length > 0; }()) JS; $this->assertJsCondition($javascript); diff --git a/core/modules/media_library/js/media_library.ui.es6.js b/core/modules/media_library/js/media_library.ui.es6.js index 95bc01ac3f..569b443121 100644 --- a/core/modules/media_library/js/media_library.ui.es6.js +++ b/core/modules/media_library/js/media_library.ui.es6.js @@ -83,37 +83,21 @@ // Override the AJAX success callback to shift focus to the media // library content. ajaxObject.success = function (response, status) { - // Remove the progress element. - if (this.progress.element) { - $(this.progress.element).remove(); - } - if (this.progress.object) { - this.progress.object.stopMonitoring(); - } - $(this.element).prop('disabled', false); - - // Execute the AJAX commands. - Object.keys(response || {}).forEach((i) => { - if (response[i].command && this.commands[response[i].command]) { - this.commands[response[i].command](this, response[i], status); + return Promise.resolve( + Drupal.Ajax.prototype.success.call(ajaxObject, response, status), + ).then(() => { + // Set focus to the first tabbable element in the media library + // content. + const mediaLibraryContent = document.getElementById( + 'media-library-content', + ); + if (mediaLibraryContent) { + const tabbableContent = tabbable(mediaLibraryContent); + if (tabbableContent.length) { + tabbableContent[0].focus(); + } } }); - - // Set focus to the first tabbable element in the media library - // content. - const mediaLibraryContent = document.getElementById( - 'media-library-content', - ); - if (mediaLibraryContent) { - const tabbableContent = tabbable(mediaLibraryContent); - if (tabbableContent.length) { - tabbableContent[0].focus(); - } - } - - // Remove any response-specific settings so they don't get used on - // the next call by mistake. - this.settings = null; }; ajaxObject.execute(); diff --git a/core/modules/media_library/js/media_library.ui.js b/core/modules/media_library/js/media_library.ui.js index 96ed3914fa..b7d5c61386 100644 --- a/core/modules/media_library/js/media_library.ui.js +++ b/core/modules/media_library/js/media_library.ui.js @@ -42,31 +42,17 @@ }); ajaxObject.success = function (response, status) { - if (this.progress.element) { - $(this.progress.element).remove(); - } + return Promise.resolve(Drupal.Ajax.prototype.success.call(ajaxObject, response, status)).then(() => { + const mediaLibraryContent = document.getElementById('media-library-content'); - if (this.progress.object) { - this.progress.object.stopMonitoring(); - } + if (mediaLibraryContent) { + const tabbableContent = tabbable(mediaLibraryContent); - $(this.element).prop('disabled', false); - Object.keys(response || {}).forEach(i => { - if (response[i].command && this.commands[response[i].command]) { - this.commands[response[i].command](this, response[i], status); + if (tabbableContent.length) { + tabbableContent[0].focus(); + } } }); - const mediaLibraryContent = document.getElementById('media-library-content'); - - if (mediaLibraryContent) { - const tabbableContent = tabbable(mediaLibraryContent); - - if (tabbableContent.length) { - tabbableContent[0].focus(); - } - } - - this.settings = null; }; ajaxObject.execute(); diff --git a/core/modules/quickedit/js/quickedit.es6.js b/core/modules/quickedit/js/quickedit.es6.js index fe0dc6a8b6..d399a13236 100644 --- a/core/modules/quickedit/js/quickedit.es6.js +++ b/core/modules/quickedit/js/quickedit.es6.js @@ -180,16 +180,9 @@ url: Drupal.url('quickedit/attachments'), submit: { 'editors[]': missingEditors }, }); - // Implement a scoped insert AJAX command: calls the callback after all AJAX - // command functions have been executed (hence the deferred calling). - const realInsert = Drupal.AjaxCommands.prototype.insert; - loadEditorsAjax.commands.insert = function (ajax, response, status) { - _.defer(callback); - realInsert(ajax, response, status); - }; // Trigger the AJAX request, which will should return AJAX commands to // insert any missing attachments. - loadEditorsAjax.execute(); + loadEditorsAjax.execute().then(callback); } /** diff --git a/core/modules/quickedit/js/quickedit.js b/core/modules/quickedit/js/quickedit.js index 5b7ef22fb9..91d07d9acc 100644 --- a/core/modules/quickedit/js/quickedit.js +++ b/core/modules/quickedit/js/quickedit.js @@ -82,15 +82,7 @@ 'editors[]': missingEditors } }); - const realInsert = Drupal.AjaxCommands.prototype.insert; - - loadEditorsAjax.commands.insert = function (ajax, response, status) { - _.defer(callback); - - realInsert(ajax, response, status); - }; - - loadEditorsAjax.execute(); + loadEditorsAjax.execute().then(callback); } function initializeEntityContextualLink(contextualLink) { diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml index 9d390854bb..eb4ae93d92 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml @@ -33,3 +33,12 @@ focus.first: dependencies: - core/drupal - core/once + +command_promise: + version: VERSION + js: + js/command_promise-ajax.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupal.ajax diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml index 01bf512adb..0ee765e664 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml @@ -93,3 +93,11 @@ ajax_test.focus_first_form: _form: '\Drupal\ajax_test\Form\AjaxTestFocusFirstForm' requirements: _access: 'TRUE' + +ajax_test.promise: + path: '/ajax-test/promise-form' + defaults: + _title: 'Ajax Form Command Promise' + _form: '\Drupal\ajax_test\Form\AjaxTestFormPromise' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.es6.js b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.es6.js new file mode 100644 index 0000000000..8f4e40d85b --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.es6.js @@ -0,0 +1,31 @@ +/** + * @file + * Testing behavior for the add_js command. + */ + +(($, Drupal) => { + /** + * Test Ajax execution Order. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.Ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.selector + * A jQuery selector string. + * + * @return {Promise} + * The promise that will resolve once this command has finished executing. + */ + Drupal.AjaxCommands.prototype.ajaxCommandReturnPromise = function ( + ajax, + response, + ) { + return new Promise((resolve, reject) => { + setTimeout(() => { + this.insert(ajax, response); + resolve(); + }, Math.random() * 500); + }); + }; +})(jQuery, Drupal); diff --git a/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.js b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.js new file mode 100644 index 0000000000..162ac73f3c --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.js @@ -0,0 +1,17 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(($, Drupal) => { + Drupal.AjaxCommands.prototype.ajaxCommandReturnPromise = function (ajax, response) { + return new Promise((resolve, reject) => { + setTimeout(() => { + this.insert(ajax, response); + resolve(); + }, Math.random() * 500); + }); + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/system/tests/modules/ajax_test/src/Ajax/AjaxTestCommandReturnPromise.php b/core/modules/system/tests/modules/ajax_test/src/Ajax/AjaxTestCommandReturnPromise.php new file mode 100644 index 0000000000..fbb0dd94b7 --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/src/Ajax/AjaxTestCommandReturnPromise.php @@ -0,0 +1,26 @@ + 'ajaxCommandReturnPromise', + 'method' => 'append', + 'selector' => $this->selector, + 'data' => $this->getRenderedContent(), + 'settings' => $this->settings, + ]; + } + +} diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestFormPromise.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestFormPromise.php new file mode 100644 index 0000000000..038b0b5bcd --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestFormPromise.php @@ -0,0 +1,73 @@ +'; + $form['custom']['#suffix'] = ''; + + // Button to test for the execution order of Ajax commands. + $form['test_execution_order_button'] = [ + '#type' => 'submit', + '#value' => $this->t('Execute commands button'), + '#button_type' => 'primary', + '#ajax' => [ + 'callback' => [static::class, 'executeCommands'], + 'progress' => [ + 'type' => 'throbber', + 'message' => NULL, + ], + 'wrapper' => 'ajax_test_form_promise_wrapper', + ], + ]; + return $form; + } + + /** + * Ajax callback for the "Execute commands button" button. + */ + public static function executeCommands(array $form, FormStateInterface $form_state) { + $selector = '#ajax_test_form_promise_wrapper'; + $response = new AjaxResponse(); + + $response->addCommand(new AppendCommand($selector, '1')); + $response->addCommand(new AjaxTestCommandReturnPromise($selector, '2')); + $response->addCommand(new AppendCommand($selector, '3')); + $response->addCommand(new AppendCommand($selector, '4')); + $response->addCommand(new AjaxTestCommandReturnPromise($selector, '5')); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // An empty implementation, as we never submit the actual form. + } + +} diff --git a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php index de95fb5761..669037f82d 100644 --- a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php +++ b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php @@ -4,10 +4,9 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\Ajax\AddCssCommand; +use Drupal\Core\Ajax\AddJsCommand; use Drupal\Core\Ajax\AlertCommand; -use Drupal\Core\Ajax\AppendCommand; use Drupal\Core\Ajax\HtmlCommand; -use Drupal\Core\Ajax\PrependCommand; use Drupal\Core\Ajax\SettingsCommand; use Drupal\Core\Asset\AttachedAssets; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; @@ -61,8 +60,8 @@ public function testOrder() { [$js_assets_header, $js_assets_footer] = $asset_resolver->getJsAssets($assets, FALSE); $js_header_render_array = $js_collection_renderer->render($js_assets_header); $js_footer_render_array = $js_collection_renderer->render($js_assets_footer); - $expected_commands[2] = new PrependCommand('head', $js_header_render_array); - $expected_commands[3] = new AppendCommand('body', $js_footer_render_array); + $expected_commands[2] = new AddJsCommand(array_column($js_header_render_array, '#attributes'), 'head'); + $expected_commands[3] = new AddJsCommand(array_column($js_footer_render_array, '#attributes')); $expected_commands[4] = new HtmlCommand('body', 'Hello, world!'); // Verify AJAX command order — this should always be the order: diff --git a/core/tests/Drupal/Nightwatch/Tests/ajaxExecutionOrderTest.js b/core/tests/Drupal/Nightwatch/Tests/ajaxExecutionOrderTest.js new file mode 100644 index 0000000000..da54f6015a --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Tests/ajaxExecutionOrderTest.js @@ -0,0 +1,34 @@ +module.exports = { + '@tags': ['core', 'ajax'], + before(browser) { + browser.drupalInstall().drupalLoginAsAdmin(() => { + browser + .drupalRelativeURL('/admin/modules') + .setValue('input[type="search"]', 'Ajax test') + .waitForElementVisible('input[name="modules[ajax_test][enable]"]', 1000) + .click('input[name="modules[ajax_test][enable]"]') + .submitForm('input[type="submit"]') // Submit module form. + .waitForElementVisible( + '.system-modules-confirm-form input[value="Continue"]', + 2000, + ) + .submitForm('input[value="Continue"]') // Confirm installation of dependencies. + .waitForElementVisible('.system-modules', 10000); + }); + }, + after(browser) { + browser.drupalUninstall(); + }, + 'Test Execution Order': (browser) => { + browser + .drupalRelativeURL('/ajax-test/promise-form') + .waitForElementVisible('body', 1000) + .click('[data-drupal-selector="edit-test-execution-order-button"]') + .waitForElementVisible('#ajax_test_form_promise_wrapper', 1000) + .assert.containsText( + '#ajax_test_form_promise_wrapper', + '12345', + 'Ajax commands execution order confirmed', + ); + }, +};