diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js index 739afa19747bc2fadafab2228c36d6b55e897706..1dae116a801f2aff344beb1827dbd0d01b9c406e 100644 --- a/core/misc/ajax.es6.js +++ b/core/misc/ajax.es6.js @@ -1010,6 +1010,60 @@ throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage); }; + /** + * Provide a wrapper for new content via Ajax. + * + * Wrap the inserted markup when inserting multiple root elements with an + * ajax effect. + * + * @param {jQuery} $newContent + * Response elements after parsing. + * @param {Drupal.Ajax} ajax + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * + * @deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. + * Use data with desired wrapper. See https://www.drupal.org/node/2974880. + * + * @todo Add deprecation warning after it is possible. For more information + * see: https://www.drupal.org/project/drupal/issues/2973400 + * + * @see https://www.drupal.org/node/2940704 + */ + Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) => ( + (response.effect || ajax.effect) !== 'none' && + $newContent.filter( + i => + !( + // We can not consider HTML comments or whitespace text as separate + // roots, since they do not cause visual regression with effect. + $newContent[i].nodeName === '#comment' || + ($newContent[i].nodeName === '#text' && /^(\s|\n|\r)*$/.test($newContent[i].textContent)) + ), + ).length > 1 + ? Drupal.theme('ajaxWrapperMultipleRootElements', $newContent) + : $newContent + ); + + /** + * Provide a wrapper for multiple root elements via Ajax. + * + * @param {jQuery} $elements + * Response elements after parsing. + * + * @deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. + * Use data with desired wrapper. See https://www.drupal.org/node/2974880. + * + * @todo Add deprecation warning after it is possible. For more information + * see: https://www.drupal.org/project/drupal/issues/2973400 + * + * @see https://www.drupal.org/node/2940704 + */ + Drupal.theme.ajaxWrapperMultipleRootElements = $elements => ( + $('
').append($elements) + ); + /** * @typedef {object} Drupal.AjaxCommands~commandDefinition * @@ -1056,39 +1110,24 @@ * A optional jQuery selector string. * @param {object} [response.settings] * An optional array of settings that will be used. - * @param {number} [status] - * The XMLHttpRequest status. */ - insert(ajax, response, status) { + insert(ajax, response) { // Get information from the response. If it is not there, default to // our presets. const $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper); const method = response.method || ajax.method; const effect = ajax.getEffect(response); - let settings; - - // We don't know what response.data contains: it might be a string of text - // without HTML, so don't rely on jQuery correctly interpreting - // $(response.data) as new HTML rather than a CSS selector. Also, if - // response.data contains top-level text nodes, they get lost with either - // $(response.data) or $('
').replaceWith(response.data). - const $newContentWrapped = $('
').html(response.data); - let $newContent = $newContentWrapped.contents(); - - // For legacy reasons, the effects processing code assumes that - // $newContent consists of a single top-level element. Also, it has not - // been sufficiently tested whether attachBehaviors() can be successfully - // called with a context object that includes top-level text nodes. - // However, to give developers full control of the HTML appearing in the - // page, and to enable Ajax content to be inserted in places where
- // elements are not allowed (e.g., within , , and - // parents), we check if the new content satisfies the requirement - // of a single top-level element, and only use the container
created - // above when it doesn't. For more information, please see - // https://www.drupal.org/node/736066. - if ($newContent.length !== 1 || $newContent.get(0).nodeType !== 1) { - $newContent = $newContentWrapped; - } + + // Apply any settings from the returned JSON if available. + const settings = response.settings || ajax.settings || drupalSettings; + + // Parse response.data into an element collection. + let $newContent = $($.parseHTML(response.data, document, true)); + // For backward compatibility, in some cases a wrapper will be added. This + // behavior will be removed before Drupal 9.0.0. If different behavior is + // needed, the theme functions can be overriden. + // @see https://www.drupal.org/node/2940704 + $newContent = Drupal.theme('ajaxWrapperNewContent', $newContent, ajax, response); // If removing content from the wrapper, detach behaviors first. switch (method) { @@ -1097,8 +1136,10 @@ case 'replaceAll': case 'empty': case 'remove': - settings = response.settings || ajax.settings || drupalSettings; Drupal.detachBehaviors($wrapper.get(0), settings); + break; + default: + break; } // Add the new content to the page. @@ -1111,10 +1152,11 @@ // Determine which effect to use and what content will receive the // effect, then show the new content. - if ($newContent.find('.ajax-new-content').length > 0) { - $newContent.find('.ajax-new-content').hide(); + const $ajaxNewContent = $newContent.find('.ajax-new-content'); + if ($ajaxNewContent.length) { + $ajaxNewContent.hide(); $newContent.show(); - $newContent.find('.ajax-new-content')[effect.showEffect](effect.showSpeed); + $ajaxNewContent[effect.showEffect](effect.showSpeed); } else if (effect.showEffect !== 'show') { $newContent[effect.showEffect](effect.showSpeed); @@ -1123,10 +1165,13 @@ // Attach all JavaScript behaviors to the new content, if it was // successfully added to the page, this if statement allows // `#ajax['wrapper']` to be optional. - if ($newContent.parents('html').length > 0) { - // Apply any settings from the returned JSON if available. - settings = response.settings || ajax.settings || drupalSettings; - Drupal.attachBehaviors($newContent.get(0), settings); + if ($newContent.parents('html').length) { + // Attach behaviors to all element nodes. + $newContent.each((index, element) => { + if (element.nodeType === Node.ELEMENT_NODE) { + Drupal.attachBehaviors(element, settings); + } + }); } }, diff --git a/core/misc/ajax.js b/core/misc/ajax.js index abe0ec2928df7cb497910d6be9b3d1cbf38e345f..ddec86d6636ada6d319bdfd7ee63eb8b65bb5556 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -490,20 +490,28 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage); }; + Drupal.theme.ajaxWrapperNewContent = function ($newContent, ajax, response) { + return (response.effect || ajax.effect) !== 'none' && $newContent.filter(function (i) { + return !($newContent[i].nodeName === '#comment' || $newContent[i].nodeName === '#text' && /^(\s|\n|\r)*$/.test($newContent[i].textContent)); + }).length > 1 ? Drupal.theme('ajaxWrapperMultipleRootElements', $newContent) : $newContent; + }; + + Drupal.theme.ajaxWrapperMultipleRootElements = function ($elements) { + return $('
').append($elements); + }; + Drupal.AjaxCommands = function () {}; Drupal.AjaxCommands.prototype = { - insert: function insert(ajax, response, status) { + insert: function insert(ajax, response) { var $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper); var method = response.method || ajax.method; var effect = ajax.getEffect(response); - var settings = void 0; - var $newContentWrapped = $('
').html(response.data); - var $newContent = $newContentWrapped.contents(); + var settings = response.settings || ajax.settings || drupalSettings; - if ($newContent.length !== 1 || $newContent.get(0).nodeType !== 1) { - $newContent = $newContentWrapped; - } + var $newContent = $($.parseHTML(response.data, document, true)); + + $newContent = Drupal.theme('ajaxWrapperNewContent', $newContent, ajax, response); switch (method) { case 'html': @@ -511,8 +519,10 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr case 'replaceAll': case 'empty': case 'remove': - settings = response.settings || ajax.settings || drupalSettings; Drupal.detachBehaviors($wrapper.get(0), settings); + break; + default: + break; } $wrapper[method]($newContent); @@ -521,17 +531,21 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr $newContent.hide(); } - if ($newContent.find('.ajax-new-content').length > 0) { - $newContent.find('.ajax-new-content').hide(); + var $ajaxNewContent = $newContent.find('.ajax-new-content'); + if ($ajaxNewContent.length) { + $ajaxNewContent.hide(); $newContent.show(); - $newContent.find('.ajax-new-content')[effect.showEffect](effect.showSpeed); + $ajaxNewContent[effect.showEffect](effect.showSpeed); } else if (effect.showEffect !== 'show') { $newContent[effect.showEffect](effect.showSpeed); } - if ($newContent.parents('html').length > 0) { - settings = response.settings || ajax.settings || drupalSettings; - Drupal.attachBehaviors($newContent.get(0), settings); + if ($newContent.parents('html').length) { + $newContent.each(function (index, element) { + if (element.nodeType === Node.ELEMENT_NODE) { + Drupal.attachBehaviors(element, settings); + } + }); } }, remove: function remove(ajax, response, status) { diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Callbacks.php b/core/modules/system/tests/modules/ajax_forms_test/src/Callbacks.php index 9b35ef7694fc921b13249c28497b462ea0558ce9..71350305ffee40430a1dc791e911e20bbe1be09b 100644 --- a/core/modules/system/tests/modules/ajax_forms_test/src/Callbacks.php +++ b/core/modules/system/tests/modules/ajax_forms_test/src/Callbacks.php @@ -27,7 +27,8 @@ public function selectCallback($form, FormStateInterface $form_state) { */ public function dateCallback($form, FormStateInterface $form_state) { $response = new AjaxResponse(); - $response->addCommand(new HtmlCommand('#ajax_date_value', $form_state->getValue('date'))); + $date = $form_state->getValue('date'); + $response->addCommand(new HtmlCommand('#ajax_date_value', sprintf('
%s
', $date))); $response->addCommand(new DataCommand('#ajax_date_value', 'form_state_value_date', $form_state->getValue('date'))); return $response; } @@ -39,7 +40,7 @@ public function datetimeCallback($form, FormStateInterface $form_state) { $datetime = $form_state->getValue('datetime')['date'] . ' ' . $form_state->getValue('datetime')['time']; $response = new AjaxResponse(); - $response->addCommand(new HtmlCommand('#ajax_datetime_value', $datetime)); + $response->addCommand(new HtmlCommand('#ajax_datetime_value', sprintf('
%s
', $datetime))); $response->addCommand(new DataCommand('#ajax_datetime_value', 'form_state_value_datetime', $datetime)); return $response; } 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 f1c73064bd27d66dd0e6949ee3689f9b79607c2b..772a05f734ff548ca97246d5048a37516f3bb110 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 @@ -1,3 +1,8 @@ +ajax_insert: + js: + js/insert-ajax.js: {} + dependencies: + - core/drupal.ajax order: drupalSettings: ajax: test 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 e8d06c0a9f27161310916a4bbd85e5a5009e017e..875b7caa9611354adea5796fbd96cba06eac4b54 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 @@ -6,6 +6,14 @@ ajax_test.dialog_contents: requirements: _access: 'TRUE' +ajax_test.ajax_render_types: + path: '/ajax-test/dialog-contents-types/{type}' + defaults: + _title: 'AJAX Dialog contents routing' + _controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderTypes' + requirements: + _access: 'TRUE' + ajax_test.dialog_form: path: '/ajax-test/dialog-form' defaults: @@ -21,6 +29,20 @@ ajax_test.dialog: requirements: _access: 'TRUE' +ajax_test.insert_links_block_wrapper: + path: '/ajax-test/insert-block-wrapper' + defaults: + _controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinksBlockWrapper' + requirements: + _access: 'TRUE' + +ajax_test.insert_links_inline_wrapper: + path: '/ajax-test/insert-inline-wrapper' + defaults: + _controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinksInlineWrapper' + requirements: + _access: 'TRUE' + ajax_test.dialog_close: path: '/ajax-test/dialog-close' defaults: diff --git a/core/modules/system/tests/modules/ajax_test/js/insert-ajax.es6.js b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.es6.js new file mode 100644 index 0000000000000000000000000000000000000000..4e359beac143a2507c0f60a1704a60e78b174c92 --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.es6.js @@ -0,0 +1,41 @@ +/** + * @file + * Drupal behavior to attach click event handlers to ajax-insert and + * ajax-insert-inline links for testing ajax requests. + */ + +(function ($, window, Drupal) { + Drupal.behaviors.insertTest = { + attach(context) { + $('.ajax-insert').once('ajax-insert').on('click', (event) => { + event.preventDefault(); + const ajaxSettings = { + url: event.currentTarget.getAttribute('href'), + wrapper: 'ajax-target', + base: false, + element: false, + method: event.currentTarget.getAttribute('data-method'), + effect: event.currentTarget.getAttribute('data-effect'), + }; + const myAjaxObject = Drupal.ajax(ajaxSettings); + myAjaxObject.execute(); + }); + + $('.ajax-insert-inline').once('ajax-insert').on('click', (event) => { + event.preventDefault(); + const ajaxSettings = { + url: event.currentTarget.getAttribute('href'), + wrapper: 'ajax-target-inline', + base: false, + element: false, + method: event.currentTarget.getAttribute('data-method'), + effect: event.currentTarget.getAttribute('data-effect'), + }; + const myAjaxObject = Drupal.ajax(ajaxSettings); + myAjaxObject.execute(); + }); + + $(context).addClass('processed'); + }, + }; +}(jQuery, window, Drupal)); diff --git a/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js new file mode 100644 index 0000000000000000000000000000000000000000..e28fcd298758efba1745eb251250bca6ddb18fae --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js @@ -0,0 +1,42 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, window, Drupal) { + Drupal.behaviors.insertTest = { + attach: function attach(context) { + $('.ajax-insert').once('ajax-insert').on('click', function (event) { + event.preventDefault(); + var ajaxSettings = { + url: event.currentTarget.getAttribute('href'), + wrapper: 'ajax-target', + base: false, + element: false, + method: event.currentTarget.getAttribute('data-method'), + effect: event.currentTarget.getAttribute('data-effect') + }; + var myAjaxObject = Drupal.ajax(ajaxSettings); + myAjaxObject.execute(); + }); + + $('.ajax-insert-inline').once('ajax-insert').on('click', function (event) { + event.preventDefault(); + var ajaxSettings = { + url: event.currentTarget.getAttribute('href'), + wrapper: 'ajax-target-inline', + base: false, + element: false, + method: event.currentTarget.getAttribute('data-method'), + effect: event.currentTarget.getAttribute('data-effect') + }; + var myAjaxObject = Drupal.ajax(ajaxSettings); + myAjaxObject.execute(); + }); + + $(context).addClass('processed'); + } + }; +})(jQuery, window, Drupal); \ No newline at end of file diff --git a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php index a34d288ff80d7117fc51b87433ce706516fd5da3..1897719f6153d728d445c1d4cf98d9b69ffb7765 100644 --- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php @@ -42,6 +42,101 @@ public static function dialogContents() { return $content; } + /** + * Example content for testing the wrapper of the response. + * + * @param string $type + * Type of response. + * + * @return array + * Renderable array of AJAX response contents. + */ + public function renderTypes($type) { + return [ + '#title' => 'AJAX Dialog & contents', + 'content' => [ + '#type' => 'inline_template', + '#template' => $this->getRenderTypes()[$type]['render'], + ], + ]; + } + + /** + * Returns a render array of links that directly Drupal.ajax(). + * + * @return array + * Renderable array of AJAX response contents. + */ + public function insertLinksBlockWrapper() { + $methods = [ + 'html', + 'replaceWith', + ]; + + $build['links'] = [ + 'ajax_target' => [ + '#markup' => '
Target
', + ], + 'links' => [ + '#theme' => 'links', + '#attached' => ['library' => ['ajax_test/ajax_insert']], + ], + ]; + foreach ($methods as $method) { + foreach ($this->getRenderTypes() as $type => $item) { + $class = 'ajax-insert'; + $build['links']['links']['#links']["$method-$type"] = [ + 'title' => "Link $method $type", + 'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => $type]), + 'attributes' => [ + 'class' => [$class], + 'data-method' => $method, + 'data-effect' => $item['effect'], + ], + ]; + } + } + return $build; + } + + /** + * Returns a render array of links that directly Drupal.ajax(). + * + * @return array + * Renderable array of AJAX response contents. + */ + public function insertLinksInlineWrapper() { + $methods = [ + 'html', + 'replaceWith', + ]; + + $build['links'] = [ + 'ajax_target' => [ + '#markup' => '
Target inline
', + ], + 'links' => [ + '#theme' => 'links', + '#attached' => ['library' => ['ajax_test/ajax_insert']], + ], + ]; + foreach ($methods as $method) { + foreach ($this->getRenderTypes() as $type => $item) { + $class = 'ajax-insert-inline'; + $build['links']['links']['#links']["$method-$type"] = [ + 'title' => "Link $method $type", + 'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => $type]), + 'attributes' => [ + 'class' => [$class], + 'data-method' => $method, + 'data-effect' => $item['effect'], + ], + ]; + } + } + return $build; + } + /** * Returns a render array that will be rendered by AjaxRenderer. * @@ -222,4 +317,41 @@ public function dialogClose() { return $response; } + /** + * Render types. + * + * @return array + * Render types. + */ + protected function getRenderTypes() { + $render_single_root = [ + 'pre-wrapped-div' => '
pre-wrapped
', + 'pre-wrapped-span' => 'pre-wrapped', + 'pre-wrapped-whitespace' => '
pre-wrapped-whitespace
' . "\r\n", + 'not-wrapped' => 'not-wrapped', + 'comment-string-not-wrapped' => 'comment-string-not-wrapped', + 'comment-not-wrapped' => '
comment-not-wrapped
', + 'svg' => '', + 'empty' => '', + ]; + $render_multiple_root = [ + 'mixed' => ' foo foo bar

some string

additional not wrapped strings,

final string

', + 'top-level-only' => '
element #1
element #2
', + 'top-level-only-pre-whitespace' => '
element #1
element #2
', + 'top-level-only-middle-whitespace-span' => 'element #1 element #2', + 'top-level-only-middle-whitespace-div' => '
element #1
element #2
', + ]; + + $render_info = []; + foreach ($render_single_root as $key => $render) { + $render_info[$key] = ['render' => $render, 'effect' => 'fade']; + } + foreach ($render_multiple_root as $key => $render) { + $render_info[$key] = ['render' => $render, 'effect' => 'none']; + $render_info["$key--effect"] = ['render' => $render, 'effect' => 'fade']; + } + + return $render_info; + } + } diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php index 06b64af67b2281400740d93247a1549e386e6e24..5a167f729cb92263eed2968543ec0f9a70afc4ef 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php @@ -54,8 +54,8 @@ public function testSimpleAJAXFormValue() { // Wait for the DOM to update. The HtmlCommand will update // #ajax_selected_color to reflect the color change. - $green_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('green')"); - $this->assertNotNull($green_div, 'DOM update: The selected color DIV is green.'); + $green_span = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('green')"); + $this->assertNotNull($green_span, 'DOM update: The selected color SPAN is green.'); // Confirm the operation of the UpdateBuildIdCommand. $build_id_first_ajax = $this->getFormBuildId(); @@ -66,8 +66,8 @@ public function testSimpleAJAXFormValue() { $session->getPage()->selectFieldOption('select', 'red'); // Wait for the DOM to update. - $red_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('red')"); - $this->assertNotNull($red_div, 'DOM update: The selected color DIV is red.'); + $red_span = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('red')"); + $this->assertNotNull($red_span, 'DOM update: The selected color SPAN is red.'); // Confirm the operation of the UpdateBuildIdCommand. $build_id_second_ajax = $this->getFormBuildId(); @@ -84,8 +84,8 @@ public function testSimpleAJAXFormValue() { $session->getPage()->selectFieldOption('select', 'green'); // Wait for the DOM to update. - $green_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('green')"); - $this->assertNotNull($green_div2, 'DOM update: After reload - the selected color DIV is green.'); + $green_span2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('green')"); + $this->assertNotNull($green_span2, 'DOM update: After reload - the selected color SPAN is green.'); $build_id_from_cache_first_ajax = $this->getFormBuildId(); $this->assertNotEquals($build_id_from_cache_initial, $build_id_from_cache_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission'); @@ -96,8 +96,8 @@ public function testSimpleAJAXFormValue() { $session->getPage()->selectFieldOption('select', 'red'); // Wait for the DOM to update. - $red_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('red')"); - $this->assertNotNull($red_div2, 'DOM update: After reload - the selected color DIV is red.'); + $red_span2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color:contains('red')"); + $this->assertNotNull($red_span2, 'DOM update: After reload - the selected color SPAN is red.'); $build_id_from_cache_second_ajax = $this->getFormBuildId(); $this->assertNotEquals($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id changes on subsequent AJAX submissions'); diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php index fa2a3e45c02fd30b2a9c0496d0a090abbe3e9b61..5d11ce49b4bc758fffdb8864cc3af79f94d4b94d 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php @@ -82,4 +82,119 @@ public function testDrupalSettingsCachingRegression() { $this->assertNotContains($fake_library, $libraries); } + /** + * Tests that various AJAX responses with DOM elements are correctly inserted. + * + * After inserting DOM elements, Drupal JavaScript behaviors should be + * reattached and all top-level elements of type Node.ELEMENT_NODE need to be + * part of the context. + */ + public function testInsertAjaxResponse() { + $render_single_root = [ + 'pre-wrapped-div' => '
pre-wrapped
', + 'pre-wrapped-span' => 'pre-wrapped', + 'pre-wrapped-whitespace' => '
pre-wrapped-whitespace
' . "\n", + 'not-wrapped' => 'not-wrapped', + 'comment-string-not-wrapped' => 'comment-string-not-wrapped', + 'comment-not-wrapped' => '
comment-not-wrapped
', + 'svg' => '', + 'empty' => '', + ]; + $render_multiple_root_unwrapper = [ + 'mixed' => ' foo foo bar

some string

additional not wrapped strings,

final string

', + 'top-level-only' => '
element #1
element #2
', + 'top-level-only-pre-whitespace' => '
element #1
element #2
', + 'top-level-only-middle-whitespace-span' => 'element #1 element #2', + 'top-level-only-middle-whitespace-div' => '
element #1
element #2
', + ]; + + // This is temporary behavior for BC reason. + $render_multiple_root_wrapper = []; + foreach ($render_multiple_root_unwrapper as $key => $render) { + $render_multiple_root_wrapper["$key--effect"] = '
' . $render . '
'; + } + + $expected_renders = array_merge( + $render_single_root, + $render_multiple_root_wrapper, + $render_multiple_root_unwrapper + ); + + // Checking default process of wrapping Ajax content. + foreach ($expected_renders as $render_type => $expected) { + $this->assertInsert($render_type, $expected); + } + + // Checking custom ajaxWrapperMultipleRootElements wrapping. + $custom_wrapper_multiple_root = <<
').append(elements); + }; + }(jQuery, Drupal)); +JS; + $expected = '
element #1 element #2
'; + $this->assertInsert('top-level-only-middle-whitespace-span--effect', $expected, $custom_wrapper_multiple_root); + + // Checking custom ajaxWrapperNewContent wrapping. + $custom_wrapper_new_content = <<').append(elements); + }; + }(jQuery, Drupal)); +JS; + $expected = '
'; + $this->assertInsert('empty', $expected, $custom_wrapper_new_content); + } + + /** + * Assert insert. + * + * @param string $render_type + * Render type. + * @param string $expected + * Expected result. + * @param string $script + * Script for additional theming. + */ + public function assertInsert($render_type, $expected, $script = '') { + // Check insert to block element. + $this->drupalGet('ajax-test/insert-block-wrapper'); + $this->getSession()->executeScript($script); + $this->clickLink("Link html $render_type"); + $this->assertWaitPageContains('
' . $expected . '
'); + + $this->drupalGet('ajax-test/insert-block-wrapper'); + $this->getSession()->executeScript($script); + $this->clickLink("Link replaceWith $render_type"); + $this->assertWaitPageContains('
' . $expected . '
'); + + // Check insert to inline element. + $this->drupalGet('ajax-test/insert-inline-wrapper'); + $this->getSession()->executeScript($script); + $this->clickLink("Link html $render_type"); + $this->assertWaitPageContains('
' . $expected . '
'); + + $this->drupalGet('ajax-test/insert-inline-wrapper'); + $this->getSession()->executeScript($script); + $this->clickLink("Link replaceWith $render_type"); + $this->assertWaitPageContains('
' . $expected . '
'); + } + + /** + * Asserts that page contains an expected value after waiting. + * + * @param string $expected + * A needle text. + */ + protected function assertWaitPageContains($expected) { + $page = $this->getSession()->getPage(); + $this->assertTrue($page->waitFor(10, function () use ($page, $expected) { + // Clear content from empty styles and "processed" classes after effect. + $content = str_replace([' class="processed"', ' processed', ' style=""'], '', $page->getContent()); + return stripos($content, $expected) !== FALSE; + }), "Page contains expected value: $expected"); + } + }