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 => (
+    $('<div></div>').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 $('<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 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 $('<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 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('<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 @@ 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('<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 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' => '<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.
    *
@@ -222,4 +317,41 @@ public function dialogClose() {
     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 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' => '<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");
+  }
+
 }