diff --git a/core/lib/Drupal/Component/Utility/Html.php b/core/lib/Drupal/Component/Utility/Html.php
index 60b83df..b62ba69 100644
--- a/core/lib/Drupal/Component/Utility/Html.php
+++ b/core/lib/Drupal/Component/Utility/Html.php
@@ -35,11 +35,11 @@ class Html {
   protected static $seenIds;
 
   /**
-   * Contains the current AJAX HTML IDs.
+   * Stores whether the current request was sent via AJAX.
    *
-   * @var string
+   * @var bool
    */
-  protected static $ajaxHTMLIDs;
+  protected static $isAjax = FALSE;
 
   /**
    * Prepares a string for use as a valid class name.
@@ -102,13 +102,13 @@ public static function cleanCssIdentifier($identifier, array $filter = array(
   }
 
   /**
-   * Sets the AJAX HTML IDs.
+   * Sets if this request is an Ajax request.
    *
-   * @param string $ajax_html_ids
-   *   The AJAX HTML IDs, probably coming from the current request.
+   * @param bool $is_ajax
+   *   TRUE if this request is an Ajax request, FALSE otherwise.
    */
-  public static function setAjaxHtmlIds($ajax_html_ids = '') {
-    static::$ajaxHTMLIDs = $ajax_html_ids;
+  public static function setIsAjax($is_ajax) {
+    static::$isAjax = $is_ajax;
   }
 
   /**
@@ -142,43 +142,14 @@ public static function setAjaxHtmlIds($ajax_html_ids = '') {
   public static function getUniqueId($id) {
     // If this is an Ajax request, then content returned by this page request
     // will be merged with content already on the base page. The HTML IDs must
-    // be unique for the fully merged content. Therefore, initialize $seen_ids
-    // to take into account IDs that are already in use on the base page.
+    // be unique for the fully merged content. Therefore use unique IDs.
+    if (static::$isAjax) {
+      return static::getId($id) . '--' . Crypt::randomBytesBase64(8);
+    }
+
+    // seenIdsInit is now only useful during testing.
     if (!isset(static::$seenIdsInit)) {
-      // Ideally, Drupal would provide an API to persist state information about
-      // prior page requests in the database, and we'd be able to add this
-      // function's $seen_ids static variable to that state information in order
-      // to have it properly initialized for this page request. However, no such
-      // page state API exists, so instead, ajax.js adds all of the in-use HTML
-      // IDs to the POST data of Ajax submissions. Direct use of $_POST is
-      // normally not recommended as it could open up security risks, but
-      // because the raw POST data is cast to a number before being returned by
-      // this function, this usage is safe.
-      if (empty(static::$ajaxHTMLIDs)) {
-        static::$seenIdsInit = array();
-      }
-      else {
-        // This function ensures uniqueness by appending a counter to the base
-        // id requested by the calling function after the first occurrence of
-        // that requested id. $_POST['ajax_html_ids'] contains the ids as they
-        // were returned by this function, potentially with the appended
-        // counter, so we parse that to reconstruct the $seen_ids array.
-        $ajax_html_ids = explode(' ', static::$ajaxHTMLIDs);
-        foreach ($ajax_html_ids as $seen_id) {
-          // We rely on '--' being used solely for separating a base id from the
-          // counter, which this function ensures when returning an id.
-          $parts = explode('--', $seen_id, 2);
-          if (!empty($parts[1]) && is_numeric($parts[1])) {
-            list($seen_id, $i) = $parts;
-          }
-          else {
-            $i = 1;
-          }
-          if (!isset(static::$seenIdsInit[$seen_id]) || ($i > static::$seenIdsInit[$seen_id])) {
-            static::$seenIdsInit[$seen_id] = $i;
-          }
-        }
-      }
+      static::$seenIdsInit = array();
     }
     if (!isset(static::$seenIds)) {
       static::$seenIds = static::$seenIdsInit;
diff --git a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php
index 8996b5d..87f89d1 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AjaxSubscriber.php
@@ -20,13 +20,21 @@
 class AjaxSubscriber implements EventSubscriberInterface {
 
   /**
-   * Sets the AJAX HTML IDs from the current request.
+   * Request parameter to indicate that a request is a Drupal Ajax request.
+   */
+  const AJAX_REQUEST_PARAMETER = '_drupal_ajax';
+
+  /**
+   * Sets the AJAX parameter from the current request.
    *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
    *   The response event, which contains the current request.
    */
   public function onRequest(GetResponseEvent $event) {
-    Html::setAjaxHtmlIds($event->getRequest()->request->get('ajax_html_ids', ''));
+    // Ensure that ajax=1 is set.
+    if ($event->getRequest()->request->get(static::AJAX_REQUEST_PARAMETER)) {
+      Html::setIsAjax(TRUE);
+    }
   }
 
   /**
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index db8dd26..39c7c37 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -377,6 +377,11 @@
   Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format';
 
   /**
+   * Request parameter to indicate that a request is a Drupal Ajax request.
+   */
+   Drupal.ajax.AJAX_REQUEST_PARAMETER = '_drupal_ajax';
+
+  /**
    * Execute the ajax request.
    *
    * Allows developers to execute an Ajax request manually without specifying
@@ -491,16 +496,8 @@
       Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
     }
 
-    // Prevent duplicate HTML ids in the returned markup.
-    // @see \Drupal\Component\Utility\Html::getUniqueId()
-    var ids = document.querySelectorAll('[id]');
-    var ajaxHtmlIds = [];
-    var il = ids.length;
-    for (var i = 0; i < il; i++) {
-      ajaxHtmlIds.push(ids[i].id);
-    }
-    // Join IDs to minimize request size.
-    options.data.ajax_html_ids = ajaxHtmlIds.join(' ');
+    // Inform Drupal that this is an AJAX request.
+    options.data[Drupal.ajax.AJAX_REQUEST_PARAMETER] = 1;
 
     // Allow Drupal to return new JavaScript and CSS files to load without
     // returning the ones already loaded.
diff --git a/core/modules/block/js/block.admin.js b/core/modules/block/js/block.admin.js
index b399dd0..4c4fccb 100644
--- a/core/modules/block/js/block.admin.js
+++ b/core/modules/block/js/block.admin.js
@@ -92,7 +92,7 @@
   Drupal.behaviors.blockHighlightPlacement = {
     attach: function (context, settings) {
       if (settings.blockPlacement) {
-        $('#blocks').once('block-highlight').each(function () {
+        $(context).find('[data-drupal-selector="blocks"]').once('block-highlight').each(function () {
           var $container = $(this);
           // Just scrolling the document.body will not work in Firefox. The html
           // element is needed as well.
diff --git a/core/modules/block/js/block.js b/core/modules/block/js/block.js
index e5c0907..e5cbfd8 100644
--- a/core/modules/block/js/block.js
+++ b/core/modules/block/js/block.js
@@ -34,9 +34,9 @@
         return vals.join(', ');
       }
 
-      $('#edit-visibility-node-type, #edit-visibility-language, #edit-visibility-user-role').drupalSetSummary(checkboxesSummary);
+      $('[data-drupal-selector="visibility-node-type"], [data-drupal-selector="visibility-language"], [data-drupal-selector="visibility-user-role"]').drupalSetSummary(checkboxesSummary);
 
-      $('#edit-visibility-request-path').drupalSetSummary(function (context) {
+      $('[data-drupal-selector="visibility-request-path"]').drupalSetSummary(function (context) {
         var $pages = $(context).find('textarea[name="visibility[request_path][pages]"]');
         if (!$pages.val()) {
           return Drupal.t('Not restricted');
@@ -63,7 +63,7 @@
         return;
       }
 
-      var table = $('#blocks');
+      var table = $('[data-drupal-selector="blocks"]');
       // Get the blocks tableDrag object.
       var tableDrag = Drupal.tableDrag.blocks;
       // Add a handler for when a row is swapped, update empty regions.
diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index 1aefa68..8fd15cc 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -233,6 +233,7 @@ protected function buildVisibilityInterface(array $form, FormStateInterface $for
 
     if (isset($form['node_type'])) {
       $form['node_type']['#title'] = $this->t('Content types');
+      $form['node_type']['#attributes']['data-drupal-selector'] = 'visibility-node-type';
       $form['node_type']['bundles']['#title'] = $this->t('Content types');
       $form['node_type']['negate']['#type'] = 'value';
       $form['node_type']['negate']['#title_display'] = 'invisible';
@@ -240,12 +241,14 @@ protected function buildVisibilityInterface(array $form, FormStateInterface $for
     }
     if (isset($form['user_role'])) {
       $form['user_role']['#title'] = $this->t('Roles');
+      $form['user_role']['#attributes']['data-drupal-selector'] = 'visibility-user-role';
       unset($form['user_role']['roles']['#description']);
       $form['user_role']['negate']['#type'] = 'value';
       $form['user_role']['negate']['#value'] = $form['user_role']['negate']['#default_value'];
     }
     if (isset($form['request_path'])) {
       $form['request_path']['#title'] = $this->t('Pages');
+      $form['request_path']['#attributes']['data-drupal-selector'] = 'visibility-request-path';
       $form['request_path']['negate']['#type'] = 'radios';
       $form['request_path']['negate']['#default_value'] = (int) $form['request_path']['negate']['#default_value'];
       $form['request_path']['negate']['#title_display'] = 'invisible';
@@ -255,6 +258,7 @@ protected function buildVisibilityInterface(array $form, FormStateInterface $for
       ];
     }
     if (isset($form['language'])) {
+      $form['language']['#attributes']['data-drupal-selector'] = 'visibility-language';
       $form['language']['negate']['#type'] = 'value';
       $form['language']['negate']['#value'] = $form['language']['negate']['#default_value'];
     }
diff --git a/core/modules/block/src/BlockListBuilder.php b/core/modules/block/src/BlockListBuilder.php
index 196713a..429f8ba 100644
--- a/core/modules/block/src/BlockListBuilder.php
+++ b/core/modules/block/src/BlockListBuilder.php
@@ -177,6 +177,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ),
       '#attributes' => array(
         'id' => 'blocks',
+        'data-drupal-selector' => 'blocks',
       ),
     );
 
diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js
index ff75541..a3ba78f 100644
--- a/core/modules/ckeditor/js/ckeditor.admin.js
+++ b/core/modules/ckeditor/js/ckeditor.admin.js
@@ -343,7 +343,7 @@
   Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
     attach: function (context) {
       var $context = $(context);
-      var $ckeditorPluginSettings = $context.find('#ckeditor-plugin-settings').once('ckeditor-plugin-settings');
+      var $ckeditorPluginSettings = $context.find('[data-drupal-selector="ckeditor-plugin-settings"]').once('ckeditor-plugin-settings');
       if ($ckeditorPluginSettings.length) {
         // Hide all button-dependent plugin settings initially.
         $ckeditorPluginSettings.find('[data-ckeditor-buttons]').each(function () {
diff --git a/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
index 44072df..fcd3203 100644
--- a/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
+++ b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
@@ -7,7 +7,7 @@
    */
   Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
     attach: function () {
-      $('#edit-editor-settings-plugins-drupalimage').drupalSetSummary(function (context) {
+      $('[data-ckeditor-plugin-id="drupalimage"]').drupalSetSummary(function (context) {
         var root = 'input[name="editor[settings][plugins][drupalimage][image_upload]';
         var $status = $(root + '[status]"]');
         var $maxFileSize = $(root + '[max_size]"]');
diff --git a/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
index 5fe814d..3736151 100644
--- a/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
+++ b/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
@@ -26,7 +26,7 @@
       var that = this;
       $context.find('[name="editor[settings][plugins][stylescombo][styles]"]')
         .on('blur.ckeditorStylesComboSettings', function () {
-          var styles = $.trim($('#edit-editor-settings-plugins-stylescombo-styles').val());
+          var styles = $.trim($(this).val());
           var stylesSet = that._generateStylesSetSetting(styles);
           if (!_.isEqual(previousStylesSet, stylesSet)) {
             previousStylesSet = stylesSet;
@@ -96,8 +96,8 @@
    */
   Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
     attach: function () {
-      $('#edit-editor-settings-plugins-stylescombo').drupalSetSummary(function (context) {
-        var styles = $.trim($('#edit-editor-settings-plugins-stylescombo-styles').val());
+      $('[data-ckeditor-plugin-id="stylescombo"]').drupalSetSummary(function (context) {
+        var styles = $.trim($('[name="editor[settings][plugins][stylescombo][styles]"]').val());
         if (styles.length === 0) {
           return Drupal.t('No styles configured');
         }
diff --git a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
index 949c8e5..974c61c 100644
--- a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
+++ b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
@@ -165,6 +165,7 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor
       '#title' => t('CKEditor plugin settings'),
       '#attributes' => array(
         'id' => 'ckeditor-plugin-settings',
+        'data-drupal-selector' => 'ckeditor-plugin-settings',
       ),
     );
     $this->ckeditorPluginManager->injectPluginSettingsForm($form, $form_state, $editor);
diff --git a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php b/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php
index 28f6334..647180c 100644
--- a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php
+++ b/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php
@@ -171,7 +171,7 @@ function testBooleanField() {
       t('Display setting checkbox is available')
     );
     $this->assertFieldByXPath(
-      '*//input[@id="edit-fields-' . $field_name . '-settings-edit-form-settings-display-label" and @value="1"]',
+      '*//input[starts-with(@id, "edit-fields-' . $field_name . '-settings-edit-form-settings-display-label") and @value="1"]',
       TRUE,
       t('Display label changes label of the checkbox')
     );
diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js
index 3c9e5ae..b55b0d9 100644
--- a/core/modules/field_ui/field_ui.js
+++ b/core/modules/field_ui/field_ui.js
@@ -12,7 +12,7 @@
    */
   Drupal.behaviors.fieldUIFieldStorageAddForm = {
     attach: function (context) {
-      var $form = $(context).find('#field-ui-field-storage-add-form').once('field_ui_add');
+      var $form = $(context).find('[data-drupal-selector="field-ui-field-storage-add-form"]').once('field_ui_add');
       if ($form.length) {
         // Add a few 'form-required' css classes here. We can not use the Form
         // API '#required' property because both label elements for "add new"
@@ -61,7 +61,7 @@
    */
   Drupal.behaviors.fieldUIDisplayOverview = {
     attach: function (context, settings) {
-      $(context).find('table#field-display-overview').once('field-display-overview').each(function () {
+      $(context).find('[data-drupal-selector="field-display-overview"]').once('field-display-overview').each(function () {
         Drupal.fieldUIOverview.attach(this, settings.fieldUIRowsData, Drupal.fieldUIDisplayOverview);
       });
     }
@@ -214,7 +214,7 @@
 
         // Fire the Ajax update.
         $('input[name=refresh_rows]').val(rowNames.join(' '));
-        $('input#edit-refresh').trigger('mousedown');
+        $('input[data-drupal-selector="field-ui-refresh"]').trigger('mousedown');
 
         // Disabled elements do not appear in POST ajax data, so we mark the
         // elements disabled only after firing the request.
diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
index 61cfd0c..e17d716 100644
--- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
+++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
@@ -167,6 +167,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#attributes' => array(
         'class' => array('field-ui-overview'),
         'id' => 'field-display-overview',
+        'data-drupal-selector' => 'field-display-overview',
       ),
       // Add Ajax wrapper.
       '#prefix' => '<div id="field-display-overview-wrapper">',
@@ -247,7 +248,10 @@ public function form(array $form, FormStateInterface $form_state) {
         // spinners will be added manually by the client-side script.
         'progress' => 'none',
       ),
-      '#attributes' => array('class' => array('visually-hidden'))
+      '#attributes' => array(
+        'class' => array('visually-hidden'),
+        'data-drupal-selector' => 'field-ui-refresh',
+      )
     );
 
     $form['actions'] = array('#type' => 'actions');
diff --git a/core/modules/field_ui/src/Form/FieldStorageAddForm.php b/core/modules/field_ui/src/Form/FieldStorageAddForm.php
index 5f087a6..63639f1 100644
--- a/core/modules/field_ui/src/Form/FieldStorageAddForm.php
+++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php
@@ -125,6 +125,8 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
       }
     }
 
+    $form['#attributes']['data-drupal-selector'] = 'field-ui-field-storage-add-form';
+
     $form['add'] = array(
       '#type' => 'container',
       '#attributes' => array('class' => array('form--inline', 'clearfix')),
diff --git a/core/modules/file/src/Element/ManagedFile.php b/core/modules/file/src/Element/ManagedFile.php
index 40d0c31..a6fe4f8 100644
--- a/core/modules/file/src/Element/ManagedFile.php
+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\file\Element;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\Core\Url;
@@ -130,9 +131,6 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
    * support for a default value.
    */
   public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) {
-    // Append the '-upload' to the #id so the field label's 'for' attribute
-    // corresponds with the file element.
-    $element['#id'] .= '-upload';
 
     // This is used sometimes so let's implode it just once.
     $parents_prefix = implode('_', $element['#parents']);
@@ -144,6 +142,9 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
     $element['#files'] = !empty($fids) ? File::loadMultiple($fids) : FALSE;
     $element['#tree'] = TRUE;
 
+    // Generate a unique wrapper HTML ID.
+    $ajax_wrapper_id = Html::getUniqueId('ajax-wrapper');
+
     $ajax_settings = [
       'url' => Url::fromRoute('file.ajax_upload'),
       'options' => [
@@ -152,7 +153,7 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
           'form_build_id' => $complete_form['form_build_id']['#value'],
         ],
       ],
-      'wrapper' => $element['#id'] . '-ajax-wrapper',
+      'wrapper' => $ajax_wrapper_id,
       'effect' => 'fade',
       'progress' => [
         'type' => $element['#progress_indicator'],
@@ -259,8 +260,12 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
       $element['upload']['#attached']['drupalSettings']['file']['elements']['#' . $element['#id']] = $extension_list;
     }
 
+    // Let #id point to the file element, so the field label's 'for' corresponds
+    // with it.
+    $element['#id'] = &$element['upload']['#id'];
+
     // Prefix and suffix used for Ajax replacement.
-    $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
+    $element['#prefix'] = '<div id="' . $ajax_wrapper_id . '">';
     $element['#suffix'] = '</div>';
 
     return $element;
diff --git a/core/modules/file/src/Tests/FileFieldWidgetTest.php b/core/modules/file/src/Tests/FileFieldWidgetTest.php
index ec02bb4..10f2cad 100644
--- a/core/modules/file/src/Tests/FileFieldWidgetTest.php
+++ b/core/modules/file/src/Tests/FileFieldWidgetTest.php
@@ -9,7 +9,6 @@
 
 use Drupal\comment\Entity\Comment;
 use Drupal\comment\Tests\CommentTestTrait;
-use Drupal\Component\Utility\Html;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field_ui\Tests\FieldUiTestTrait;
 use Drupal\user\RoleInterface;
@@ -86,7 +85,8 @@ function testSingleValuedWidget() {
       $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'After clicking the "Remove" button, it is no longer displayed.');
       $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.');
       // Test label has correct 'for' attribute.
-      $label = $this->xpath("//label[@for='edit-" . Html::cleanCssIdentifier($field_name) . "-0-upload']");
+      $input = $this->xpath('//input[@name="files[' . $field_name . '_0]"]');
+      $label = $this->xpath('//label[@for="' . (string) $input[0]['id'] . '"]');
       $this->assertTrue(isset($label[0]), 'Label for upload found.');
 
       // Save the node and ensure it does not have the file.
diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php
index 240aea9..54dba00 100644
--- a/core/modules/simpletest/src/AssertContentTrait.php
+++ b/core/modules/simpletest/src/AssertContentTrait.php
@@ -822,14 +822,13 @@ protected function assertThemeOutput($callback, array $variables = array(), $exp
   }
 
   /**
-   * Asserts that a field exists in the current page by the given XPath.
+   * Asserts that a field exists in the current page with a given Xpath result.
    *
-   * @param string $xpath
-   *   XPath used to find the field.
+   * @param \SimpleXmlElement[] $fields
+   *   Xml elements.
    * @param string $value
-   *   (optional) Value of the field to assert. You may pass in NULL (default)
-   *   to skip checking the actual value, while still checking that the field
-   *   exists.
+   *   (optional) Value of the field to assert. You may pass in NULL (default) to skip
+   *   checking the actual value, while still checking that the field exists.
    * @param string $message
    *   (optional) A message to display with the assertion. Do not translate
    *   messages: use format_string() to embed variables in the message text, not
@@ -843,9 +842,7 @@ protected function assertThemeOutput($callback, array $variables = array(), $exp
    * @return bool
    *   TRUE on pass, FALSE on fail.
    */
-  protected function assertFieldByXPath($xpath, $value = NULL, $message = '', $group = 'Other') {
-    $fields = $this->xpath($xpath);
-
+  protected function assertFieldsByValue($fields, $value = NULL, $message = '', $group = 'Other') {
     // If value specified then check array for match.
     $found = TRUE;
     if (isset($value)) {
@@ -881,6 +878,34 @@ protected function assertFieldByXPath($xpath, $value = NULL, $message = '', $gro
   }
 
   /**
+   * Asserts that a field exists in the current page by the given XPath.
+   *
+   * @param string $xpath
+   *   XPath used to find the field.
+   * @param string $value
+   *   (optional) Value of the field to assert. You may pass in NULL (default)
+   *   to skip checking the actual value, while still checking that the field
+   *   exists.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use format_string() to embed variables in the message text, not
+   *   t(). If left blank, a default message will be displayed.
+   * @param string $group
+   *   (optional) The group this message is in, which is displayed in a column
+   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
+   *   translate this string. Defaults to 'Other'; most tests do not override
+   *   this default.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertFieldByXPath($xpath, $value = NULL, $message = '', $group = 'Other') {
+    $fields = $this->xpath($xpath);
+
+    return $this->assertFieldsByValue($fields, $value, $message, $group);
+  }
+
+  /**
    * Get the selected value from a select field.
    *
    * @param \SimpleXmlElement $element
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 2cf859a..51c43e4 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -19,6 +19,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\EventSubscriber\AjaxSubscriber;
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Session\AccountInterface;
@@ -1674,13 +1675,7 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p
         $extra_post[$key] = $value;
       }
     }
-    $ajax_html_ids = array();
-    foreach ($this->xpath('//*[@id]') as $element) {
-      $ajax_html_ids[] = (string) $element['id'];
-    }
-    if (!empty($ajax_html_ids)) {
-      $extra_post['ajax_html_ids'] = implode(' ', $ajax_html_ids);
-    }
+    $extra_post[AjaxSubscriber::AJAX_REQUEST_PARAMETER] = 1;
     $extra_post += $this->getAjaxPageStatePostData();
     // Now serialize all the $extra_post values, and prepend it with an '&'.
     $extra_post = '&' . $this->serializePostValues($extra_post);
diff --git a/core/modules/system/src/Tests/Ajax/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php
index e290977..decd64c 100644
--- a/core/modules/system/src/Tests/Ajax/DialogTest.php
+++ b/core/modules/system/src/Tests/Ajax/DialogTest.php
@@ -124,6 +124,10 @@ public function testDialog() {
       // Don't send a target.
       'submit' => array()
     ));
+    // Make sure the selector ID starts with the right string.
+    $this->assert(strpos($ajax_result[3]['selector'], $no_target_expected_response['selector']) === 0, 'Selector starts with right string.');
+    unset($ajax_result[3]['selector']);
+    unset($no_target_expected_response['selector']);
     $this->assertEqual($no_target_expected_response, $ajax_result[3], 'Normal dialog with no target JSON response matches.');
 
     // Emulate closing the dialog via an AJAX request. There is no non-JS
diff --git a/core/modules/system/src/Tests/Ajax/MultiFormTest.php b/core/modules/system/src/Tests/Ajax/MultiFormTest.php
index 449861c..87a00fe 100644
--- a/core/modules/system/src/Tests/Ajax/MultiFormTest.php
+++ b/core/modules/system/src/Tests/Ajax/MultiFormTest.php
@@ -58,10 +58,9 @@ function testMultiForm() {
     // each Ajax submission, but these variables are stable and help target the
     // desired elements.
     $field_name = 'field_ajax_test';
-    $field_xpaths = array(
-      'node-page-form' => '//form[@id="node-page-form"]//div[contains(@class, "field-name-field-ajax-test")]',
-      'node-page-form--2' => '//form[@id="node-page-form--2"]//div[contains(@class, "field-name-field-ajax-test")]',
-    );
+
+    $form_xpath = '//form[starts-with(@id, "node-page-form")]';
+    $field_xpath = '//div[contains(@class, "field-name-field-ajax-test")]';
     $button_name = $field_name . '_add_more';
     $button_value = t('Add another item');
     $button_xpath_suffix = '//input[@name="' . $button_name . '"]';
@@ -71,19 +70,28 @@ function testMultiForm() {
     // of field items and "add more" button for the multi-valued field within
     // each form.
     $this->drupalGet('form-test/two-instances-of-same-form');
-    foreach ($field_xpaths as $field_xpath) {
-      $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == 1, 'Found the correct number of field items on the initial page.');
-      $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, 'Found the "add more" button on the initial page.');
+
+    $fields = $this->xpath($form_xpath . $field_xpath);
+    $this->assertEqual(count($fields), 2);
+    foreach ($fields as $field) {
+      $this->assertEqual(count($field->xpath('.' . $field_items_xpath_suffix)), 1, 'Found the correct number of field items on the initial page.');
+      $this->assertFieldsByValue($field->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button on the initial page.');
     }
+
     $this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other');
 
     // Submit the "add more" button of each form twice. After each corresponding
     // page update, ensure the same as above.
-    foreach ($field_xpaths as $form_html_id => $field_xpath) {
-      for ($i = 0; $i < 2; $i++) {
+    for ($i = 0; $i < 2; $i++) {
+      $forms = $this->xpath($form_xpath);
+      foreach ($forms as $offset => $form) {
+        $form_html_id = (string) $form['id'];
         $this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_html_id);
-        $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == $i+2, 'Found the correct number of field items after an AJAX submission.');
-        $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, 'Found the "add more" button after an AJAX submission.');
+        $form = $this->xpath($form_xpath)[$offset];
+        $field = $form->xpath('.' . $field_xpath);
+
+        $this->assertEqual(count($field[0]->xpath('.' . $field_items_xpath_suffix)), $i+2, 'Found the correct number of field items after an AJAX submission.');
+        $this->assertFieldsByValue($field[0]->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button after an AJAX submission.');
         $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other');
       }
     }
diff --git a/core/modules/views/src/Controller/ViewAjaxController.php b/core/modules/views/src/Controller/ViewAjaxController.php
index 2608344..9818f17 100644
--- a/core/modules/views/src/Controller/ViewAjaxController.php
+++ b/core/modules/views/src/Controller/ViewAjaxController.php
@@ -133,7 +133,7 @@ public function ajaxView(Request $request) {
 
       // Remove all of this stuff from the query of the request so it doesn't
       // end up in pagers and tablesort URLs.
-      foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids') as $key) {
+      foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax') as $key) {
         $request->query->remove($key);
         $request->request->remove($key);
       }
diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
index d2e9fe8..ec82885 100644
--- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
+++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php
@@ -412,7 +412,10 @@ protected function showBuildGroupButton(&$form, FormStateInterface $form_state)
         '#type' => 'submit',
         '#value' => $this->t('Grouped filters'),
         '#submit' => array(array($this, 'buildGroupForm')),
-        '#attributes' => array('class' => array('use-ajax-submit')),
+        '#attributes' => array(
+          'class' => array('use-ajax-submit'),
+          'data-drupal-selector' => 'options-group-button-button',
+        ),
       );
       $form['group_button']['radios']['radios']['#default_value'] = 0;
     }
@@ -422,7 +425,10 @@ protected function showBuildGroupButton(&$form, FormStateInterface $form_state)
         '#type' => 'submit',
         '#value' => $this->t('Single filter'),
         '#submit' => array(array($this, 'buildGroupForm')),
-        '#attributes' => array('class' => array('use-ajax-submit')),
+        '#attributes' => array(
+          'class' => array('use-ajax-submit'),
+          'data-drupal-selector' => 'options-group-button-button',
+        ),
       );
       $form['group_button']['radios']['radios']['#default_value'] = 1;
     }
@@ -487,7 +493,10 @@ public function showExposeButton(&$form, FormStateInterface $form_state) {
         '#type' => 'submit',
         '#value' => $this->t('Expose filter'),
         '#submit' => array(array($this, 'displayExposedForm')),
-        '#attributes' => array('class' => array('use-ajax-submit')),
+        '#attributes' => array(
+          'class' => array('use-ajax-submit'),
+          'data-drupal-selector' => 'options-expose-button-button',
+        ),
       );
       $form['expose_button']['checkbox']['checkbox']['#default_value'] = 0;
     }
@@ -500,7 +509,10 @@ public function showExposeButton(&$form, FormStateInterface $form_state) {
         '#type' => 'submit',
         '#value' => $this->t('Hide filter'),
         '#submit' => array(array($this, 'displayExposedForm')),
-        '#attributes' => array('class' => array('use-ajax-submit')),
+        '#attributes' => array(
+          'class' => array('use-ajax-submit'),
+          'data-drupal-selector' => 'options-expose-button-button',
+        ),
       );
       $form['expose_button']['checkbox']['checkbox']['#default_value'] = 1;
     }
diff --git a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
index 98f3797..55b60e1 100644
--- a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
+++ b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php
@@ -124,7 +124,10 @@ public function showExposeButton(&$form, FormStateInterface $form_state) {
         '#type' => 'submit',
         '#value' => $this->t('Expose sort'),
         '#submit' => array(array($this, 'displayExposedForm')),
-        '#attributes' => array('class' => array('use-ajax-submit')),
+        '#attributes' => array(
+          'class' => array('use-ajax-submit'),
+          'data-drupal-selector' => 'options-expose-button-button',
+        ),
       );
       $form['expose_button']['checkbox']['checkbox']['#default_value'] = 0;
     }
@@ -137,7 +140,10 @@ public function showExposeButton(&$form, FormStateInterface $form_state) {
         '#type' => 'submit',
         '#value' => $this->t('Hide sort'),
         '#submit' => array(array($this, 'displayExposedForm')),
-        '#attributes' => array('class' => array('use-ajax-submit')),
+        '#attributes' => array(
+          'class' => array('use-ajax-submit'),
+          'data-drupal-selector' => 'options-expose-button-button',
+        ),
       );
       $form['expose_button']['checkbox']['checkbox']['#default_value'] = 1;
     }
diff --git a/core/modules/views_ui/js/views-admin.js b/core/modules/views_ui/js/views-admin.js
index 17b16d5..8b9aeb5 100644
--- a/core/modules/views_ui/js/views-admin.js
+++ b/core/modules/views_ui/js/views-admin.js
@@ -21,7 +21,7 @@
     attach: function () {
       // Only show the SQL rewrite warning when the user has chosen the
       // corresponding checkbox.
-      $('#edit-query-options-disable-sql-rewrite').on('click', function () {
+      $('[name="query[options][disable_sql_rewrite]"]').on('click', function () {
         $('.sql-rewrite-warning').toggleClass('js-hide');
       });
     }
@@ -412,7 +412,7 @@
     /**
      * Add a keyup handler to the search box.
      */
-    this.$searchBox = this.$form.find('#edit-override-controls-options-search');
+    this.$searchBox = this.$form.find('[name="override[controls][options_search]"]');
     this.$searchBox.on('keyup', $.proxy(this.handleKeyup, this));
 
     /**
@@ -951,7 +951,7 @@
   Drupal.behaviors.viewsFilterConfigSelectAll = {
     attach: function (context) {
       // Show the select all checkbox.
-      $(context).find('#views-ui-handler-form div.form-item-options-value-all').once('filterConfigSelectAll')
+      $(context).find('[data-drupal-selector="views-ui-handler-form"] div.form-item-options-value-all').once('filterConfigSelectAll')
         .show()
         .find('input[type=checkbox]')
         .on('click', function () {
@@ -962,7 +962,7 @@
           });
         });
       // Uncheck the select all checkbox if any of the others are unchecked.
-      $('#views-ui-handler-form').find('div.js-form-type-checkbox').not($('.form-item-options-value-all'))
+      $('[data-drupal-selector="views-ui-handler-form"]').find('div.js-form-type-checkbox').not($('.form-item-options-value-all'))
         .find('input[type=checkbox]')
         .on('click', function () {
           if ($(this).is('checked') === false) {
@@ -990,7 +990,7 @@
    */
   Drupal.behaviors.viewsUiCheckboxify = {
     attach: function (context, settings) {
-      var $buttons = $('#edit-options-expose-button-button, #edit-options-group-button-button').once('views-ui-checkboxify');
+      var $buttons = $('[data-drupal-selector="options-expose-button-button"], [data-drupal-selector="options-group-button-button"]').once('views-ui-checkboxify');
       var length = $buttons.length;
       var i;
       for (i = 0; i < length; i++) {
@@ -1066,7 +1066,7 @@
    */
   Drupal.behaviors.viewsUiOverrideSelect = {
     attach: function (context) {
-      $(context).find('#edit-override-dropdown').once('views-ui-override-button-text').each(function () {
+      $(context).find('[name="override[dropdown]"]').once('views-ui-override-button-text').each(function () {
         // Closures! :(
         var $context = $(context);
         var $submit = $context.find('[id^=edit-submit]');
diff --git a/core/modules/views_ui/src/Form/Ajax/AddHandler.php b/core/modules/views_ui/src/Form/Ajax/AddHandler.php
index da54d50..00e2115 100644
--- a/core/modules/views_ui/src/Form/Ajax/AddHandler.php
+++ b/core/modules/views_ui/src/Form/Ajax/AddHandler.php
@@ -59,6 +59,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '#theme_wrappers' => array('container'),
         '#attributes' => array('class' => array('scroll'), 'data-drupal-views-scroll' => TRUE),
       ),
+      '#attributes' => array('data-drupal-selector' => 'views-ui-handler-form'),
     );
 
     $executable = $view->getExecutable();
diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php
index 6532689..54f9a61 100644
--- a/core/modules/views_ui/src/ViewUI.php
+++ b/core/modules/views_ui/src/ViewUI.php
@@ -582,7 +582,7 @@ public function renderPreview($display_id, $args = array()) {
       // have some input in the query parameters, so we merge request() and
       // query() to ensure we get it all.
       $exposed_input = array_merge(\Drupal::request()->request->all(), \Drupal::request()->query->all());
-      foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
+      foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
         if (isset($exposed_input[$key])) {
           unset($exposed_input[$key]);
         }
diff --git a/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
index 18b07f7..0addf3c 100644
--- a/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
@@ -143,19 +143,25 @@ public function providerTestHtmlGetUniqueId() {
    *   The expected result.
    * @param string $source
    *   The string being transformed to an ID.
-   * @param bool $reset
-   *   (optional) If TRUE, reset the list of seen IDs. Defaults to FALSE.
    *
    * @dataProvider providerTestHtmlGetUniqueIdWithAjaxIds
    *
    * @covers ::getUniqueId
    */
-  public function testHtmlGetUniqueIdWithAjaxIds($expected, $source, $reset = FALSE) {
-    if ($reset) {
-      Html::resetSeenIds();
+  public function testHtmlGetUniqueIdWithAjaxIds($expected, $source) {
+    Html::setIsAjax(TRUE);
+    $id = Html::getUniqueId($source);
+
+    // Note, we truncate two hyphens at the end.
+    // @see \Drupal\Component\Utility\Html::getId()
+    if (strpos($source, '--') !== FALSE) {
+      $random_suffix = substr($id, strlen($source) + 1);
     }
-    Html::setAjaxHtmlIds('test-unique-id1 test-unique-id2--3');
-    $this->assertSame($expected, Html::getUniqueId($source));
+    else {
+      $random_suffix = substr($id, strlen($source) + 2);
+    }
+    $expected = $expected . $random_suffix;
+    $this->assertSame($expected, $id);
   }
 
   /**
@@ -166,10 +172,11 @@ public function testHtmlGetUniqueIdWithAjaxIds($expected, $source, $reset = FALS
    */
   public function providerTestHtmlGetUniqueIdWithAjaxIds() {
     return array(
-      array('test-unique-id1--2', 'test-unique-id1', TRUE),
-      array('test-unique-id1--3', 'test-unique-id1'),
-      array('test-unique-id2--4', 'test-unique-id2', TRUE),
-      array('test-unique-id2--5', 'test-unique-id2'),
+      array('test-unique-id1--', 'test-unique-id1'),
+      // Note, we truncate two hyphens at the end.
+      // @see \Drupal\Component\Utility\Html::getId()
+      array('test-unique-id1---', 'test-unique-id1--'),
+      array('test-unique-id2--', 'test-unique-id2'),
     );
   }
 
@@ -186,6 +193,7 @@ public function providerTestHtmlGetUniqueIdWithAjaxIds() {
    * @covers ::getId
    */
   public function testHtmlGetId($expected, $source) {
+    Html::setIsAjax(FALSE);
     $this->assertSame($expected, Html::getId($source));
   }
 
