diff --git a/core/modules/system/src/Tests/Ajax/AjaxInGroupTest.php b/core/modules/system/src/Tests/Ajax/AjaxInGroupTest.php
deleted file mode 100644
index 4116f5f892..0000000000
--- a/core/modules/system/src/Tests/Ajax/AjaxInGroupTest.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-/**
- * Tests that form elements in groups work correctly with AJAX.
- *
- * @group Ajax
- */
-class AjaxInGroupTest extends AjaxTestBase {
-  protected function setUp() {
-    parent::setUp();
-
-    $this->drupalLogin($this->drupalCreateUser(['access content']));
-  }
-
-  /**
-   * Submits forms with select and checkbox elements via Ajax.
-   */
-  public function testSimpleAjaxFormValue() {
-    $this->drupalGet('/ajax_forms_test_get_form');
-    $this->assertText('Test group');
-    $this->assertText('AJAX checkbox in a group');
-
-    $this->drupalPostAjaxForm(NULL, ['checkbox_in_group' => TRUE], 'checkbox_in_group');
-    $this->assertText('Test group');
-    $this->assertText('AJAX checkbox in a group');
-    $this->assertText('AJAX checkbox in a nested group');
-    $this->assertText('Another AJAX checkbox in a nested group');
-  }
-
-}
diff --git a/core/modules/system/src/Tests/Ajax/AjaxTestBase.php b/core/modules/system/src/Tests/Ajax/AjaxTestBase.php
index 66e06c88a4..859bbbed41 100644
--- a/core/modules/system/src/Tests/Ajax/AjaxTestBase.php
+++ b/core/modules/system/src/Tests/Ajax/AjaxTestBase.php
@@ -2,10 +2,16 @@
 
 namespace Drupal\system\Tests\Ajax;
 
+@trigger_error(__FILE__ . ' is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. See https://www.drupal.org/node/2862510');
+
 use Drupal\simpletest\WebTestBase;
 
 /**
  * Provides a base class for Ajax tests.
+ *
+ * @deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0.
+  *
+  * @see https://www.drupal.org/node/2862510
  */
 abstract class AjaxTestBase extends WebTestBase {
 
diff --git a/core/modules/system/src/Tests/Ajax/ElementValidationTest.php b/core/modules/system/src/Tests/Ajax/ElementValidationTest.php
deleted file mode 100644
index d20f453be4..0000000000
--- a/core/modules/system/src/Tests/Ajax/ElementValidationTest.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-/**
- * Various tests of AJAX behavior.
- *
- * @group Ajax
- */
-class ElementValidationTest extends AjaxTestBase {
-  /**
-   * Tries to post an Ajax change to a form that has a validated element.
-   *
-   * The drivertext field is Ajax-enabled. An additional field is not, but
-   * is set to be a required field. In this test the required field is not
-   * filled in, and we want to see if the activation of the "drivertext"
-   * Ajax-enabled field fails due to the required field being empty.
-   */
-  public function testAjaxElementValidation() {
-    $edit = ['drivertext' => t('some dumb text')];
-
-    // Post with 'drivertext' as the triggering element.
-    $this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivertext');
-    // Look for a validation failure in the resultant JSON.
-    $this->assertNoText(t('Error message'), 'No error message in resultant JSON');
-    $this->assertText('ajax_forms_test_validation_form_callback invoked', 'The correct callback was invoked');
-
-    $this->drupalGet('ajax_validation_test');
-    $edit = ['drivernumber' => 12345];
-
-    // Post with 'drivernumber' as the triggering element.
-    $this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivernumber');
-    // Look for a validation failure in the resultant JSON.
-    $this->assertNoText(t('Error message'), 'No error message in resultant JSON');
-    $this->assertText('ajax_forms_test_validation_number_form_callback invoked', 'The correct callback was invoked');
-  }
-
-}
diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php
similarity index 54%
rename from core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php
rename to core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php
index 11a4044e07..2f36e0d48d 100644
--- a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormCacheTest.php
@@ -1,17 +1,25 @@
 <?php
 
-namespace Drupal\system\Tests\Ajax;
+namespace Drupal\FunctionalJavascriptTests\Ajax;
 
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Url;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
 
 /**
  * Tests the usage of form caching for AJAX forms.
  *
  * @group Ajax
  */
-class AjaxFormCacheTest extends AjaxTestBase {
+class AjaxFormCacheTest extends JavascriptTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['ajax_test', 'ajax_forms_test'];
 
   /**
    * Tests the usage of form cache for AJAX forms.
@@ -47,11 +55,28 @@ public function testBlockForms() {
     $this->drupalPlaceBlock('ajax_forms_test_block');
 
     $this->drupalGet('');
-    $this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
-    $this->assertOptionSelectedWithDrupalSelector('edit-test1', 'option1');
-    $this->assertOptionWithDrupalSelector('edit-test1', 'option3');
-    $this->drupalPostForm(NULL, ['test1' => 'option1'], 'Submit');
-    $this->assertText('Submission successful.');
+    $session = $this->getSession();
+
+    // Select first option and trigger ajax update.
+    $session->getPage()->selectFieldOption('edit-test1', 'option1');
+
+    // Wait for DOM update:
+    // The InsertCommand in the AJAX response changes the text in the option
+    // element to 'Option1!!!'
+    $opt1_selector = $this->assertSession()->waitForElement('css', "select[data-drupal-selector='edit-test1'] option:contains('Option 1!!!')");
+    $this->assertNotEmpty($opt1_selector);
+    $this->assertTrue($opt1_selector->isSelected());
+
+    // Confirm option 3 exists.
+    $page = $session->getPage();
+    $opt3_selector = $page->find('xpath', '//select[@data-drupal-selector="edit-test1"]//option[@value="option3"]');
+    $this->assertNotEmpty($opt3_selector);
+
+    // Confirm success message appears after a submit.
+    $page->findButton('edit-submit')->click();
+    $this->assertSession()->waitForButton('edit-submit');
+    $updated_page = $session->getPage();
+    $updated_page->hasContent('Submission successful.');
   }
 
   /**
@@ -65,7 +90,16 @@ public function testQueryString() {
 
     $url = Url::fromRoute('entity.user.canonical', ['user' => $this->rootUser->id()], ['query' => ['foo' => 'bar']]);
     $this->drupalGet($url);
-    $this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
+
+    $session = $this->getSession();
+    // Select first option and trigger ajax update.
+    $session->getPage()->selectFieldOption('edit-test1', 'option1');
+
+    // DOM update: The InsertCommand in the AJAX response changes the text
+    // in the option element to 'Option1!!!'
+    $opt1_selector = $this->assertSession()->waitForElement('css', "option:contains('Option 1!!!')");
+    $this->assertNotEmpty($opt1_selector);
+
     $url->setOption('query', [
       'foo' => 'bar',
       FormBuilderInterface::AJAX_FORM_REQUEST => 1,
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php
new file mode 100644
index 0000000000..621e113ac6
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxInGroupTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+/**
+ * Tests that form elements in groups work correctly with AJAX.
+ *
+ * @group Ajax
+ */
+class AjaxInGroupTest extends AjaxTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->drupalLogin($this->drupalCreateUser(['access content']));
+  }
+
+  /**
+   * Submits forms with select and checkbox elements via Ajax.
+   */
+  public function testSimpleAjaxFormValue() {
+    $this->drupalGet('/ajax_forms_test_get_form');
+    $this->assertText('Test group');
+    $this->assertText('AJAX checkbox in a group');
+
+    $session = $this->getSession();
+    $checkbox_original = $session->getPage()->findField('checkbox_in_group');
+    $this->assertNotNull($checkbox_original, 'The checkbox_in_group is on the page.');
+    $original_id = $checkbox_original->getAttribute('id');
+
+    // Triggers a AJAX request/response.
+    $checkbox_original->check();
+
+    // The reponse contains a new nested "test group" form element, similar
+    // to the one already in the DOM except for a change in the form build id.
+    $checkbox_new = $this->assertSession()->waitForElement('xpath', "//input[@name='checkbox_in_group' and not(@id='$original_id')]");
+    $this->assertNotNull($checkbox_new, 'DOM update: clicking the checkbox refreshed the checkbox_in_group structure');
+
+    $this->assertText('Test group');
+    $this->assertText('AJAX checkbox in a group');
+    $this->assertText('AJAX checkbox in a nested group');
+    $this->assertText('Another AJAX checkbox in a nested group');
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTestBase.php
new file mode 100644
index 0000000000..b2c0b5697f
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTestBase.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\Component\Render\FormattableMarkup;
+
+/**
+ * Provides a base class for Ajax tests.
+ */
+abstract class AjaxTestBase extends JavascriptTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+  /**
+   * Asserts the array of Ajax commands contains the searched command.
+   *
+   * An AjaxResponse object stores an array of Ajax commands. This array
+   * sometimes includes commands automatically provided by the framework in
+   * addition to commands returned by a particular controller. During testing,
+   * we're usually interested that a particular command is present, and don't
+   * care whether other commands precede or follow the one we're interested in.
+   * Additionally, the command we're interested in may include additional data
+   * that we're not interested in. Therefore, this function simply asserts that
+   * one of the commands in $haystack contains all of the keys and values in
+   * $needle. Furthermore, if $needle contains a 'settings' key with an array
+   * value, we simply assert that all keys and values within that array are
+   * present in the command we're checking, and do not consider it a failure if
+   * the actual command contains additional settings that aren't part of
+   * $needle.
+   *
+   * @param $haystack
+   *   An array of rendered Ajax commands returned by the server.
+   * @param $needle
+   *   Array of info we're expecting in one of those commands.
+   * @param $message
+   *   An assertion message.
+   */
+  public function assertCommand($haystack, $needle, $message) {
+    $found = FALSE;
+    foreach ($haystack as $command) {
+      // If the command has additional settings that we're not testing for, do
+      // not consider that a failure.
+      if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) {
+        $command['settings'] = array_intersect_key($command['settings'], $needle['settings']);
+      }
+      // If the command has additional data that we're not testing for, do not
+      // consider that a failure. Also, == instead of ===, because we don't
+      // require the key/value pairs to be in any particular order
+      // (http://php.net/manual/language.operators.array.php).
+      if (array_intersect_key($command, $needle) == $needle) {
+        $found = TRUE;
+        break;
+      }
+    }
+    $this->assertTrue($found, $message);
+  }
+
+  /**
+   * Asserts that each HTML ID is used for just a single element.
+   *
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() 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.
+   * @param array $ids_to_skip
+   *   An optional array of ids to skip when checking for duplicates. It is
+   *   always a bug to have duplicate HTML IDs, so this parameter is to enable
+   *   incremental fixing of core code. Whenever a test passes this parameter,
+   *   it should add a "todo" comment above the call to this function explaining
+   *   the legacy bug that the test wishes to ignore and including a link to an
+   *   issue that is working to fix that legacy bug.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  public function assertNoDuplicateIds($message = '', $group = 'Other', $ids_to_skip = []) {
+    $status = TRUE;
+    $seen_ids = [];
+    $page = $this->getSession()->getPage();
+
+    $elements = $page->findAll('xpath', '//*[@id]');
+    foreach ($elements as $element) {
+      $id = $element->getAttribute('id');
+      if (isset($seen_ids[$id]) && !in_array($id, $ids_to_skip)) {
+        $this->fail(new  FormattableMarkup('The HTML ID %id is unique.', ['%id' => $id]), $group);
+        $status = FALSE;
+      }
+      $seen_ids[$id] = TRUE;
+    }
+    return $this->assert($status, $message, $group);
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php
new file mode 100644
index 0000000000..34a2b191e0
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ElementValidationTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+/**
+ * Various tests of AJAX behavior.
+ *
+ * @group Ajax
+ */
+class ElementValidationTest extends AjaxTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['ajax_test', 'ajax_forms_test'];
+
+  /**
+   * Tries to post an Ajax change to a form that has a validated elements.
+   *
+   * Drupal AJAX commands in the response update the DOM echoing back the
+   * validated values in the form of messages that appear on the page.
+   */
+  public function testAjaxElementValidation() {
+    $this->drupalGet('ajax_validation_test');
+
+    // Partially complete the form with a string.
+    $this->getSession()->getPage()->fillField('drivertext', 'some dumb text');
+
+    // When the AJAX command updates the DOM a <ul> unsorted list
+    // "message__list" structure will appear on the page echoing back the
+    // "some dumb text" message.
+    $placeholder_text = $this->assertSession()->waitForElement('css', "ul.messages__list li.messages__item em:contains('some dumb text')");
+    $this->assertNotNull($placeholder_text, 'A callback successfully echoed back a string.');
+
+    // Partialy complete the form with a number.
+    $this->getSession()->getPage()->fillField('drivernumber', '12345');
+
+    // The AJAX request/resonse will complete successfully when a InsertCommand
+    // injects a message with a placeholder element into the DOM with the
+    // submitted number.
+    $placeholder_number = $this->assertSession()->waitForElement('css', "ul.messages__list li.messages__item em:contains('12345')");
+    $this->assertNotNull($placeholder_number, 'A callback successfully echoed back a number.');
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Ajax/FormValuesTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php
similarity index 50%
rename from core/modules/system/src/Tests/Ajax/FormValuesTest.php
rename to core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php
index 9f4b521752..c9dd7d9043 100644
--- a/core/modules/system/src/Tests/Ajax/FormValuesTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/FormValuesTest.php
@@ -1,15 +1,22 @@
 <?php
 
-namespace Drupal\system\Tests\Ajax;
+namespace Drupal\FunctionalJavascriptTests\Ajax;
 
-use Drupal\Core\Ajax\DataCommand;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
 
 /**
  * Tests that form values are properly delivered to AJAX callbacks.
  *
  * @group Ajax
  */
-class FormValuesTest extends AjaxTestBase {
+class FormValuesTest extends JavascriptTestBase {
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node','ajax_test', 'ajax_forms_test'];
+
   protected function setUp() {
     parent::setUp();
 
@@ -20,25 +27,30 @@ protected function setUp() {
    * Submits forms with select and checkbox elements via Ajax.
    */
   public function testSimpleAjaxFormValue() {
+
+    $this->drupalGet('ajax_forms_test_get_form');
+
+    $session = $this->getSession();
+    $assertSession = $this->assertSession();
+
     // Verify form values of a select element.
-    foreach (['red', 'green', 'blue'] as $item) {
-      $edit = [
-        'select' => $item,
-      ];
-      $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'select');
-      $expected = new DataCommand('#ajax_selected_color', 'form_state_value_select', $item);
-      $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a selectbox issued with a correct value.');
+    foreach (['green', 'blue', 'red'] as $item) {
+      // Updating the field will trigger a AJAX request/response.
+      $session->getPage()->selectFieldOption('select', $item);
+
+      // The AJAX command in the response will update the DOM
+      $select = $assertSession->waitForElement('css', "div#ajax_selected_color div:contains('$item')");
+      $this->assertNotNull($select, "DataCommand has updated the page with a value of $item.");
     }
 
     // Verify form values of a checkbox element.
-    foreach ([FALSE, TRUE] as $item) {
-      $edit = [
-        'checkbox' => $item,
-      ];
-      $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'checkbox');
-      $expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item);
-      $this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.');
-    }
+    $session->getPage()->checkField('checkbox');
+    $div0 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value div:contains('1')");
+    $this->assertNotNull($div0, 'DataCommand updates the DOM as expected when a checkbox is selected');
+
+    $session->getPage()->uncheckField('checkbox');
+    $div1 = $this->assertSession()->waitForElement('css', "div#ajax_checkbox_value div:contains('0')");
+    $this->assertNotNull($div1, 'DataCommand updates the DOM as expected when a checkbox is de-selected');
 
     // Verify that AJAX elements with invalid callbacks return error code 500.
     // Ensure the test error log is empty before these tests.
diff --git a/core/modules/system/src/Tests/Ajax/MultiFormTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php
similarity index 62%
rename from core/modules/system/src/Tests/Ajax/MultiFormTest.php
rename to core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php
index 9ca2a66781..4acf9963b9 100644
--- a/core/modules/system/src/Tests/Ajax/MultiFormTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/MultiFormTest.php
@@ -1,10 +1,11 @@
 <?php
 
-namespace Drupal\system\Tests\Ajax;
+namespace Drupal\FunctionalJavascriptTests\Ajax;
 
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\Ajax\AjaxTestBase;
 
 /**
  * Tests that AJAX-enabled forms work when multiple instances of the same form
@@ -15,11 +16,9 @@
 class MultiFormTest extends AjaxTestBase {
 
   /**
-   * Modules to enable.
-   *
-   * @var array
+   * {@inheritdoc}
    */
-  public static $modules = ['form_test'];
+  public static $modules = ['node', 'form_test'];
 
   protected function setUp() {
     parent::setUp();
@@ -68,11 +67,16 @@ public function testMultiForm() {
     // each form.
     $this->drupalGet('form-test/two-instances-of-same-form');
 
-    $fields = $this->xpath($form_xpath . $field_xpath);
+    // Wait for javascript on the page to prepare the form attributes.
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $fields = $page->findAll('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->assertEqual(count($field->find('xpath', '.' . $field_items_xpath_suffix)), 1, 'Found the correct number of field items on the initial page.');
+      $this->assertFieldsByValue($field->find('xpath', '.' . $button_xpath_suffix), NULL, 'Found the "add more" button on the initial page.');
     }
 
     $this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other');
@@ -81,15 +85,20 @@ public function testMultiForm() {
     // page update, ensure the same as above.
 
     for ($i = 0; $i < 2; $i++) {
-      $forms = $this->xpath($form_xpath);
+      $forms = $page->find('xpath', $form_xpath);
       foreach ($forms as $offset => $form) {
-        $form_html_id = (string) $form['id'];
-        $this->drupalPostAjaxForm(NULL, [], [$button_name => $button_value], NULL, [], [], $form_html_id);
-        $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.');
+        $button = $form->findButton($button_value);
+        $this->assertNotNull($button, 'Add Another Item button exists');
+        $button->press();
+
+        // Wait for page update.
+        $this->assertSession()->assertWaitOnAjaxRequest();
+
+        // After AJAX request and response page will update.
+        $page_updated = $session->getPage();
+        $field = $page_updated->findAll('xpath', '.' . $field_xpath);
+        $this->assertEqual(count($field[0]->find('xpath', '.' . $field_items_xpath_suffix)), $i + 2, 'Found the correct number of field items after an AJAX submission.');
+        $this->assertFieldsByValue($field[0]->find('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/tests/Drupal/FunctionalTests/AJAX/AjaxTestBase.php b/core/tests/Drupal/FunctionalTests/AJAX/AjaxTestBase.php
new file mode 100644
index 0000000000..631e39ed59
--- /dev/null
+++ b/core/tests/Drupal/FunctionalTests/AJAX/AjaxTestBase.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\FunctionalTests\AJAX;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Provides a base class for Ajax tests.
+ */
+abstract class AjaxTestBase extends BrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+  /**
+   * Asserts the array of Ajax commands contains the searched command.
+   *
+   * An AjaxResponse object stores an array of Ajax commands. This array
+   * sometimes includes commands automatically provided by the framework in
+   * addition to commands returned by a particular controller. During testing,
+   * we're usually interested that a particular command is present, and don't
+   * care whether other commands precede or follow the one we're interested in.
+   * Additionally, the command we're interested in may include additional data
+   * that we're not interested in. Therefore, this function simply asserts that
+   * one of the commands in $haystack contains all of the keys and values in
+   * $needle. Furthermore, if $needle contains a 'settings' key with an array
+   * value, we simply assert that all keys and values within that array are
+   * present in the command we're checking, and do not consider it a failure if
+   * the actual command contains additional settings that aren't part of
+   * $needle.
+   *
+   * @param $haystack
+   *   An array of rendered Ajax commands returned by the server.
+   * @param $needle
+   *   Array of info we're expecting in one of those commands.
+   * @param $message
+   *   An assertion message.
+   */
+  protected function assertCommand($haystack, $needle, $message) {
+    $found = FALSE;
+    foreach ($haystack as $command) {
+      // If the command has additional settings that we're not testing for, do
+      // not consider that a failure.
+      if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) {
+        $command['settings'] = array_intersect_key($command['settings'], $needle['settings']);
+      }
+      // If the command has additional data that we're not testing for, do not
+      // consider that a failure. Also, == instead of ===, because we don't
+      // require the key/value pairs to be in any particular order
+      // (http://php.net/manual/language.operators.array.php).
+      if (array_intersect_key($command, $needle) == $needle) {
+        $found = TRUE;
+        break;
+      }
+    }
+    $this->assertTrue($found, $message);
+  }
+
+}
