From 761657a7dc71b423eed2e8377f5dfa055a08ee6b Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Thu, 22 Sep 2016 14:21:08 +0100
Subject: [PATCH] Issue #2763401 by klausi: PHPunit browser tests should log
 all Mink requests

---
 core/tests/Drupal/Tests/BrowserTestBase.php | 104 ++++++++++++++++++--
 1 file changed, 95 insertions(+), 9 deletions(-)

diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 5c772ea1b61f..f9f443caf102 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -23,6 +23,7 @@
 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
 use Drupal\Core\Test\TestRunnerKernel;
 use Drupal\Core\Url;
+use Drupal\Core\Utility\Error;
 use Drupal\Core\Test\TestDatabase;
 use Drupal\FunctionalTests\AssertLegacyTrait;
 use Drupal\simpletest\AssertHelperTrait;
@@ -32,6 +33,8 @@
 use Drupal\simpletest\UserCreationTrait;
 use Symfony\Component\CssSelector\CssSelectorConverter;
 use Symfony\Component\HttpFoundation\Request;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
 
 /**
  * Provides a test case for functional Drupal tests.
@@ -349,7 +352,14 @@ protected function initMink() {
     $driver = $this->getDefaultDriverInstance();
 
     if ($driver instanceof GoutteDriver) {
-      $driver->getClient()->setClient(\Drupal::httpClient());
+      $client = \Drupal::httpClient();
+
+      // Inject a Guzzle middleware to generate debug output for every request
+      // performed in the test.
+      $handler_stack = $client->getConfig('handler');
+      $handler_stack->push($this->getResponseLogHandler());
+
+      $driver->getClient()->setClient($client);
     }
 
     $session = new Session($driver);
@@ -408,6 +418,43 @@ protected function getDefaultDriverInstance() {
     return $driver;
   }
 
+  /**
+   * Provides a Guzzle middleware handler to log every response received.
+   *
+   * @return callable
+   *   The callable handler that will do the logging.
+   */
+  protected function getResponseLogHandler() {
+    return function (callable $handler) {
+      return function (RequestInterface $request, array $options) use ($handler) {
+        return $handler($request, $options)
+          ->then(function (ResponseInterface $response) use ($request) {
+            if ($this->htmlOutputEnabled) {
+
+              $caller = $this->getTestMethodCaller();
+              $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line'];
+              $html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri();
+
+              // On redirect responses (status code starting with '3') we need
+              // to remove the meta tag that would do a browser refresh. We
+              // don't want to redirect developers away when they look at the
+              // debug output file in their browser.
+              $body = $response->getBody();
+              $status_code = (string) $response->getStatusCode();
+              if ($status_code[0] === '3') {
+                $body = preg_replace('#<meta http-equiv="refresh" content=.+/>#', '', $body, 1);
+              }
+              $html_output .= '<hr />' . $body;
+              $html_output .= $this->formatHtmlOutputHeaders($response->getHeaders());
+
+              $this->htmlOutput($html_output);
+            }
+            return $response;
+          });
+      };
+    };
+  }
+
   /**
    * Registers additional Mink sessions.
    *
@@ -701,7 +748,9 @@ protected function drupalGet($path, array $options = array(), array $headers = a
     // Ensure that any changes to variables in the other thread are picked up.
     $this->refreshVariables();
 
-    if ($this->htmlOutputEnabled) {
+    // Log only for JavascriptTestBase tests because for Goutte we log with
+    // ::getResponseLogHandler.
+    if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
       $html_output = 'GET request to: ' . $url .
         '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
       $html_output .= '<hr />' . $out;
@@ -871,7 +920,10 @@ 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();
-    if ($this->htmlOutputEnabled) {
+
+    // Log only for JavascriptTestBase tests because for Goutte we log with
+    // ::getResponseLogHandler.
+    if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
       $out = $this->getSession()->getPage()->getContent();
       $html_output = 'POST request to: ' . $action .
         '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
@@ -879,6 +931,7 @@ protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
       $html_output .= $this->getHtmlOutputHeaders();
       $this->htmlOutput($html_output);
     }
+
   }
 
   /**
@@ -1578,15 +1631,28 @@ protected function htmlOutput($message) {
    *   HTML output headers.
    */
   protected function getHtmlOutputHeaders() {
-    $headers = array_map(function($headers) {
-      if (is_array($headers)) {
-        return implode(';', array_map('trim', $headers));
+    return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders());
+  }
+
+  /**
+   * Formats HTTP headers as string for HTML output logging.
+   *
+   * @param array[] $headers
+   *   Headers that should be formatted.
+   *
+   * @return string
+   *   The formatted HTML string.
+   */
+  protected function formatHtmlOutputHeaders(array $headers) {
+    $flattened_headers = array_map(function($header) {
+      if (is_array($header)) {
+        return implode(';', array_map('trim', $header));
       }
       else {
-        return $headers;
+        return $header;
       }
-    }, $this->getSession()->getResponseHeaders());
-    return '<hr />Headers: <pre>' . Html::escape(var_export($headers, TRUE)) . '</pre>';
+    }, $headers);
+    return '<hr />Headers: <pre>' . Html::escape(var_export($flattened_headers, TRUE)) . '</pre>';
   }
 
   /**
@@ -1788,4 +1854,24 @@ protected function getConfigSchemaExclusions() {
     return array_unique($exceptions);
   }
 
+  /**
+   * Retrieves the current calling line in the class under test.
+   *
+   * @return array
+   *   An associative array with keys 'file', 'line' and 'function'.
+   */
+  protected function getTestMethodCaller() {
+    $backtrace = debug_backtrace();
+
+    // Remove all calls until we reach the current test class.
+    while (($caller = $backtrace[1]) &&
+      (!isset($caller['class']) || $caller['class'] !== get_class($this))
+    ) {
+      // We remove that call.
+      array_shift($backtrace);
+    }
+
+    return Error::getLastCaller($backtrace);
+  }
+
 }
-- 
GitLab