diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
index c8366fb..054b405 100644
--- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\Url;
 use Drupal\simpletest\TestDiscovery;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -58,37 +59,37 @@ public function __construct(Connection $database) {
   /**
    * Builds the status image map.
    */
-  protected function buildStatusImageMap() {
+  protected static function buildStatusImageMap(TranslationInterface $translation) {
     // Initialize image mapping property.
     $image_pass = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/73b355/check.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Pass'),
+      '#alt' => $translation->translate('Pass'),
     );
     $image_fail = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/ea2800/error.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Fail'),
+      '#alt' => $translation->translate('Fail'),
     );
     $image_exception = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/e29700/warning.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Exception'),
+      '#alt' => $translation->translate('Exception'),
     );
     $image_debug = array(
       '#theme' => 'image',
       '#uri' => 'core/misc/icons/e29700/warning.svg',
       '#width' => 18,
       '#height' => 18,
-      '#alt' => $this->t('Debug'),
+      '#alt' => $translation->translate('Debug'),
     );
-    $this->statusImageMap = array(
+    return array(
       'pass' => drupal_render($image_pass),
       'fail' => drupal_render($image_fail),
       'exception' => drupal_render($image_exception),
@@ -107,11 +108,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 +118,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 +219,34 @@ 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.
+   *
+   * @param array $form
+   *   The form to attach the results to.
+   * @param array $test_results
+   *   The simpletest results.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
+   *   The string translation service.
+   *
+   * @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.
+   */
+  public static function addResultForm(array &$form, array $results, TranslationInterface $translation) {
+    // 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 +255,93 @@ protected function getResults($test_id) {
       $test_results[$result->test_class][] = $result;
     }
 
-    return $test_results;
+    $image_status_map = static::buildStatusImageMap($translation);
+
+    // Keep track of which test cases passed or failed.
+    $filter = array(
+      'pass' => array(),
+      'fail' => array(),
+    );
+
+    // Summary result widget.
+    $form['result'] = array(
+      '#type' => 'fieldset',
+      '#title' => $translation->translate('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(
+      $translation->translate('Message'),
+      $translation->translate('Group'),
+      $translation->translate('Filename'),
+      $translation->translate('Line'),
+      $translation->translate('Function'),
+      array('colspan' => 2, 'data' => $translation->translate('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 f71e323..4f6b824 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -857,10 +857,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 7bf821c..eb0fa55 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,10 @@ function simpletest_script_parse_args() {
     exit;
   }
 
+  //
+  if ($args['browser']) {
+    $args['keep-results'] = TRUE;
+  }
   return array($args, $count);
 }
 
@@ -1162,3 +1180,51 @@ 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();
+  // Generate html results.
+  $form = array();
+  SimpletestResultsForm::addResultForm($form, $results, \Drupal::service('string_translation'));
+  $html = '<head>
+<link type="text/css" rel="stylesheet" href="'. file_create_url(drupal_get_path('module', 'system') . '/css/system.module.css') . '" media="all">
+<link type="text/css" rel="stylesheet" href="'. file_create_url(drupal_get_path('module', 'system') . '/css/system.theme.css') . '" media="all">
+<link type="text/css" rel="stylesheet" href="'. file_create_url(drupal_get_path('module', 'simpletest') . '/css/simpletest.module.css') . '" media="all">
+</head><body>' . drupal_render($form) .'</body>';
+  $uuid = new Php();
+  $directory = PublicStream::basePath() . '/simpletest/verbose';
+  file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+  $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.
+  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';
+  }
+  else {
+    // Can't find a valid browser.
+    // @todo Output some help giving the user the filename of the html to open.
+    $browser = FALSE;
+  }
+
+  if ($browser) {
+    shell_exec($browser . ' ' . escapeshellarg($filename));
+  }
+}
