summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLauri Eskola2018-07-04 18:59:57 (GMT)
committerLauri Eskola2018-07-04 18:59:57 (GMT)
commit5f0f49d74136eca18cb2470370646e61da4893a2 (patch)
tree28d2c4628c29573883bf8712d82147fdb52a85a0
parent27740c26de6dd978ed1c33e6b9f6b7ace72b6528 (diff)
Issue #736066 by droplet, drpal, vaplas, dmsmidt, effulgentsia, nod_, tic2000, tim.plunkett, lauriii, casey, morsok, idebr, alexpott, yched, tedbow, GuyPaddock, drintios, edurenye, dtamajon, adinac, VinayLondhe, ajalan065, hrmoller, ericduran, piyuesh23, andreyks, roborew, Wim Leers, lokapujya, samuel.mortenson, Dries, DuaelFr, OnkelTem, manu manu, khiminrm, Greg Boggs, SKAUGHT, itsekhmistro, fubhy, SpadXIII, s.messaris, zviryatko: ajax.js insert command sometimes wraps content in a div, potentially producing invalid HTML and other bugs
-rw-r--r--core/misc/ajax.es6.js115
-rw-r--r--core/misc/ajax.js42
-rw-r--r--core/modules/system/tests/modules/ajax_forms_test/src/Callbacks.php5
-rw-r--r--core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml5
-rw-r--r--core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml22
-rw-r--r--core/modules/system/tests/modules/ajax_test/js/insert-ajax.es6.js41
-rw-r--r--core/modules/system/tests/modules/ajax_test/js/insert-ajax.js42
-rw-r--r--core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php132
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php16
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php115
10 files changed, 476 insertions, 59 deletions
diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js
index 739afa1..1dae116 100644
--- a/core/misc/ajax.es6.js
+++ b/core/misc/ajax.es6.js
@@ -1011,6 +1011,60 @@
};
/**
+ * 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 => (
+ $('<div></div>').append($elements)
+ );
+
+ /**
* @typedef {object} Drupal.AjaxCommands~commandDefinition
*
* @prop {string} command
@@ -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 $('<div></div>').replaceWith(response.data).
- const $newContentWrapped = $('<div></div>').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 <div>
- // elements are not allowed (e.g., within <table>, <tr>, and <span>
- // parents), we check if the new content satisfies the requirement
- // of a single top-level element, and only use the container <div> 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 abe0ec2..ddec86d 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 $('<div></div>').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 = $('<div></div>').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 9b35ef7..7135030 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 @@ class Callbacks {
*/
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('<div>%s</div>', $date)));
$response->addCommand(new DataCommand('#ajax_date_value', 'form_state_value_date', $form_state->getValue('date')));
return $response;
}
@@ -39,7 +40,7 @@ class Callbacks {
$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('<div>%s</div>', $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 f1c7306..772a05f 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 e8d06c0..875b7ca 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 0000000..4e359be
--- /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 0000000..e28fcd2
--- /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 a34d288..1897719 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
@@ -43,6 +43,101 @@ class AjaxTestController {
}
/**
+ * 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' => '<em>AJAX Dialog & contents</em>',
+ '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' => '<div class="ajax-target-wrapper"><div id="ajax-target">Target</div></div>',
+ ],
+ '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' => '<div class="ajax-target-wrapper"><span id="ajax-target-inline">Target inline</span></div>',
+ ],
+ '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.
*
* Verifies that the response incorporates JavaScript settings generated
@@ -222,4 +317,41 @@ class AjaxTestController {
return $response;
}
+ /**
+ * Render types.
+ *
+ * @return array
+ * Render types.
+ */
+ protected function getRenderTypes() {
+ $render_single_root = [
+ 'pre-wrapped-div' => '<div class="pre-wrapped">pre-wrapped<script> var test;</script></div>',
+ 'pre-wrapped-span' => '<span class="pre-wrapped">pre-wrapped<script> var test;</script></span>',
+ 'pre-wrapped-whitespace' => ' <div class="pre-wrapped-whitespace">pre-wrapped-whitespace</div>' . "\r\n",
+ 'not-wrapped' => 'not-wrapped',
+ 'comment-string-not-wrapped' => '<!-- COMMENT -->comment-string-not-wrapped',
+ 'comment-not-wrapped' => '<!-- COMMENT --><div class="comment-not-wrapped">comment-not-wrapped</div>',
+ 'svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" height="10" width="10" fill="green"/></svg>',
+ 'empty' => '',
+ ];
+ $render_multiple_root = [
+ 'mixed' => ' foo <!-- COMMENT --> foo bar<div class="a class"><p>some string</p></div> additional not wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p>',
+ 'top-level-only' => '<div>element #1</div><div>element #2</div>',
+ 'top-level-only-pre-whitespace' => ' <div>element #1</div><div>element #2</div> ',
+ 'top-level-only-middle-whitespace-span' => '<span>element #1</span> <span>element #2</span>',
+ 'top-level-only-middle-whitespace-div' => '<div>element #1</div> <div>element #2</div>',
+ ];
+
+ $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 06b64af..5a167f7 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php
@@ -54,8 +54,8 @@ class AjaxFormPageCacheTest extends WebDriverTestBase {
// 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 @@ class AjaxFormPageCacheTest extends WebDriverTestBase {
$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 @@ class AjaxFormPageCacheTest extends WebDriverTestBase {
$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 @@ class AjaxFormPageCacheTest extends WebDriverTestBase {
$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 fa2a3e4..5d11ce4 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -82,4 +82,119 @@ class AjaxTest extends WebDriverTestBase {
$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' => '<div class="pre-wrapped">pre-wrapped<script> var test;</script></div>',
+ 'pre-wrapped-span' => '<span class="pre-wrapped">pre-wrapped<script> var test;</script></span>',
+ 'pre-wrapped-whitespace' => ' <div class="pre-wrapped-whitespace">pre-wrapped-whitespace</div>' . "\n",
+ 'not-wrapped' => 'not-wrapped',
+ 'comment-string-not-wrapped' => '<!-- COMMENT -->comment-string-not-wrapped',
+ 'comment-not-wrapped' => '<!-- COMMENT --><div class="comment-not-wrapped">comment-not-wrapped</div>',
+ 'svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" height="10" width="10" fill="green"/></svg>',
+ 'empty' => '',
+ ];
+ $render_multiple_root_unwrapper = [
+ 'mixed' => ' foo <!-- COMMENT --> foo bar<div class="a class"><p>some string</p></div> additional not wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p>',
+ 'top-level-only' => '<div>element #1</div><div>element #2</div>',
+ 'top-level-only-pre-whitespace' => ' <div>element #1</div><div>element #2</div> ',
+ 'top-level-only-middle-whitespace-span' => '<span>element #1</span> <span>element #2</span>',
+ 'top-level-only-middle-whitespace-div' => '<div>element #1</div> <div>element #2</div>',
+ ];
+
+ // 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"] = '<div>' . $render . '</div>';
+ }
+
+ $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 = <<<JS
+ (function($, Drupal){
+ Drupal.theme.ajaxWrapperMultipleRootElements = function (elements) {
+ return $('<div class="my-favorite-div"></div>').append(elements);
+ };
+ }(jQuery, Drupal));
+JS;
+ $expected = '<div class="my-favorite-div"><span>element #1</span> <span>element #2</span></div>';
+ $this->assertInsert('top-level-only-middle-whitespace-span--effect', $expected, $custom_wrapper_multiple_root);
+
+ // Checking custom ajaxWrapperNewContent wrapping.
+ $custom_wrapper_new_content = <<<JS
+ (function($, Drupal){
+ Drupal.theme.ajaxWrapperNewContent = function (elements) {
+ return $('<div class="div-wrapper-forever"></div>').append(elements);
+ };
+ }(jQuery, Drupal));
+JS;
+ $expected = '<div class="div-wrapper-forever"></div>';
+ $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('<div class="ajax-target-wrapper"><div id="ajax-target">' . $expected . '</div></div>');
+
+ $this->drupalGet('ajax-test/insert-block-wrapper');
+ $this->getSession()->executeScript($script);
+ $this->clickLink("Link replaceWith $render_type");
+ $this->assertWaitPageContains('<div class="ajax-target-wrapper">' . $expected . '</div>');
+
+ // Check insert to inline element.
+ $this->drupalGet('ajax-test/insert-inline-wrapper');
+ $this->getSession()->executeScript($script);
+ $this->clickLink("Link html $render_type");
+ $this->assertWaitPageContains('<div class="ajax-target-wrapper"><span id="ajax-target-inline">' . $expected . '</span></div>');
+
+ $this->drupalGet('ajax-test/insert-inline-wrapper');
+ $this->getSession()->executeScript($script);
+ $this->clickLink("Link replaceWith $render_type");
+ $this->assertWaitPageContains('<div class="ajax-target-wrapper">' . $expected . '</div>');
+ }
+
+ /**
+ * 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");
+ }
+
}