diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index cc70557..0958c3a 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -170,7 +170,7 @@ function _batch_progress_page() {
   $build = array(
     '#theme' => 'progress_bar',
     '#percent' => $percentage,
-    '#message' => $message,
+    '#message' => array('#markup' => $message),
     '#label' => $label,
     '#attached' => array(
       'html_head' => array(
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 30aa69e..7cc75ee 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -166,6 +166,21 @@
    */
   protected $redirectCount;
 
+
+  /**
+   * The number of meta refresh redirects to follow, or NULL if unlimited.
+   *
+   * @var null|int
+   */
+  protected $maximumMetaRefreshCount = NULL;
+
+  /**
+   * The number of meta refresh redirects followed during ::drupalGet().
+   *
+   * @var int
+   */
+  protected $metaRefreshCount = 0;
+
   /**
    * The kernel used in this test.
    *
@@ -1495,6 +1510,8 @@ protected function drupalGet($path, array $options = array(), array $headers = a
     // Replace original page output with new output from redirected page(s).
     if ($new = $this->checkForMetaRefresh()) {
       $out = $new;
+      // We are finished with all meta refresh redirects, so reset the counter.
+      $this->metaRefreshCount = 0;
     }
 
     if ($path instanceof Url) {
@@ -2153,12 +2170,13 @@ protected function cronRun() {
    *   Either the new page content or FALSE.
    */
   protected function checkForMetaRefresh() {
-    if (strpos($this->getRawContent(), '<meta ') && $this->parse()) {
+    if (strpos($this->getRawContent(), '<meta ') && $this->parse() && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) {
       $refresh = $this->xpath('//meta[@http-equiv="Refresh"]');
       if (!empty($refresh)) {
         // Parse the content attribute of the meta tag for the format:
         // "[delay]: URL=[page_to_redirect_to]".
         if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]['content'], $match)) {
+          $this->metaRefreshCount++;
           return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url'])));
         }
       }
diff --git a/core/modules/system/src/Tests/Batch/PageTest.php b/core/modules/system/src/Tests/Batch/PageTest.php
index 3d9adfc..7b3243c 100644
--- a/core/modules/system/src/Tests/Batch/PageTest.php
+++ b/core/modules/system/src/Tests/Batch/PageTest.php
@@ -61,4 +61,20 @@ function testBatchProgressPageTitle() {
     $this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
   }
 
+  /**
+   * Tests that the progress messages are correct.
+   */
+  function testBatchProgressMessages() {
+    // Go to the initial step only.
+    $this->maximumMetaRefreshCount = 0;
+    $this->drupalGet('batch-test/test-title');
+    $this->assertRaw('<div class="progress__description">Initializing.<br />&nbsp;</div>', 'Initial progress message appears correctly.');
+    $this->assertNoRaw('&amp;nbsp;', 'Initial progress message is not double escaped.');
+    // Now also go to the next step.
+    $this->maximumMetaRefreshCount = 1;
+    $this->drupalGet('batch-test/test-title');
+    $this->assertRaw('<div class="progress__description">Completed 1 of 1.</div>', 'Progress message for second step appears correctly.');
+    $this->maximumMetaRefreshCount = NULL;
+  }
+
 }
