diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
index c8366fb6a5156620560f846f02bc9820201d5aee..9ddc4516f6985a6ed098bfb96b2e1c376f037d51 100644
--- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
@@ -19,6 +19,12 @@
 
 /**
  * Test results form for $test_id.
+ *
+ * Note that the UI strings are not translated because this form is also used
+ * from run-tests.sh.
+ *
+ * @see simpletest_script_open_browser()
+ * @see run-tests.sh
  */
 class SimpletestResultsForm extends FormBase {
 
@@ -58,37 +64,36 @@ public function __construct(Connection $database) {
   /**
    * Builds the status image map.
    */
-  protected function buildStatusImageMap() {
-    // Initialize image mapping property.
+  protected static function buildStatusImageMap() {
     $image_pass = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/73b355/check.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Pass'),
+      '#alt' => 'Pass',
     );
     $image_fail = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/ea2800/error.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Fail'),
+      '#alt' => 'Fail',
     );
     $image_exception = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/e29700/warning.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Exception'),
+      '#alt' => 'Exception',
     );
     $image_debug = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/e29700/warning.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Debug'),
+      '#alt' => 'Debug',
     );
-    $this->statusImageMap = array(
+    return array(
       'pass' => drupal_render($image_pass),
       'fail' => drupal_render($image_fail),
       'exception' => drupal_render($image_exception),
@@ -107,11 +112,9 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state, $test_id = NULL) {
-    $this->buildStatusImageMap();
     // Make sure there are test results to display and a re-run is not being
     // performed.
     $results = array();
-
     if (is_numeric($test_id) && !$results = $this->getResults($test_id)) {
       drupal_set_message($this->t('No test results to display.'), 'error');
       return new RedirectResponse($this->url('simpletest.test_form', array(), array('absolute' => TRUE)));
@@ -119,90 +122,11 @@ public function buildForm(array $form, FormStateInterface $form_state, $test_id
 
     // Load all classes and include CSS.
     $form['#attached']['library'][] = 'simpletest/drupal.simpletest';
-
-    // Keep track of which test cases passed or failed.
-    $filter = array(
-      'pass' => array(),
-      'fail' => array(),
-    );
-
-    // Summary result widget.
-    $form['result'] = array(
-      '#type' => 'fieldset',
-      '#title' => $this->t('Results'),
-    );
-    $form['result']['summary'] = $summary = array(
-      '#theme' => 'simpletest_result_summary',
-      '#pass' => 0,
-      '#fail' => 0,
-      '#exception' => 0,
-      '#debug' => 0,
-    );
-
-    simpletest_classloader_register();
-
-    // Cycle through each test group.
-    $header = array(
-      $this->t('Message'),
-      $this->t('Group'),
-      $this->t('Filename'),
-      $this->t('Line'),
-      $this->t('Function'),
-      array('colspan' => 2, 'data' => $this->t('Status'))
-    );
-    $form['result']['results'] = array();
-    foreach ($results as $group => $assertions) {
-      // Create group details with summary information.
-      $info = TestDiscovery::getTestInfo($group);
-      $form['result']['results'][$group] = array(
-        '#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 = array();
-      foreach ($assertions as $assertion) {
-        $row = array();
-        // Assertion messages are in code, so we assume they are safe.
-        $row[] = SafeMarkup::set($assertion->message);
-        $row[] = $assertion->message_group;
-        $row[] = drupal_basename($assertion->file);
-        $row[] = $assertion->line;
-        $row[] = $assertion->function;
-        $row[] = $this->statusImageMap[$assertion->status];
-
-        $class = 'simpletest-' . $assertion->status;
-        if ($assertion->message_group == 'Debug') {
-          $class = 'simpletest-debug';
-        }
-        $rows[] = array('data' => $row, 'class' => array($class));
-
-        $group_summary['#' . $assertion->status]++;
-        $form['result']['summary']['#' . $assertion->status]++;
-      }
-      $form['result']['results'][$group]['table'] = array(
-        '#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;
+    // Add the results form.
+    $filter = static::addResultForm($form, $results, $this->getStringTranslation());
 
     // Actions.
-    $form['#action'] = \Drupal::url('simpletest.result_form', array('test_id' => 're-run'));
+    $form['#action'] = $this->url('simpletest.result_form', array('test_id' => 're-run'));
     $form['action'] = array(
       '#type' => 'fieldset',
       '#title' => $this->t('Actions'),
@@ -299,13 +223,36 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    *  Array of results grouped by test_class.
    */
   protected function getResults($test_id) {
-    $results = $this->database->select('simpletest')
+    return $this->database->select('simpletest')
       ->fields('simpletest')
       ->condition('test_id', $test_id)
       ->orderBy('test_class')
       ->orderBy('message_id')
-      ->execute();
+      ->execute()
+      ->fetchAll();
+  }
 
+  /**
+   * 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 $test_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
+   */
+  public static function addResultForm(array &$form, array $results) {
+    // Transform the test results to be grouped by test class.
     $test_results = array();
     foreach ($results as $result) {
       if (!isset($test_results[$result->test_class])) {
@@ -314,7 +261,93 @@ protected function getResults($test_id) {
       $test_results[$result->test_class][] = $result;
     }
 
-    return $test_results;
+    $image_status_map = static::buildStatusImageMap();
+
+    // Keep track of which test cases passed or failed.
+    $filter = array(
+      'pass' => array(),
+      'fail' => array(),
+    );
+
+    // Summary result widget.
+    $form['result'] = array(
+      '#type' => 'fieldset',
+      '#title' => 'Results',
+      // Because this is used in a theme-less situation need to provide a
+      // default.
+      '#attributes' => array(),
+    );
+    $form['result']['summary'] = $summary = array(
+      '#theme' => 'simpletest_result_summary',
+      '#pass' => 0,
+      '#fail' => 0,
+      '#exception' => 0,
+      '#debug' => 0,
+    );
+
+    \Drupal::service('test_discovery')->registerTestNamespaces();
+
+    // Cycle through each test group.
+    $header = array(
+      'Message',
+      'Group',
+      'Filename',
+      'Line',
+      'Function',
+      array('colspan' => 2, 'data' => 'Status')
+    );
+    $form['result']['results'] = array();
+    foreach ($test_results as $group => $assertions) {
+      // Create group details with summary information.
+      $info = TestDiscovery::getTestInfo($group);
+      $form['result']['results'][$group] = array(
+        '#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 = array();
+      foreach ($assertions as $assertion) {
+        $row = array();
+        // Assertion messages are in code, so we assume they are safe.
+        $row[] = SafeMarkup::set($assertion->message);
+        $row[] = $assertion->message_group;
+        $row[] = \Drupal::service('file_system')->basename(($assertion->file));
+        $row[] = $assertion->line;
+        $row[] = $assertion->function;
+        $row[] = $image_status_map[$assertion->status];
+
+        $class = 'simpletest-' . $assertion->status;
+        if ($assertion->message_group == 'Debug') {
+          $class = 'simpletest-debug';
+        }
+        $rows[] = array('data' => $row, 'class' => array($class));
+
+        $group_summary['#' . $assertion->status]++;
+        $form['result']['summary']['#' . $assertion->status]++;
+      }
+      $form['result']['results'][$group]['table'] = array(
+        '#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;
   }
 
 }
diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php
index abf09fd05d87ae6e4d5d45f2232b36a04fed1096..497f0855b221fd402aaf29d38bddcf13a0d4b433 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -873,10 +873,10 @@ protected function verbose($message) {
       return;
     }
 
-    $message = '<hr />ID #' . $this->verboseId . ' (<a href="' . $this->verboseClassName . '-' . ($this->verboseId - 1) . '.html">Previous</a> | <a href="' . $this->verboseClassName . '-' . ($this->verboseId + 1) . '.html">Next</a>)<hr />' . $message;
-    $verbose_filename = $this->verboseDirectory . '/' . $this->verboseClassName . '-' . $this->verboseId . '.html';
-    if (file_put_contents($verbose_filename, $message, FILE_APPEND)) {
-      $url = $this->verboseDirectoryUrl . '/' . $this->verboseClassName . '-' . $this->verboseId . '.html';
+    $message = '<hr />ID #' . $this->verboseId . ' (<a href="' . $this->verboseClassName . '-' . ($this->verboseId - 1) . '-' . $this->testId . '.html">Previous</a> | <a href="' . $this->verboseClassName . '-' . ($this->verboseId + 1) . '-' . $this->testId . '.html">Next</a>)<hr />' . $message;
+    $verbose_filename =  $this->verboseClassName . '-' . $this->verboseId . '-' . $this->testId . '.html';
+    if (file_put_contents($this->verboseDirectory . '/' . $verbose_filename, $message)) {
+      $url = $this->verboseDirectoryUrl . '/' . $verbose_filename;
       // Not using _l() to avoid invoking the theme system, so that unit tests
       // can use verbose() as well.
       $url = '<a href="' . $url . '" target="_blank">Verbose message</a>';
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 7bf821c52c09a56b4c8ae182790ecd9e473e3a84..2c13e752d1d9ef694399e7461abd2d6cbc056cf2 100644
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -6,9 +6,13 @@
  */
 
 use Drupal\Component\Utility\Timer;
+use Drupal\Component\Uuid\Php;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Form\FormState;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\StreamWrapper\PublicStream;
 use Drupal\Core\Test\TestRunnerKernel;
+use Drupal\simpletest\Form\SimpletestResultsForm;
 use Symfony\Component\HttpFoundation\Request;
 
 $autoloader = require_once __DIR__ . '/../vendor/autoload.php';
@@ -88,7 +92,12 @@
 simpletest_script_reporter_timer_stop();
 
 // Display results before database is cleared.
-simpletest_script_reporter_display_results();
+if ($args['browser']) {
+  simpletest_script_open_browser();
+}
+else {
+  simpletest_script_reporter_display_results();
+}
 
 if ($args['xml']) {
   simpletest_script_reporter_write_xml_results();
@@ -188,6 +197,10 @@ function simpletest_script_help() {
               test database and configuration directories. Use in combination
               with --repeat for debugging random test failures.
 
+  --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.
+
   <test1>[,<test2>[,<test3> ...]]
 
               One or more tests to be run. By default, these are interpreted
@@ -246,6 +259,7 @@ function simpletest_script_parse_args() {
     'test_names' => array(),
     'repeat' => 1,
     'die-on-fail' => FALSE,
+    'browser' => FALSE,
     // Used internally.
     'test-id' => 0,
     'execute-test' => '',
@@ -291,6 +305,9 @@ function simpletest_script_parse_args() {
     exit;
   }
 
+  if ($args['browser']) {
+    $args['keep-results'] = TRUE;
+  }
   return array($args, $count);
 }
 
@@ -1162,3 +1179,65 @@ function simpletest_script_load_messages_by_test_id($test_ids) {
 
   return $results;
 }
+
+/**
+ * Display test results.
+ */
+function simpletest_script_open_browser() {
+  global $test_ids;
+
+  $connection = Database::getConnection('default', 'test-runner');
+  $results = $connection->select('simpletest')
+    ->fields('simpletest')
+    ->condition('test_id', $test_ids, 'IN')
+    ->orderBy('test_class')
+    ->orderBy('message_id')
+    ->execute()
+    ->fetchAll();
+
+  // Get the results form.
+  $form = array();
+  SimpletestResultsForm::addResultForm($form, $results);
+
+  // Get the assets to make the details element collapsible and theme the result
+  // form.
+  $assets = new \Drupal\Core\Asset\AttachedAssets();
+  $assets->setLibraries(['core/drupal.collapse', 'system/admin', 'simpletest/drupal.simpletest']);
+  $resolver = \Drupal::service('asset.resolver');
+  list($js_assets_header, $js_assets_footer) = $resolver->getJsAssets($assets, FALSE);
+  $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
+  $js_assets_header = $js_collection_renderer->render($js_assets_header);
+  $js_assets_footer = $js_collection_renderer->render($js_assets_footer);
+  $css_assets = \Drupal::service('asset.css.collection_renderer')->render($resolver->getCssAssets($assets, FALSE));
+
+  // Make the html page to write to disk.
+  $html = '<head>' . drupal_render($js_assets_header) . drupal_render($css_assets) . '</head><body>' . drupal_render($form) . drupal_render($js_assets_footer) .'</body>';
+
+  // Ensure we have assets verbose directory - tests with no verbose output will not
+  // have created one.
+  $directory = PublicStream::basePath() . '/simpletest/verbose';
+  file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+  $uuid = new Php();
+  $filename = $directory .'/results-'. $uuid->generate() .'.html';
+  file_put_contents($filename, $html);
+
+  // See if we can find an OS helper to open URLs in default browser.
+  $browser = FALSE;
+  if (shell_exec('which xdg-open')) {
+    $browser = 'xdg-open';
+  }
+  elseif (shell_exec('which open')) {
+    $browser = 'open';
+  }
+  elseif (substr(PHP_OS, 0, 3) == 'WIN') {
+    $browser = 'start';
+  }
+
+  if ($browser) {
+    shell_exec($browser . ' ' . escapeshellarg($filename));
+  }
+  else {
+    // Can't find assets valid browser.
+    print 'Open file://' . realpath($filename) . ' in your browser to see the verbose output.';
+  }
+}