diff --git a/core/lib/Drupal/Core/Test/RunTests/RunTestsResultsBuilder.php b/core/lib/Drupal/Core/Test/RunTests/RunTestsResultsBuilder.php
new file mode 100644
index 0000000000..b5685edae5
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/RunTests/RunTestsResultsBuilder.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace Drupal\Core\Test\RunTests;
+
+use Drupal\Core\Test\TestDiscovery;
+
+/**
+ * Build the results part of a test result report.
+ *
+ * @internal
+ */
+class RunTestsResultsBuilder {
+
+  /**
+   * The inline Twig template for a results summary.
+   *
+   * @var string
+   */
+  protected static $summaryTemplate = <<<'TWIG'
+<div class="simpletest-{{ fail + exception == 0 ? 'pass' : 'fail' }}">
+  {{ label }} {{ items|join(', ') }}
+</div>
+TWIG;
+
+  /**
+   * Builds the status image map.
+   *
+   * @return string[]
+   *   Render array of icons for status displays.
+   */
+  public static function buildStatusImageMap() {
+    $image_pass = [
+      '#theme' => 'image',
+      '#uri' => 'core/misc/icons/73b355/check.svg',
+      '#width' => 18,
+      '#height' => 18,
+      '#alt' => 'Pass',
+    ];
+    $image_fail = [
+      '#theme' => 'image',
+      '#uri' => 'core/misc/icons/e32700/error.svg',
+      '#width' => 18,
+      '#height' => 18,
+      '#alt' => 'Fail',
+    ];
+    $image_exception = [
+      '#theme' => 'image',
+      '#uri' => 'core/misc/icons/e29700/warning.svg',
+      '#width' => 18,
+      '#height' => 18,
+      '#alt' => 'Exception',
+    ];
+    $image_debug = [
+      '#theme' => 'image',
+      '#uri' => 'core/misc/icons/e29700/warning.svg',
+      '#width' => 18,
+      '#height' => 18,
+      '#alt' => 'Debug',
+    ];
+    return [
+      'pass' => $image_pass,
+      'fail' => $image_fail,
+      'exception' => $image_exception,
+      'debug' => $image_debug,
+    ];
+  }
+
+  /**
+   * Adds the result form to a $form.
+   *
+   * This is a static method so that run-tests.sh can use it to generate a
+   * results page completely external to Drupal. This is why the UI strings are
+   * not wrapped in t().
+   *
+   * @param array $form
+   *   The form to attach the results to.
+   * @param array $results
+   *   The simpletest results.
+   *
+   * @return array
+   *   A list of tests the passed and failed. The array has two keys, 'pass' and
+   *   'fail'. Each contains a list of test classes.
+   *
+   * @see simpletest_script_open_browser()
+   * @see run-tests.sh
+   * @see \Drupal\simpletest\Form\SimpletestResultsForm
+   */
+  public static function addResultForm(array &$form, array $results) {
+    // Transform the test results to be grouped by test class.
+    $test_results = [];
+    foreach ($results as $result) {
+      if (!isset($test_results[$result->test_class])) {
+        $test_results[$result->test_class] = [];
+      }
+      $test_results[$result->test_class][] = $result;
+    }
+
+    $image_status_map = static::buildStatusImageMap();
+
+    // Keep track of which test cases passed or failed.
+    $filter = [
+      'pass' => [],
+      'fail' => [],
+    ];
+
+    // Summary result widget.
+    $form['result'] = [
+      '#type' => 'fieldset',
+      '#title' => 'Results',
+      // Because this is used in a theme-less situation need to provide a
+      // default.
+      '#attributes' => [],
+    ];
+
+    // Add summary line.
+    $form['result']['summary'] = $summary = [
+      '#type' => 'inline_template',
+      '#template' => static::$summaryTemplate,
+      // Fill in the context for the template after we've tallied results in
+      // #pass, #fail, etc.
+      '#context' => [],
+      '#pass' => 0,
+      '#fail' => 0,
+      '#exception' => 0,
+      '#debug' => 0,
+    ];
+
+    \Drupal::service('test_discovery')->registerTestNamespaces();
+
+    // Cycle through each test group.
+    $header = [
+      'Message',
+      'Group',
+      'Filename',
+      'Line',
+      'Function',
+      ['colspan' => 2, 'data' => 'Status'],
+    ];
+    $form['result']['results'] = [];
+    foreach ($test_results as $group => $assertions) {
+      // Create group details with summary information.
+      $info = TestDiscovery::getTestInfo($group);
+      $form['result']['results'][$group] = [
+        '#type' => 'details',
+        '#title' => $info['name'],
+        '#open' => TRUE,
+        '#description' => $info['description'],
+      ];
+      $form['result']['results'][$group]['summary'] = $summary;
+      $group_summary = & $form['result']['results'][$group]['summary'];
+
+      // Create table of assertions for the group.
+      $rows = [];
+      foreach ($assertions as $assertion) {
+        $row = [];
+        $row[] = ['data' => ['#markup' => $assertion->message]];
+        $row[] = $assertion->message_group;
+        $row[] = \Drupal::service('file_system')->basename(($assertion->file));
+        $row[] = $assertion->line;
+        $row[] = $assertion->function;
+        $row[] = ['data' => $image_status_map[$assertion->status]];
+
+        $class = 'simpletest-' . $assertion->status;
+        if ($assertion->message_group == 'Debug') {
+          $class = 'simpletest-debug';
+        }
+        $rows[] = ['data' => $row, 'class' => [$class]];
+
+        $group_summary['#' . $assertion->status]++;
+        $form['result']['summary']['#' . $assertion->status]++;
+      }
+      $form['result']['results'][$group]['table'] = [
+        '#type' => 'table',
+        '#header' => $header,
+        '#rows' => $rows,
+      ];
+
+      $group_summary['#context'] = static::buildSummaryContext(
+        $group_summary['#pass'],
+        $group_summary['#fail'],
+        $group_summary['#exception'],
+        $group_summary['#debug']
+      );
+
+      // Set summary information.
+      $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0;
+      $form['result']['results'][$group]['#open'] = !$group_summary['#ok'];
+
+      // Store test group (class) as for use in filter.
+      $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group;
+    }
+
+    // Now that we've tallied all the results, we can compute the context for
+    // the results summary template.
+
+    $form['result']['summary']['#context'] = static::buildSummaryContext(
+      $form['result']['summary']['#pass'],
+      $form['result']['summary']['#fail'],
+      $form['result']['summary']['#exception'],
+      $form['result']['summary']['#debug']
+    );
+
+    // Overall summary status.
+    $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0;
+
+    return $filter;
+  }
+
+  /**
+   * Assemble the context for the twig template in static::$summaryTemplate.
+   *
+   * @param int $pass
+   *   The count of passing tests.
+   * @param int $fail
+   *   The count of failing tests.
+   * @param int $exception
+   *   The count of tests that threw an exception.
+   * @param int $debug
+   *   The count of debug messages present.
+   * @param string $label
+   *   An optional label for the summary line.
+   *
+   * @return string[]
+   *   The context for the twig template in static::$summaryTemplate.
+   */
+  protected static function buildSummaryContext($pass, $fail, $exception, $debug, $label = NULL) {
+    $context = [];
+    $translation = \Drupal::translation();
+
+    $context['label'] = $label;
+    $context['pass'] = $pass;
+    $context['fail'] = $fail;
+    $context['exception'] = $exception;
+    $context['debug'] = $debug;
+
+    $context['items']['pass'] = $translation->formatPlural($pass, '1 pass', '@count passes');
+    $context['items']['fail'] = $translation->formatPlural($fail, '1 fail', '@count fails');
+    $context['items']['exception'] = $translation->formatPlural($exception, '1 exception', '@count exceptions');
+    if ($debug) {
+      $context['items']['debug'] = $translation->formatPlural($debug, '1 debug message', '@count debug messages');
+    }
+    return $context;
+  }
+
+}
diff --git a/core/modules/simpletest/simpletest.libraries.yml b/core/modules/simpletest/simpletest.libraries.yml
index 7bcbc07de5..b84366e2d5 100644
--- a/core/modules/simpletest/simpletest.libraries.yml
+++ b/core/modules/simpletest/simpletest.libraries.yml
@@ -2,9 +2,6 @@ drupal.simpletest:
   version: VERSION
   js:
     simpletest.js: {}
-  css:
-    component:
-      css/simpletest.module.css: {}
   dependencies:
     - core/jquery
     - core/drupal
@@ -12,3 +9,4 @@ drupal.simpletest:
     - core/jquery.once
     - core/drupal.tableselect
     - core/drupal.debounce
+    - system/runtests
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index 5775c1d83d..3c6f0feeb6 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -41,6 +41,10 @@ function simpletest_help($route_name, RouteMatchInterface $route_match) {
 
 /**
  * Implements hook_theme().
+ *
+ * @todo Replace this theming system with
+ *   Drupal\Core\Test\RunTests\RunTestsResultsBuilder in
+ *   https://www.drupal.org/project/drupal/issues/3075490
  */
 function simpletest_theme() {
   return [
@@ -100,20 +104,6 @@ function _simpletest_build_summary_line($summary) {
   return $items;
 }
 
-/**
- * Formats test result summaries into a comma separated string for run-tests.sh.
- *
- * @param array $summary
- *   A summary of the test results.
- *
- * @return string
- *   A concatenated string of the formatted test results.
- */
-function _simpletest_format_summary_line($summary) {
-  $parts = _simpletest_build_summary_line($summary);
-  return implode(', ', $parts);
-}
-
 /**
  * Runs tests.
  *
diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
index adc1d4a5bb..5de37fcbbe 100644
--- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
@@ -7,8 +7,8 @@
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Test\EnvironmentCleanerInterface;
+use Drupal\Core\Test\RunTests\RunTestsResultsBuilder;
 use Drupal\Core\Url;
-use Drupal\simpletest\TestDiscovery;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -70,40 +70,7 @@ public function __construct(Connection $database, EnvironmentCleanerInterface $c
    * Builds the status image map.
    */
   protected static function buildStatusImageMap() {
-    $image_pass = [
-      '#theme' => 'image',
-      '#uri' => 'core/misc/icons/73b355/check.svg',
-      '#width' => 18,
-      '#height' => 18,
-      '#alt' => 'Pass',
-    ];
-    $image_fail = [
-      '#theme' => 'image',
-      '#uri' => 'core/misc/icons/e32700/error.svg',
-      '#width' => 18,
-      '#height' => 18,
-      '#alt' => 'Fail',
-    ];
-    $image_exception = [
-      '#theme' => 'image',
-      '#uri' => 'core/misc/icons/e29700/warning.svg',
-      '#width' => 18,
-      '#height' => 18,
-      '#alt' => 'Exception',
-    ];
-    $image_debug = [
-      '#theme' => 'image',
-      '#uri' => 'core/misc/icons/e29700/warning.svg',
-      '#width' => 18,
-      '#height' => 18,
-      '#alt' => 'Debug',
-    ];
-    return [
-      'pass' => $image_pass,
-      'fail' => $image_fail,
-      'exception' => $image_exception,
-      'debug' => $image_debug,
-    ];
+    return RunTestsResultsBuilder::buildStatusImageMap();
   }
 
   /**
@@ -257,101 +224,7 @@ protected function getResults($test_id) {
    * @see run-tests.sh
    */
   public static function addResultForm(array &$form, array $results) {
-    // Transform the test results to be grouped by test class.
-    $test_results = [];
-    foreach ($results as $result) {
-      if (!isset($test_results[$result->test_class])) {
-        $test_results[$result->test_class] = [];
-      }
-      $test_results[$result->test_class][] = $result;
-    }
-
-    $image_status_map = static::buildStatusImageMap();
-
-    // Keep track of which test cases passed or failed.
-    $filter = [
-      'pass' => [],
-      'fail' => [],
-    ];
-
-    // Summary result widget.
-    $form['result'] = [
-      '#type' => 'fieldset',
-      '#title' => 'Results',
-      // Because this is used in a theme-less situation need to provide a
-      // default.
-      '#attributes' => [],
-    ];
-    $form['result']['summary'] = $summary = [
-      '#theme' => 'simpletest_result_summary',
-      '#pass' => 0,
-      '#fail' => 0,
-      '#exception' => 0,
-      '#debug' => 0,
-    ];
-
-    \Drupal::service('test_discovery')->registerTestNamespaces();
-
-    // Cycle through each test group.
-    $header = [
-      'Message',
-      'Group',
-      'Filename',
-      'Line',
-      'Function',
-      ['colspan' => 2, 'data' => 'Status'],
-    ];
-    $form['result']['results'] = [];
-    foreach ($test_results as $group => $assertions) {
-      // Create group details with summary information.
-      $info = TestDiscovery::getTestInfo($group);
-      $form['result']['results'][$group] = [
-        '#type' => 'details',
-        '#title' => $info['name'],
-        '#open' => TRUE,
-        '#description' => $info['description'],
-      ];
-      $form['result']['results'][$group]['summary'] = $summary;
-      $group_summary =& $form['result']['results'][$group]['summary'];
-
-      // Create table of assertions for the group.
-      $rows = [];
-      foreach ($assertions as $assertion) {
-        $row = [];
-        $row[] = ['data' => ['#markup' => $assertion->message]];
-        $row[] = $assertion->message_group;
-        $row[] = \Drupal::service('file_system')->basename(($assertion->file));
-        $row[] = $assertion->line;
-        $row[] = $assertion->function;
-        $row[] = ['data' => $image_status_map[$assertion->status]];
-
-        $class = 'simpletest-' . $assertion->status;
-        if ($assertion->message_group == 'Debug') {
-          $class = 'simpletest-debug';
-        }
-        $rows[] = ['data' => $row, 'class' => [$class]];
-
-        $group_summary['#' . $assertion->status]++;
-        $form['result']['summary']['#' . $assertion->status]++;
-      }
-      $form['result']['results'][$group]['table'] = [
-        '#type' => 'table',
-        '#header' => $header,
-        '#rows' => $rows,
-      ];
-
-      // Set summary information.
-      $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0;
-      $form['result']['results'][$group]['#open'] = !$group_summary['#ok'];
-
-      // Store test group (class) as for use in filter.
-      $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group;
-    }
-
-    // Overall summary status.
-    $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0;
-
-    return $filter;
+    return RunTestsResultsBuilder::addResultForm($form, $results);
   }
 
 }
diff --git a/core/modules/simpletest/css/simpletest.module.css b/core/modules/system/css/system.runtests.css
similarity index 100%
rename from core/modules/simpletest/css/simpletest.module.css
rename to core/modules/system/css/system.runtests.css
diff --git a/core/modules/system/system.libraries.yml b/core/modules/system/system.libraries.yml
index 98eb283919..d47b4fae9f 100644
--- a/core/modules/system/system.libraries.yml
+++ b/core/modules/system/system.libraries.yml
@@ -43,6 +43,13 @@ maintenance:
     - system/base
     - system/admin
 
+# Styling for the run-tests.sh --browser option.
+runtests:
+  version: VERSION
+  css:
+    theme:
+      css/system.runtests.css: {}
+
 drupal.system:
   version: VERSION
   js:
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 9d3f3894eb..f7651696e7 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -16,10 +16,10 @@
 use Drupal\Core\StreamWrapper\PublicStream;
 use Drupal\Core\Test\EnvironmentCleaner;
 use Drupal\Core\Test\PhpUnitTestRunner;
+use Drupal\Core\Test\RunTests\RunTestsResultsBuilder;
 use Drupal\Core\Test\RunTests\TestFileParser;
 use Drupal\Core\Test\TestDatabase;
 use Drupal\Core\Test\TestRunnerKernel;
-use Drupal\simpletest\Form\SimpletestResultsForm;
 use Drupal\Core\Test\TestDiscovery;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\Console\Output\ConsoleOutput;
@@ -319,7 +319,8 @@ function simpletest_script_help() {
 
   --browser   Opens the results in the browser. This enforces --keep-results and
               if you want to also view any pages rendered in the simpletest
-              browser you need to add --verbose to the command line.
+              browser you need to add --verbose to the command line. See:
+              https://www.drupal.org/project/drupal/issues/3084354
 
   --non-html  Removes escaping from output. Useful for reading results on the
               CLI.
@@ -815,6 +816,9 @@ function simpletest_script_run_one_test($test_id, $test_class) {
   global $args;
 
   try {
+    // Default to status = success. This could mean that we didn't discover any
+    // tests and that none ran.
+    $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
     if (strpos($test_class, '::') > 0) {
       list($class_name, $method) = explode('::', $test_class, 2);
       $methods = [$method];
@@ -831,7 +835,10 @@ function simpletest_script_run_one_test($test_id, $test_class) {
     if (is_subclass_of($test_class, TestCase::class)) {
       $status = simpletest_script_run_phpunit($test_id, $test_class);
     }
-    else {
+    // If we aren't running a PHPUnit-based test, then we might have a
+    // Simpletest-based one. Ensure that: 1) The simpletest framework exists,
+    // and 2) That our test belongs to that framework.
+    elseif (class_exists('\Drupal\simpletest\TestBase') && is_subclass_of($test_class, '\Drupal\simpletest\TestBase')) {
       $test->dieOnFail = (bool) $args['die-on-fail'];
       $test->verbose = (bool) $args['verbose'];
       $test->run($methods);
@@ -1515,7 +1522,7 @@ function simpletest_script_open_browser() {
 
   // Get the results form.
   $form = [];
-  SimpletestResultsForm::addResultForm($form, $results);
+  RunTestsResultsBuilder::addResultForm($form, $results);
 
   // Get the assets to make the details element collapsible and theme the result
   // form.
@@ -1523,7 +1530,7 @@ function simpletest_script_open_browser() {
   $assets->setLibraries([
     'core/drupal.collapse',
     'system/admin',
-    'simpletest/drupal.simpletest',
+    'system/runtests',
   ]);
   $resolver = \Drupal::service('asset.resolver');
   list($js_assets_header, $js_assets_footer) = $resolver->getJsAssets($assets, FALSE);
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php
index 795ea33466..41d74cfc67 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php
@@ -44,7 +44,13 @@ class StableLibraryOverrideTest extends KernelTestBase {
    *
    * @var string[]
    */
-  protected $librariesToSkip = [];
+  protected $librariesToSkip = [
+    // The system/runtests library supports the run-tests.sh --verbose option,
+    // which does not use a theme. This library is also a dependency of
+    // drupal.simpletest which will be deprecated.
+    // @see https://www.drupal.org/project/drupal/issues/3057420
+    'system/runtests',
+  ];
 
   /**
    * {@inheritdoc}
