diff --git a/core/modules/system/src/Tests/Batch/PageTest.php b/core/modules/system/tests/src/Functional/Batch/PageTest.php
similarity index 95%
rename from core/modules/system/src/Tests/Batch/PageTest.php
rename to core/modules/system/tests/src/Functional/Batch/PageTest.php
index dbdf18c3d2..9b1d4dd4ad 100644
--- a/core/modules/system/src/Tests/Batch/PageTest.php
+++ b/core/modules/system/tests/src/Functional/Batch/PageTest.php
@@ -1,15 +1,15 @@
 <?php
 
-namespace Drupal\system\Tests\Batch;
+namespace Drupal\Tests\system\Functional\Batch;
 
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests the content of the progress page.
  *
  * @group Batch
  */
-class PageTest extends WebTestBase {
+class PageTest extends BrowserTestBase {
 
   /**
    * Modules to enable.
diff --git a/core/modules/system/src/Tests/Batch/ProcessingTest.php b/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php
similarity index 98%
rename from core/modules/system/src/Tests/Batch/ProcessingTest.php
rename to core/modules/system/tests/src/Functional/Batch/ProcessingTest.php
index 6b90cc804c..2d7c7ae01c 100644
--- a/core/modules/system/src/Tests/Batch/ProcessingTest.php
+++ b/core/modules/system/tests/src/Functional/Batch/ProcessingTest.php
@@ -1,16 +1,16 @@
 <?php
 
-namespace Drupal\system\Tests\Batch;
+namespace Drupal\Tests\system\Functional\Batch;
 
 use Drupal\Core\Url;
-use Drupal\simpletest\WebTestBase;
+use Drupal\Tests\BrowserTestBase;
 
 /**
  * Tests batch processing in form and non-form workflow.
  *
  * @group Batch
  */
-class ProcessingTest extends WebTestBase {
+class ProcessingTest extends BrowserTestBase {
 
   /**
    * Modules to enable.
diff --git a/core/modules/user/src/Tests/UserCancelTest.php b/core/modules/user/tests/src/Functional/UserCancelTest.php
similarity index 99%
rename from core/modules/user/src/Tests/UserCancelTest.php
rename to core/modules/user/tests/src/Functional/UserCancelTest.php
index a74626e16e..83020b431a 100644
--- a/core/modules/user/src/Tests/UserCancelTest.php
+++ b/core/modules/user/tests/src/Functional/UserCancelTest.php
@@ -1,11 +1,11 @@
 <?php
 
-namespace Drupal\user\Tests;
+namespace Drupal\Tests\user\Functional;
 
-use Drupal\comment\Tests\CommentTestTrait;
-use Drupal\simpletest\WebTestBase;
 use Drupal\comment\CommentInterface;
 use Drupal\comment\Entity\Comment;
+use Drupal\comment\Tests\CommentTestTrait;
+use Drupal\Tests\BrowserTestBase;
 use Drupal\user\Entity\User;
 
 /**
@@ -13,7 +13,7 @@
  *
  * @group user
  */
-class UserCancelTest extends WebTestBase {
+class UserCancelTest extends BrowserTestBase {
 
   use CommentTestTrait;
 
@@ -577,7 +577,7 @@ public function testMassUserCancelByAdmin() {
     $this->drupalPostForm(NULL, NULL, t('Cancel accounts'));
     $status = TRUE;
     foreach ($users as $account) {
-      $status = $status && (strpos($this->content, $account->getUsername() . '</em> has been deleted.') !== FALSE);
+      $status = $status && (strpos($this->getTextContent(), $account->getUsername() . ' has been deleted.') !== FALSE);
       $user_storage->resetCache([$account->id()]);
       $status = $status && !$user_storage->load($account->id());
     }
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 5751368f61..a95beb0366 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -23,15 +23,16 @@
 use Drupal\Core\Utility\Error;
 use Drupal\FunctionalTests\AssertLegacyTrait;
 use Drupal\simpletest\AssertHelperTrait;
-use Drupal\simpletest\ContentTypeCreationTrait;
 use Drupal\simpletest\BlockCreationTrait;
+use Drupal\simpletest\ContentTypeCreationTrait;
 use Drupal\simpletest\NodeCreationTrait;
 use Drupal\simpletest\UserCreationTrait;
+use Drupal\Tests\Traits\Core\MetaRefreshTrait;
 use PHPUnit\Framework\TestCase;
-use Symfony\Component\CssSelector\CssSelectorConverter;
-use Symfony\Component\HttpFoundation\Request;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\ResponseInterface;
+use Symfony\Component\CssSelector\CssSelectorConverter;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Provides a test case for functional Drupal tests.
@@ -248,6 +249,20 @@
   protected $originalShutdownCallbacks = [];
 
   /**
+   * 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;
+
+  /**
    * Initializes Mink sessions.
    */
   protected function initMink() {
@@ -638,6 +653,13 @@ protected function drupalGet($path, array $options = [], array $headers = []) {
     // Ensure that any changes to variables in the other thread are picked up.
     $this->refreshVariables();
 
+    // 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;
+    }
+
     // Log only for JavascriptTestBase tests because for Goutte we log with
     // ::getResponseLogHandler.
     if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
@@ -811,6 +833,12 @@ protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
     // Ensure that any changes to variables in the other thread are picked up.
     $this->refreshVariables();
 
+    // Check if there are any meta refresh redirects (like Batch API pages).
+    if ($this->checkForMetaRefresh()) {
+      // We are finished with all meta refresh redirects, so reset the counter.
+      $this->metaRefreshCount = 0;
+    }
+
     // Log only for JavascriptTestBase tests because for Goutte we log with
     // ::getResponseLogHandler.
     if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
@@ -1370,4 +1398,26 @@ protected function getTestMethodCaller() {
     return $caller;
   }
 
+  /**
+   * Checks for meta refresh tag and if found call drupalGet() recursively.
+   *
+   * This function looks for the http-equiv attribute to be set to "Refresh" and
+   * is case-sensitive.
+   *
+   * @return string|false
+   *   Either the new page content or FALSE.
+   */
+  protected function checkForMetaRefresh() {
+    $refresh = $this->cssSelect('meta[http-equiv="Refresh"]');
+    if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) {
+      // 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]->getAttribute('content'), $match)) {
+        $this->metaRefreshCount++;
+        return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url'])));
+      }
+    }
+    return FALSE;
+  }
+
 }
