diff --git a/core/.eslintrc.passing.json b/core/.eslintrc.passing.json
index abb9a63c..15c26571 100644
--- a/core/.eslintrc.passing.json
+++ b/core/.eslintrc.passing.json
@@ -9,6 +9,7 @@
     "default-case": "off",
     "prefer-destructuring": "off",
     "operator-linebreak": "off",
+    "no-else-return": "off",
     "no-restricted-globals": "off",
     "react/no-this-in-sfc": "off",
     "react/destructuring-assignment": "off",
diff --git a/core/misc/tabledrag.es6.js b/core/misc/tabledrag.es6.js
index 93cc0349..29e8e03e 100644
--- a/core/misc/tabledrag.es6.js
+++ b/core/misc/tabledrag.es6.js
@@ -1107,7 +1107,7 @@
       delta = (delta > 0 && delta < trigger) ? delta : trigger;
       return delta * this.scrollSettings.amount;
     }
-    if (cursorY - scrollY < trigger) {
+    else if (cursorY - scrollY < trigger) {
       delta = trigger / (cursorY - scrollY);
       delta = (delta > 0 && delta < trigger) ? delta : trigger;
       return -delta * this.scrollSettings.amount;
diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js
index 97ead1c6..3330aeb8 100644
--- a/core/misc/tabledrag.js
+++ b/core/misc/tabledrag.js
@@ -725,8 +725,7 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
       delta = trigger / (windowHeight + scrollY - cursorY);
       delta = delta > 0 && delta < trigger ? delta : trigger;
       return delta * this.scrollSettings.amount;
-    }
-    if (cursorY - scrollY < trigger) {
+    } else if (cursorY - scrollY < trigger) {
       delta = trigger / (cursorY - scrollY);
       delta = delta > 0 && delta < trigger ? delta : trigger;
       return -delta * this.scrollSettings.amount;
diff --git a/core/modules/book/book.es6.js b/core/modules/book/book.es6.js
index 5458e614..c04929bf 100644
--- a/core/modules/book/book.es6.js
+++ b/core/modules/book/book.es6.js
@@ -21,7 +21,7 @@
         if (val === '0') {
           return Drupal.t('Not in book');
         }
-        if (val === 'new') {
+        else if (val === 'new') {
           return Drupal.t('New book');
         }
 
diff --git a/core/modules/book/book.js b/core/modules/book/book.js
index 0ed6601a..9000c932 100644
--- a/core/modules/book/book.js
+++ b/core/modules/book/book.js
@@ -14,8 +14,7 @@
 
         if (val === '0') {
           return Drupal.t('Not in book');
-        }
-        if (val === 'new') {
+        } else if (val === 'new') {
           return Drupal.t('New book');
         }
 
diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.es6.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.es6.js
index ec512440..94f92e6b 100644
--- a/core/modules/ckeditor/js/plugins/drupalimage/plugin.es6.js
+++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.es6.js
@@ -159,7 +159,7 @@
             return;
           }
           // Don't initialize on pasted fake objects.
-          if (element.attributes['data-cke-realelement']) {
+          else if (element.attributes['data-cke-realelement']) {
             return;
           }
 
diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
index 72180faa..8176a72a 100644
--- a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
@@ -95,11 +95,9 @@
         widgetDefinition.upcast = function (element, data) {
           if (element.name !== 'img') {
             return;
-          }
-
-          if (element.attributes['data-cke-realelement']) {
-            return;
-          }
+          } else if (element.attributes['data-cke-realelement']) {
+              return;
+            }
 
           data['data-entity-type'] = element.attributes['data-entity-type'];
 
diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js
index 6b6a09e2..140a1842 100644
--- a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js
+++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js
@@ -139,7 +139,7 @@
             return;
           }
           // Don't initialize on pasted fake objects.
-          if (element.attributes['data-cke-realelement']) {
+          else if (element.attributes['data-cke-realelement']) {
             return;
           }
 
diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
index 7cd215a7..d19f3b3a 100644
--- a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
@@ -88,11 +88,9 @@
         widgetDefinition.upcast = function (element, data) {
           if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) {
             return;
-          }
-
-          if (element.attributes['data-cke-realelement']) {
-            return;
-          }
+          } else if (element.attributes['data-cke-realelement']) {
+              return;
+            }
 
           element = originalUpcast.call(this, element, data);
           var attrs = element.attributes;
diff --git a/core/modules/media/js/form.es6.js b/core/modules/media/js/form.es6.js
index 73948c0f..cb0da60d 100644
--- a/core/modules/media/js/form.es6.js
+++ b/core/modules/media/js/form.es6.js
@@ -24,10 +24,10 @@
         if (name && date) {
           return Drupal.t('By @name on @date', { '@name': name, '@date': date });
         }
-        if (name) {
+        else if (name) {
           return Drupal.t('By @name', { '@name': name });
         }
-        if (date) {
+        else if (date) {
           return Drupal.t('Authored on @date', { '@date': date });
         }
       });
diff --git a/core/modules/media/js/form.js b/core/modules/media/js/form.js
index 09394c4c..d033ec50 100644
--- a/core/modules/media/js/form.js
+++ b/core/modules/media/js/form.js
@@ -17,11 +17,9 @@
 
         if (name && date) {
           return Drupal.t('By @name on @date', { '@name': name, '@date': date });
-        }
-        if (name) {
+        } else if (name) {
           return Drupal.t('By @name', { '@name': name });
-        }
-        if (date) {
+        } else if (date) {
           return Drupal.t('Authored on @date', { '@date': date });
         }
       });
diff --git a/core/modules/node/node.es6.js b/core/modules/node/node.es6.js
index 26d0792e..6d6beb92 100644
--- a/core/modules/node/node.es6.js
+++ b/core/modules/node/node.es6.js
@@ -24,10 +24,10 @@
         if (name && date) {
           return Drupal.t('By @name on @date', { '@name': name, '@date': date });
         }
-        if (name) {
+        else if (name) {
           return Drupal.t('By @name', { '@name': name });
         }
-        if (date) {
+        else if (date) {
           return Drupal.t('Authored on @date', { '@date': date });
         }
       });
diff --git a/core/modules/node/node.js b/core/modules/node/node.js
index b7fbae02..fc2da881 100644
--- a/core/modules/node/node.js
+++ b/core/modules/node/node.js
@@ -17,11 +17,9 @@
 
         if (name && date) {
           return Drupal.t('By @name on @date', { '@name': name, '@date': date });
-        }
-        if (name) {
+        } else if (name) {
           return Drupal.t('By @name', { '@name': name });
-        }
-        if (date) {
+        } else if (date) {
           return Drupal.t('Authored on @date', { '@date': date });
         }
       });
diff --git a/core/modules/quickedit/js/models/EntityModel.es6.js b/core/modules/quickedit/js/models/EntityModel.es6.js
index 3fb7cdd6..4ea83713 100644
--- a/core/modules/quickedit/js/models/EntityModel.es6.js
+++ b/core/modules/quickedit/js/models/EntityModel.es6.js
@@ -514,7 +514,7 @@
         }
         // If that function accepts it, then ensure all fields are also in an
         // acceptable state.
-        if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
+        else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
           return 'state change not accepted because fields are not in acceptable state';
         }
       }
diff --git a/core/modules/quickedit/js/models/EntityModel.js b/core/modules/quickedit/js/models/EntityModel.js
index 15584400..3c55c6ab 100644
--- a/core/modules/quickedit/js/models/EntityModel.js
+++ b/core/modules/quickedit/js/models/EntityModel.js
@@ -256,11 +256,9 @@
 
         if (!this._acceptStateChange(currentState, nextState, options)) {
           return 'state change not accepted';
-        }
-
-        if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
-          return 'state change not accepted because fields are not in acceptable state';
-        }
+        } else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
+            return 'state change not accepted because fields are not in acceptable state';
+          }
       }
 
       var currentIsCommitting = this.get('isCommitting');
diff --git a/core/modules/quickedit/js/quickedit.es6.js b/core/modules/quickedit/js/quickedit.es6.js
index 145c26dc..e4c35b13 100644
--- a/core/modules/quickedit/js/quickedit.es6.js
+++ b/core/modules/quickedit/js/quickedit.es6.js
@@ -236,7 +236,7 @@
     // The entity for the given contextual link contains at least one field that
     // the current user may edit in-place; instantiate EntityModel,
     // EntityDecorationView and ContextualLinkView.
-    if (hasFieldWithPermission(fieldIDs)) {
+    else if (hasFieldWithPermission(fieldIDs)) {
       const entityModel = new Drupal.quickedit.EntityModel({
         el: contextualLink.region,
         entityID: contextualLink.entityID,
@@ -278,7 +278,7 @@
     }
     // There was not at least one field that the current user may edit in-place,
     // even though the metadata for all fields within this entity is available.
-    if (allMetadataExists(fieldIDs)) {
+    else if (allMetadataExists(fieldIDs)) {
       return true;
     }
 
diff --git a/core/modules/quickedit/js/quickedit.js b/core/modules/quickedit/js/quickedit.js
index d8317da8..4d49eb55 100644
--- a/core/modules/quickedit/js/quickedit.js
+++ b/core/modules/quickedit/js/quickedit.js
@@ -120,47 +120,43 @@
 
     if (fieldIDs.length === 0) {
       return false;
-    }
-
-    if (hasFieldWithPermission(fieldIDs)) {
-      var entityModel = new Drupal.quickedit.EntityModel({
-        el: contextualLink.region,
-        entityID: contextualLink.entityID,
-        entityInstanceID: contextualLink.entityInstanceID,
-        id: contextualLink.entityID + '[' + contextualLink.entityInstanceID + ']',
-        label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label')
-      });
-      Drupal.quickedit.collections.entities.add(entityModel);
-
-      var entityDecorationView = new Drupal.quickedit.EntityDecorationView({
-        el: contextualLink.region,
-        model: entityModel
-      });
-      entityModel.set('entityDecorationView', entityDecorationView);
+    } else if (hasFieldWithPermission(fieldIDs)) {
+        var entityModel = new Drupal.quickedit.EntityModel({
+          el: contextualLink.region,
+          entityID: contextualLink.entityID,
+          entityInstanceID: contextualLink.entityInstanceID,
+          id: contextualLink.entityID + '[' + contextualLink.entityInstanceID + ']',
+          label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label')
+        });
+        Drupal.quickedit.collections.entities.add(entityModel);
 
-      _.each(fields, function (field) {
-        initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
-      });
-      fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
-
-      var initContextualLink = _.once(function () {
-        var $links = $(contextualLink.el).find('.contextual-links');
-        var contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
-          el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
-          model: entityModel,
-          appModel: Drupal.quickedit.app.model
-        }, options));
-        entityModel.set('contextualLinkView', contextualLinkView);
-      });
+        var entityDecorationView = new Drupal.quickedit.EntityDecorationView({
+          el: contextualLink.region,
+          model: entityModel
+        });
+        entityModel.set('entityDecorationView', entityDecorationView);
 
-      loadMissingEditors(initContextualLink);
+        _.each(fields, function (field) {
+          initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
+        });
+        fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
+
+        var initContextualLink = _.once(function () {
+          var $links = $(contextualLink.el).find('.contextual-links');
+          var contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
+            el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
+            model: entityModel,
+            appModel: Drupal.quickedit.app.model
+          }, options));
+          entityModel.set('contextualLinkView', contextualLinkView);
+        });
 
-      return true;
-    }
+        loadMissingEditors(initContextualLink);
 
-    if (allMetadataExists(fieldIDs)) {
-      return true;
-    }
+        return true;
+      } else if (allMetadataExists(fieldIDs)) {
+          return true;
+        }
 
     return false;
   }
diff --git a/core/modules/system/src/Tests/Ajax/CommandsTest.php b/core/modules/system/src/Tests/Ajax/CommandsTest.php
deleted file mode 100644
index b91ac982..00000000
--- a/core/modules/system/src/Tests/Ajax/CommandsTest.php
+++ /dev/null
@@ -1,159 +0,0 @@
-<?php
-
-namespace Drupal\system\Tests\Ajax;
-
-use Drupal\Core\Ajax\AddCssCommand;
-use Drupal\Core\Ajax\AfterCommand;
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\AlertCommand;
-use Drupal\Core\Ajax\AppendCommand;
-use Drupal\Core\Ajax\BeforeCommand;
-use Drupal\Core\Ajax\ChangedCommand;
-use Drupal\Core\Ajax\CssCommand;
-use Drupal\Core\Ajax\DataCommand;
-use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Core\Ajax\InvokeCommand;
-use Drupal\Core\Ajax\InsertCommand;
-use Drupal\Core\Ajax\PrependCommand;
-use Drupal\Core\Ajax\RemoveCommand;
-use Drupal\Core\Ajax\RestripeCommand;
-use Drupal\Core\Ajax\SettingsCommand;
-use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
-
-/**
- * Performs tests on AJAX framework commands.
- *
- * @group Ajax
- */
-class CommandsTest extends AjaxTestBase {
-
-  /**
-   * Tests the various Ajax Commands.
-   */
-  public function testAjaxCommands() {
-    $form_path = 'ajax_forms_test_ajax_commands_form';
-    $web_user = $this->drupalCreateUser(['access content']);
-    $this->drupalLogin($web_user);
-
-    $edit = [];
-
-    // Tests the 'add_css' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'add_css' command")]);
-    $expected = new AddCssCommand('my/file.css');
-    $this->assertCommand($commands, $expected->render(), "'add_css' AJAX command issued with correct data.");
-
-    // Tests the 'after' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'After': Click to put something after the div")]);
-    $expected = new AfterCommand('#after_div', 'This will be placed after');
-    $this->assertCommand($commands, $expected->render(), "'after' AJAX command issued with correct data.");
-
-    // Tests the 'alert' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'Alert': Click to alert")]);
-    $expected = new AlertCommand(t('Alert'));
-    $this->assertCommand($commands, $expected->render(), "'alert' AJAX Command issued with correct text.");
-
-    // Tests the 'append' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'Append': Click to append something")]);
-    $expected = new AppendCommand('#append_div', 'Appended text');
-    $this->assertCommand($commands, $expected->render(), "'append' AJAX command issued with correct data.");
-
-    // Tests the 'before' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'before': Click to put something before the div")]);
-    $expected = new BeforeCommand('#before_div', 'Before text');
-    $this->assertCommand($commands, $expected->render(), "'before' AJAX command issued with correct data.");
-
-    // Tests the 'changed' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX changed: Click to mark div changed.")]);
-    $expected = new ChangedCommand('#changed_div');
-    $this->assertCommand($commands, $expected->render(), "'changed' AJAX command issued with correct selector.");
-
-    // Tests the 'changed' command using the second argument.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX changed: Click to mark div changed with asterisk.")]);
-    $expected = new ChangedCommand('#changed_div', '#changed_div_mark_this');
-    $this->assertCommand($commands, $expected->render(), "'changed' AJAX command (with asterisk) issued with correct selector.");
-
-    // Tests the 'css' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("Set the '#box' div to be blue.")]);
-    $expected = new CssCommand('#css_div', ['background-color' => 'blue']);
-    $this->assertCommand($commands, $expected->render(), "'css' AJAX command issued with correct selector.");
-
-    // Tests the 'data' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX data command: Issue command.")]);
-    $expected = new DataCommand('#data_div', 'testkey', 'testvalue');
-    $this->assertCommand($commands, $expected->render(), "'data' AJAX command issued with correct key and value.");
-
-    // Tests the 'html' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX html: Replace the HTML in a selector.")]);
-    $expected = new HtmlCommand('#html_div', 'replacement text');
-    $this->assertCommand($commands, $expected->render(), "'html' AJAX command issued with correct data.");
-
-    // Tests the 'insert' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX insert: Let client insert based on #ajax['method'].")]);
-    $expected = new InsertCommand('#insert_div', 'insert replacement text');
-    $this->assertCommand($commands, $expected->render(), "'insert' AJAX command issued with correct data.");
-
-    // Tests the 'invoke' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX invoke command: Invoke addClass() method.")]);
-    $expected = new InvokeCommand('#invoke_div', 'addClass', ['error']);
-    $this->assertCommand($commands, $expected->render(), "'invoke' AJAX command issued with correct method and argument.");
-
-    // Tests the 'prepend' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'prepend': Click to prepend something")]);
-    $expected = new PrependCommand('#prepend_div', 'prepended text');
-    $this->assertCommand($commands, $expected->render(), "'prepend' AJAX command issued with correct data.");
-
-    // Tests the 'remove' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'remove': Click to remove text")]);
-    $expected = new RemoveCommand('#remove_text');
-    $this->assertCommand($commands, $expected->render(), "'remove' AJAX command issued with correct command and selector.");
-
-    // Tests the 'restripe' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'restripe' command")]);
-    $expected = new RestripeCommand('#restripe_table');
-    $this->assertCommand($commands, $expected->render(), "'restripe' AJAX command issued with correct selector.");
-
-    // Tests the 'settings' command.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, ['op' => t("AJAX 'settings' command")]);
-    $expected = new SettingsCommand(['ajax_forms_test' => ['foo' => 42]]);
-    $this->assertCommand($commands, $expected->render(), "'settings' AJAX command issued with correct data.");
-  }
-
-  /**
-   * Regression test: Settings command exists regardless of JS aggregation.
-   */
-  public function testAttachedSettings() {
-    $assert = function ($message) {
-      $response = new AjaxResponse();
-      $response->setAttachments([
-        'library' => ['core/drupalSettings'],
-        'drupalSettings' => ['foo' => 'bar'],
-      ]);
-
-      $ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
-      $subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor);
-      $event = new FilterResponseEvent(
-        \Drupal::service('http_kernel'),
-        new Request(),
-        HttpKernelInterface::MASTER_REQUEST,
-        $response
-      );
-      $subscriber->onResponse($event);
-      $expected = [
-        'command' => 'settings',
-      ];
-      $this->assertCommand($response->getCommands(), $expected, $message);
-    };
-
-    $config = $this->config('system.performance');
-
-    $config->set('js.preprocess', FALSE)->save();
-    $assert('Settings command exists when JS aggregation is disabled.');
-
-    $config->set('js.preprocess', TRUE)->save();
-    $assert('Settings command exists when JS aggregation is enabled.');
-  }
-
-}
diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
index a93372d0..60e37411 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
+++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
@@ -30,7 +30,7 @@ function ajax_forms_test_advanced_commands_after_callback($form, FormStateInterf
   $selector = '#after_div';
 
   $response = new AjaxResponse();
-  $response->addCommand(new AfterCommand($selector, "This will be placed after"));
+  $response->addCommand(new AfterCommand($selector, "<div>This will be placed after</div>"));
   return $response;
 }
 
@@ -59,7 +59,7 @@ function ajax_forms_test_advanced_commands_append_callback($form, FormStateInter
 function ajax_forms_test_advanced_commands_before_callback($form, FormStateInterface $form_state) {
   $selector = '#before_div';
   $response = new AjaxResponse();
-  $response->addCommand(new BeforeCommand($selector, "Before text"));
+  $response->addCommand(new BeforeCommand($selector, "<div>Before text</div>"));
   return $response;
 }
 
diff --git a/core/modules/tour/src/Tests/TourTest.php b/core/modules/tour/src/Tests/TourTest.php
new file mode 100644
index 00000000..05d3ae61
--- /dev/null
+++ b/core/modules/tour/src/Tests/TourTest.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\tour\Tests;
+
+/**
+ * A legacy test for \Drupal\tour\Tests\TourTestBase.
+ *
+ * @group tour
+ */
+class TourTest extends TourTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['block', 'tour', 'locale', 'language', 'tour_test'];
+
+  /**
+   * Tour tip attributes to be tested. Keyed by the path.
+   *
+   * @var array
+   *   An array of tip attributes, keyed by path.
+   */
+  protected $tips = [
+    'tour-test-1' => [
+      'data-id' => 'tour-test-1',
+      'data-class' => 'tour-test-1',
+    ],
+  ];
+
+  /**
+   * An admin user with administrative permissions for tour.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * The permissions required for a logged in user to test tour tips.
+   *
+   * @var array
+   *   A list of permissions.
+   */
+  protected $permissions = ['access tour'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Make sure we are using distinct default and administrative themes for
+    // the duration of these tests.
+    $this->container->get('theme_handler')->install(['bartik', 'seven']);
+    $this->config('system.theme')
+      ->set('default', 'bartik')
+      ->set('admin', 'seven')
+      ->save();
+
+    $this->permissions[] = 'view the administration theme';
+
+    // Create an admin user to view tour tips.
+    $this->adminUser = $this->drupalCreateUser($this->permissions);
+    $this->drupalLogin($this->adminUser);
+
+    $this->drupalPlaceBlock('local_actions_block', [
+      'theme' => 'seven',
+      'region' => 'content',
+    ]);
+  }
+
+  /**
+   * A simple tip test.
+   */
+  public function testTips() {
+    foreach ($this->tips as $path => $attributes) {
+      $this->drupalGet($path);
+      $this->assertTourTips($attributes);
+    }
+  }
+
+}
diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv b/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv
index 196f32a0..64116e85 100644
--- a/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv
+++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv
@@ -3,5 +3,5 @@ Give it a go and grow your own herbs,give-it-a-go-and-grow-your-own-herbs.html,H
 The real deal for supermarket savvy shopping,the-real-deal-for-supermarket-savvy-shopping.html,Megan Collins Quinlan,articles/the-real-deal-for-supermarket-savvy-shopping,supermarket-savvy-umami.jpg,Products presented on supermarket shelving.,"Supermarkets,Shopping"
 The umami guide to our favorite mushrooms,the-umami-guide-to-our-favourite-mushrooms.html,Umami,articles/the-umami-guide-to-our-favourite-mushrooms,mushrooms-umami.jpg,A delightful selection of mushroom varieties laid out on a simple wooden plate.,"Mushrooms,Vegetarian"
 Let's hear it for carrots,lets-hear-it-for-carrots.html,Umami,articles/lets-hear-it-for-carrots,heritage-carrots.jpg,"Purple, orange, yellow and white heritage carrots.","Carrots,Vegetarian,Healthy"
-Baking mishaps - our troubleshooting tips,baking-mishaps-our-troubleshooting-tips.html,"Umami",articles/baking-mishaps-our-troubleshooting-tips,chocolate-brownie-umami.jpg,"A delicious chocolate brownie","Baking,Learn to cook"
+Baking mishaps - our troubleshooting tips,baking-mishaps-our-troubleshooting-tips.html,"Umami",articles/baking-mishaps-our-troubleshooting-tips,chocolate-brownie-umami.jpg,"Alt text to be supplied","Baking,Learn to cook"
 Skip the spirits with delicious mocktails,skip-the-spirits-with-delicious-mocktails.html,Megan Collins,articles/skip-the-spirits-with-delicious-mocktails,mojito-mocktail.jpg,"Fresh mojito mocktail with garnish of mint leaves, ice, and sliced lime","Alcohol free,Drinks,Party,Cocktail party,Dinner party"
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
new file mode 100644
index 00000000..247f5cad
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/CommandsTest.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Drupal\FunctionalJavascriptTests\Ajax;
+
+use Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Performs tests on AJAX framework commands.
+ *
+ * @group Ajax
+ */
+class CommandsTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'ajax_test', 'ajax_forms_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $minkDefaultDriverClass = DrupalSelenium2Driver::class;
+
+  /**
+   * Tests the various Ajax Commands.
+   */
+  public function testAjaxCommands() {
+    $session = $this->getSession();
+    $page = $this->getSession()->getPage();
+
+    $form_path = 'ajax_forms_test_ajax_commands_form';
+    $web_user = $this->drupalCreateUser(['access content']);
+    $this->drupalLogin($web_user);
+    $this->drupalGet($form_path);
+
+    // Tests the 'add_css' command.
+    $page->pressButton("AJAX 'add_css' command");
+    $this->assertWaitPageContains('my/file.css');
+
+    // Tests the 'after' command.
+    $page->pressButton("AJAX 'After': Click to put something after the div");
+    $this->assertTrue($page->waitFor(10, function () use ($page) {
+      $element = $page->find('css', '#after_div + div');
+      return $element && $element->getHtml() === 'This will be placed after';
+    }));
+
+    // Tests the 'alert' command.
+    $test_alert_command = <<<JS
+window.alert = function() {
+  document.body.innerHTML += '<div class="alert-command">Alert</div>';
+};
+JS;
+    $session->executeScript($test_alert_command);
+    $page->pressButton("AJAX 'Alert': Click to alert");
+    $this->assertWaitPageContains('<div class="alert-command">Alert</div>');
+
+    // Tests the 'append' command.
+    $page->pressButton("AJAX 'Append': Click to append something");
+    $this->assertWaitPageContains('<div id="append_div">Append inside this divAppended text</div>');
+
+    // Tests the 'before' command.
+    $page->pressButton("AJAX 'before': Click to put something before the div");
+    $this->assertTrue($page->waitFor(10, function () use ($page) {
+      return $page->find('xpath', '//div[text() = "Before text"]/following-sibling::div[@id = "before_div"]');
+    }));
+
+    // Tests the 'changed' command.
+    $page->pressButton("AJAX changed: Click to mark div changed.");
+    $this->assertWaitPageContains('<div id="changed_div" class="ajax-changed">');
+
+    // Tests the 'changed' command using the second argument.
+    // Refresh page for testing 'changed' command to same element again.
+    $this->drupalGet($form_path);
+    $page->pressButton("AJAX changed: Click to mark div changed with asterisk.");
+    $this->assertWaitPageContains('<div id="changed_div" class="ajax-changed"> <div id="changed_div_mark_this">This div can be marked as changed or not. <abbr class="ajax-changed" title="Changed">*</abbr> </div></div>');
+
+    // Tests the 'css' command.
+    $page->pressButton("Set the '#box' div to be blue.");
+    $this->assertWaitPageContains('<div id="css_div" style="background-color: blue;">');
+
+    // Tests the 'data' command.
+    $page->pressButton("AJAX data command: Issue command.");
+    $this->assertTrue($page->waitFor(10, function () use ($session) {
+      return 'testvalue' === $session->evaluateScript('window.jQuery("#data_div").data("testkey")');
+    }));
+
+    // Tests the 'html' command.
+    $page->pressButton("AJAX html: Replace the HTML in a selector.");
+    $this->assertWaitPageContains('<div id="html_div">replacement text</div>');
+
+    // Tests the 'insert' command.
+    $page->pressButton("AJAX insert: Let client insert based on #ajax['method'].");
+    $this->assertWaitPageContains('<div id="insert_div">insert replacement textOriginal contents</div>');
+
+    // Tests the 'invoke' command.
+    $page->pressButton("AJAX invoke command: Invoke addClass() method.");
+    $this->assertWaitPageContains('<div id="invoke_div" class="error">Original contents</div>');
+
+    // Tests the 'prepend' command.
+    $page->pressButton("AJAX 'prepend': Click to prepend something");
+    $this->assertWaitPageContains('<div id="prepend_div">prepended textSomething will be prepended to this div. </div>');
+
+    // Tests the 'remove' command.
+    $page->pressButton("AJAX 'remove': Click to remove text");
+    $this->assertWaitPageContains('<div id="remove_div"></div>');
+
+    // Tests the 'restripe' command.
+    $page->pressButton("AJAX 'restripe' command");
+    $this->assertWaitPageContains('<tr id="table-first" class="odd"><td>first row</td></tr>');
+    $this->assertWaitPageContains('<tr class="even"><td>second row</td></tr>');
+
+    // Tests the 'settings' command.
+    $test_settings_command = <<<JS
+Drupal.behaviors.testSettingsCommand = {
+  attach: function (context, settings) {
+    window.jQuery('body').append('<div class="test-settings-command">' + settings.ajax_forms_test.foo + '</div>');
+  }
+};
+JS;
+    $session->executeScript($test_settings_command);
+    // @todo: Replace after https://www.drupal.org/project/drupal/issues/2616184
+    $session->executeScript('window.jQuery("#edit-settings-command-example").mousedown();');
+    $this->assertWaitPageContains('<div class="test-settings-command">42</div>');
+  }
+
+  /**
+   * Asserts that page contains a text after waiting.
+   *
+   * @param string $text
+   *   A needle text.
+   */
+  protected function assertWaitPageContains($text) {
+    $page = $this->getSession()->getPage();
+    $page->waitFor(10, function () use ($page, $text) {
+      return stripos($page->getContent(), $text) !== FALSE;
+    });
+    $this->assertContains($text, $page->getContent());
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Ajax/CommandsTest.php b/core/tests/Drupal/KernelTests/Core/Ajax/CommandsTest.php
new file mode 100644
index 00000000..a6bbe0b2
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Ajax/CommandsTest.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Ajax;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
+use Drupal\KernelTests\KernelTestBase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * Performs tests on AJAX framework commands.
+ *
+ * @group Ajax
+ */
+class CommandsTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system', 'node', 'ajax_test', 'ajax_forms_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installSchema('system', ['performance']);
+  }
+
+  /**
+   * Regression test: Settings command exists regardless of JS aggregation.
+   */
+  public function testAttachedSettings() {
+    $assert = function ($message) {
+      $response = new AjaxResponse();
+      $response->setAttachments([
+        'library' => ['core/drupalSettings'],
+        'drupalSettings' => ['foo' => 'bar'],
+      ]);
+
+      $ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
+      $subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor);
+      $event = new FilterResponseEvent(
+        \Drupal::service('http_kernel'),
+        new Request(),
+        HttpKernelInterface::MASTER_REQUEST,
+        $response
+      );
+      $subscriber->onResponse($event);
+      $expected = [
+        'command' => 'settings',
+      ];
+      $this->assertCommand($response->getCommands(), $expected, $message);
+    };
+
+    $config = $this->config('system.performance');
+
+    $config->set('js.preprocess', FALSE)->save();
+    $assert('Settings command exists when JS aggregation is disabled.');
+
+    $config->set('js.preprocess', TRUE)->save();
+    $assert('Settings command exists when JS aggregation is enabled.');
+  }
+
+  /**
+   * 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);
+  }
+
+}
