Security update for Core, with self-updated composer
[yaffs-website] / web / core / tests / Drupal / Tests / BrowserTestBase.php
1 <?php
2
3 namespace Drupal\Tests;
4
5 use Behat\Mink\Driver\GoutteDriver;
6 use Behat\Mink\Element\Element;
7 use Behat\Mink\Mink;
8 use Behat\Mink\Selector\SelectorsHandler;
9 use Behat\Mink\Session;
10 use Drupal\Component\Render\FormattableMarkup;
11 use Drupal\Component\Serialization\Json;
12 use Drupal\Component\Utility\Html;
13 use Drupal\Component\Utility\UrlHelper;
14 use Drupal\Core\Database\Database;
15 use Drupal\Core\Session\AccountInterface;
16 use Drupal\Core\Session\AnonymousUserSession;
17 use Drupal\Core\Test\FunctionalTestSetupTrait;
18 use Drupal\Core\Test\TestSetupTrait;
19 use Drupal\Core\Url;
20 use Drupal\Core\Utility\Error;
21 use Drupal\FunctionalTests\AssertLegacyTrait;
22 use Drupal\Tests\block\Traits\BlockCreationTrait;
23 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
24 use Drupal\Tests\node\Traits\NodeCreationTrait;
25 use Drupal\Tests\user\Traits\UserCreationTrait;
26 use PHPUnit\Framework\TestCase;
27 use Psr\Http\Message\RequestInterface;
28 use Psr\Http\Message\ResponseInterface;
29 use Symfony\Component\CssSelector\CssSelectorConverter;
30
31 /**
32  * Provides a test case for functional Drupal tests.
33  *
34  * Tests extending BrowserTestBase must exist in the
35  * Drupal\Tests\yourmodule\Functional namespace and live in the
36  * modules/yourmodule/tests/src/Functional directory.
37  *
38  * Tests extending this base class should only translate text when testing
39  * translation functionality. For example, avoid wrapping test text with t()
40  * or TranslatableMarkup().
41  *
42  * @ingroup testing
43  */
44 abstract class BrowserTestBase extends TestCase {
45
46   use FunctionalTestSetupTrait;
47   use TestSetupTrait;
48   use AssertHelperTrait;
49   use BlockCreationTrait {
50     placeBlock as drupalPlaceBlock;
51   }
52   use AssertLegacyTrait;
53   use RandomGeneratorTrait;
54   use SessionTestTrait;
55   use NodeCreationTrait {
56     getNodeByTitle as drupalGetNodeByTitle;
57     createNode as drupalCreateNode;
58   }
59   use ContentTypeCreationTrait {
60     createContentType as drupalCreateContentType;
61   }
62   use ConfigTestTrait;
63   use TestRequirementsTrait;
64   use UserCreationTrait {
65     createRole as drupalCreateRole;
66     createUser as drupalCreateUser;
67   }
68   use XdebugRequestTrait;
69   use PhpunitCompatibilityTrait;
70
71   /**
72    * The database prefix of this test run.
73    *
74    * @var string
75    */
76   protected $databasePrefix;
77
78   /**
79    * Time limit in seconds for the test.
80    *
81    * @var int
82    */
83   protected $timeLimit = 500;
84
85   /**
86    * The translation file directory for the test environment.
87    *
88    * This is set in BrowserTestBase::prepareEnvironment().
89    *
90    * @var string
91    */
92   protected $translationFilesDirectory;
93
94   /**
95    * The config importer that can be used in a test.
96    *
97    * @var \Drupal\Core\Config\ConfigImporter
98    */
99   protected $configImporter;
100
101   /**
102    * Modules to enable.
103    *
104    * The test runner will merge the $modules lists from this class, the class
105    * it extends, and so on up the class hierarchy. It is not necessary to
106    * include modules in your list that a parent class has already declared.
107    *
108    * @var string[]
109    *
110    * @see \Drupal\Tests\BrowserTestBase::installDrupal()
111    */
112   protected static $modules = [];
113
114   /**
115    * The profile to install as a basis for testing.
116    *
117    * @var string
118    */
119   protected $profile = 'testing';
120
121   /**
122    * The current user logged in using the Mink controlled browser.
123    *
124    * @var \Drupal\user\UserInterface
125    */
126   protected $loggedInUser = FALSE;
127
128   /**
129    * An array of custom translations suitable for drupal_rewrite_settings().
130    *
131    * @var array
132    */
133   protected $customTranslations;
134
135   /*
136    * Mink class for the default driver to use.
137    *
138    * Shoud be a fully qualified class name that implements
139    * Behat\Mink\Driver\DriverInterface.
140    *
141    * Value can be overridden using the environment variable MINK_DRIVER_CLASS.
142    *
143    * @var string.
144    */
145   protected $minkDefaultDriverClass = GoutteDriver::class;
146
147   /*
148    * Mink default driver params.
149    *
150    * If it's an array its contents are used as constructor params when default
151    * Mink driver class is instantiated.
152    *
153    * Can be overridden using the environment variable MINK_DRIVER_ARGS. In this
154    * case that variable should be a JSON array, for example:
155    * '["firefox", null, "http://localhost:4444/wd/hub"]'.
156    *
157    *
158    * @var array
159    */
160   protected $minkDefaultDriverArgs;
161
162   /**
163    * Mink session manager.
164    *
165    * This will not be initialized if there was an error during the test setup.
166    *
167    * @var \Behat\Mink\Mink|null
168    */
169   protected $mink;
170
171   /**
172    * {@inheritdoc}
173    *
174    * Browser tests are run in separate processes to prevent collisions between
175    * code that may be loaded by tests.
176    */
177   protected $runTestInSeparateProcess = TRUE;
178
179   /**
180    * {@inheritdoc}
181    */
182   protected $preserveGlobalState = FALSE;
183
184   /**
185    * Class name for HTML output logging.
186    *
187    * @var string
188    */
189   protected $htmlOutputClassName;
190
191   /**
192    * Directory name for HTML output logging.
193    *
194    * @var string
195    */
196   protected $htmlOutputDirectory;
197
198   /**
199    * Counter storage for HTML output logging.
200    *
201    * @var string
202    */
203   protected $htmlOutputCounterStorage;
204
205   /**
206    * Counter for HTML output logging.
207    *
208    * @var int
209    */
210   protected $htmlOutputCounter = 1;
211
212   /**
213    * HTML output output enabled.
214    *
215    * @var bool
216    */
217   protected $htmlOutputEnabled = FALSE;
218
219   /**
220    * The file name to write the list of URLs to.
221    *
222    * This file is read by the PHPUnit result printer.
223    *
224    * @var string
225    *
226    * @see \Drupal\Tests\Listeners\HtmlOutputPrinter
227    */
228   protected $htmlOutputFile;
229
230   /**
231    * HTML output test ID.
232    *
233    * @var int
234    */
235   protected $htmlOutputTestId;
236
237   /**
238    * The base URL.
239    *
240    * @var string
241    */
242   protected $baseUrl;
243
244   /**
245    * The original array of shutdown function callbacks.
246    *
247    * @var array
248    */
249   protected $originalShutdownCallbacks = [];
250
251   /**
252    * The number of meta refresh redirects to follow, or NULL if unlimited.
253    *
254    * @var null|int
255    */
256   protected $maximumMetaRefreshCount = NULL;
257
258   /**
259    * The number of meta refresh redirects followed during ::drupalGet().
260    *
261    * @var int
262    */
263   protected $metaRefreshCount = 0;
264
265   /**
266    * The app root.
267    *
268    * @var string
269    */
270   protected $root;
271
272   /**
273    * The original container.
274    *
275    * Move this to \Drupal\Core\Test\FunctionalTestSetupTrait once TestBase no
276    * longer provides the same value.
277    *
278    * @var \Symfony\Component\DependencyInjection\ContainerInterface
279    */
280   protected $originalContainer;
281
282   /**
283    * {@inheritdoc}
284    */
285   public function __construct($name = NULL, array $data = [], $dataName = '') {
286     parent::__construct($name, $data, $dataName);
287
288     $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
289   }
290
291   /**
292    * Initializes Mink sessions.
293    */
294   protected function initMink() {
295     $driver = $this->getDefaultDriverInstance();
296
297     if ($driver instanceof GoutteDriver) {
298       // Turn off curl timeout. Having a timeout is not a problem in a normal
299       // test running, but it is a problem when debugging. Also, disable SSL
300       // peer verification so that testing under HTTPS always works.
301       /** @var \GuzzleHttp\Client $client */
302       $client = $this->container->get('http_client_factory')->fromOptions([
303         'timeout' => NULL,
304         'verify' => FALSE,
305       ]);
306
307       // Inject a Guzzle middleware to generate debug output for every request
308       // performed in the test.
309       $handler_stack = $client->getConfig('handler');
310       $handler_stack->push($this->getResponseLogHandler());
311
312       $driver->getClient()->setClient($client);
313     }
314
315     $selectors_handler = new SelectorsHandler([
316       'hidden_field_selector' => new HiddenFieldSelector()
317     ]);
318     $session = new Session($driver, $selectors_handler);
319     $this->mink = new Mink();
320     $this->mink->registerSession('default', $session);
321     $this->mink->setDefaultSessionName('default');
322     $this->registerSessions();
323
324     // According to the W3C WebDriver specification a cookie can only be set if
325     // the cookie domain is equal to the domain of the active document. When the
326     // browser starts up the active document is not our domain but 'about:blank'
327     // or similar. To be able to set our User-Agent and Xdebug cookies at the
328     // start of the test we now do a request to the front page so the active
329     // document matches the domain.
330     // @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
331     // @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
332     $session = $this->getSession();
333     $session->visit($this->baseUrl);
334
335     return $session;
336   }
337
338   /**
339    * Gets an instance of the default Mink driver.
340    *
341    * @return Behat\Mink\Driver\DriverInterface
342    *   Instance of default Mink driver.
343    *
344    * @throws \InvalidArgumentException
345    *   When provided default Mink driver class can't be instantiated.
346    */
347   protected function getDefaultDriverInstance() {
348     // Get default driver params from environment if availables.
349     if ($arg_json = getenv('MINK_DRIVER_ARGS')) {
350       $this->minkDefaultDriverArgs = json_decode($arg_json, TRUE);
351     }
352
353     // Get and check default driver class from environment if availables.
354     if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) {
355       if (class_exists($minkDriverClass)) {
356         $this->minkDefaultDriverClass = $minkDriverClass;
357       }
358       else {
359         throw new \InvalidArgumentException("Can't instantiate provided $minkDriverClass class by environment as default driver class.");
360       }
361     }
362
363     if (is_array($this->minkDefaultDriverArgs)) {
364       // Use ReflectionClass to instantiate class with received params.
365       $reflector = new \ReflectionClass($this->minkDefaultDriverClass);
366       $driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs);
367     }
368     else {
369       $driver = new $this->minkDefaultDriverClass();
370     }
371     return $driver;
372   }
373
374   /**
375    * Creates the directory to store browser output.
376    *
377    * Creates the directory to store browser output in if a file to write
378    * URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter.
379    */
380   protected function initBrowserOutputFile() {
381     $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE');
382     $this->htmlOutputEnabled = is_file($browser_output_file);
383     if ($this->htmlOutputEnabled) {
384       $this->htmlOutputFile = $browser_output_file;
385       $this->htmlOutputClassName = str_replace("\\", "_", get_called_class());
386       $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output';
387       if (file_prepare_directory($this->htmlOutputDirectory, FILE_CREATE_DIRECTORY) && !file_exists($this->htmlOutputDirectory . '/.htaccess')) {
388         file_put_contents($this->htmlOutputDirectory . '/.htaccess', "<IfModule mod_expires.c>\nExpiresActive Off\n</IfModule>\n");
389       }
390       $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter';
391       $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory);
392       if (is_file($this->htmlOutputCounterStorage)) {
393         $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1;
394       }
395     }
396   }
397
398   /**
399    * Provides a Guzzle middleware handler to log every response received.
400    *
401    * @return callable
402    *   The callable handler that will do the logging.
403    */
404   protected function getResponseLogHandler() {
405     return function (callable $handler) {
406       return function (RequestInterface $request, array $options) use ($handler) {
407         return $handler($request, $options)
408           ->then(function (ResponseInterface $response) use ($request) {
409             if ($this->htmlOutputEnabled) {
410
411               $caller = $this->getTestMethodCaller();
412               $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line'];
413               $html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri();
414
415               // On redirect responses (status code starting with '3') we need
416               // to remove the meta tag that would do a browser refresh. We
417               // don't want to redirect developers away when they look at the
418               // debug output file in their browser.
419               $body = $response->getBody();
420               $status_code = (string) $response->getStatusCode();
421               if ($status_code[0] === '3') {
422                 $body = preg_replace('#<meta http-equiv="refresh" content=.+/>#', '', $body, 1);
423               }
424               $html_output .= '<hr />' . $body;
425               $html_output .= $this->formatHtmlOutputHeaders($response->getHeaders());
426
427               $this->htmlOutput($html_output);
428             }
429             return $response;
430           });
431       };
432     };
433   }
434
435   /**
436    * Registers additional Mink sessions.
437    *
438    * Tests wishing to use a different driver or change the default driver should
439    * override this method.
440    *
441    * @code
442    *   // Register a new session that uses the MinkPonyDriver.
443    *   $pony = new MinkPonyDriver();
444    *   $session = new Session($pony);
445    *   $this->mink->registerSession('pony', $session);
446    * @endcode
447    */
448   protected function registerSessions() {}
449
450   /**
451    * {@inheritdoc}
452    */
453   protected function setUp() {
454     // Installing Drupal creates 1000s of objects. Garbage collection of these
455     // objects is expensive. This appears to be causing random segmentation
456     // faults in PHP 5.x due to https://bugs.php.net/bug.php?id=72286. Once
457     // Drupal is installed is rebuilt, garbage collection is re-enabled.
458     $disable_gc = version_compare(PHP_VERSION, '7', '<') && gc_enabled();
459     if ($disable_gc) {
460       gc_collect_cycles();
461       gc_disable();
462     }
463     parent::setUp();
464
465     $this->setupBaseUrl();
466
467     // Install Drupal test site.
468     $this->prepareEnvironment();
469     $this->installDrupal();
470
471     // Setup Mink.
472     $session = $this->initMink();
473
474     $cookies = $this->extractCookiesFromRequest(\Drupal::request());
475     foreach ($cookies as $cookie_name => $values) {
476       foreach ($values as $value) {
477         $session->setCookie($cookie_name, $value);
478       }
479     }
480
481     // Set up the browser test output file.
482     $this->initBrowserOutputFile();
483     // If garbage collection was disabled prior to rebuilding container,
484     // re-enable it.
485     if ($disable_gc) {
486       gc_enable();
487     }
488   }
489
490   /**
491    * Ensures test files are deletable within file_unmanaged_delete_recursive().
492    *
493    * Some tests chmod generated files to be read only. During
494    * BrowserTestBase::cleanupEnvironment() and other cleanup operations,
495    * these files need to get deleted too.
496    *
497    * @param string $path
498    *   The file path.
499    */
500   public static function filePreDeleteCallback($path) {
501     // When the webserver runs with the same system user as phpunit, we can
502     // make read-only files writable again. If not, chmod will fail while the
503     // file deletion still works if file permissions have been configured
504     // correctly. Thus, we ignore any problems while running chmod.
505     @chmod($path, 0700);
506   }
507
508   /**
509    * Clean up the Simpletest environment.
510    */
511   protected function cleanupEnvironment() {
512     // Remove all prefixed tables.
513     $original_connection_info = Database::getConnectionInfo('simpletest_original_default');
514     $original_prefix = $original_connection_info['default']['prefix']['default'];
515     $test_connection_info = Database::getConnectionInfo('default');
516     $test_prefix = $test_connection_info['default']['prefix']['default'];
517     if ($original_prefix != $test_prefix) {
518       $tables = Database::getConnection()->schema()->findTables('%');
519       foreach ($tables as $table) {
520         if (Database::getConnection()->schema()->dropTable($table)) {
521           unset($tables[$table]);
522         }
523       }
524     }
525
526     // Delete test site directory.
527     file_unmanaged_delete_recursive($this->siteDirectory, [$this, 'filePreDeleteCallback']);
528   }
529
530   /**
531    * {@inheritdoc}
532    */
533   protected function tearDown() {
534     parent::tearDown();
535
536     // Destroy the testing kernel.
537     if (isset($this->kernel)) {
538       $this->cleanupEnvironment();
539       $this->kernel->shutdown();
540     }
541
542     // Ensure that internal logged in variable is reset.
543     $this->loggedInUser = FALSE;
544
545     if ($this->mink) {
546       $this->mink->stopSessions();
547     }
548
549     // Restore original shutdown callbacks.
550     if (function_exists('drupal_register_shutdown_function')) {
551       $callbacks = &drupal_register_shutdown_function();
552       $callbacks = $this->originalShutdownCallbacks;
553     }
554   }
555
556   /**
557    * Returns Mink session.
558    *
559    * @param string $name
560    *   (optional) Name of the session. Defaults to the active session.
561    *
562    * @return \Behat\Mink\Session
563    *   The active Mink session object.
564    */
565   public function getSession($name = NULL) {
566     return $this->mink->getSession($name);
567   }
568
569   /**
570    * Returns WebAssert object.
571    *
572    * @param string $name
573    *   (optional) Name of the session. Defaults to the active session.
574    *
575    * @return \Drupal\Tests\WebAssert
576    *   A new web-assert option for asserting the presence of elements with.
577    */
578   public function assertSession($name = NULL) {
579     return new WebAssert($this->getSession($name), $this->baseUrl);
580   }
581
582   /**
583    * Prepare for a request to testing site.
584    *
585    * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is
586    * checked by drupal_valid_test_ua().
587    *
588    * @see drupal_valid_test_ua()
589    */
590   protected function prepareRequest() {
591     $session = $this->getSession();
592     $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix));
593   }
594
595   /**
596    * Builds an a absolute URL from a system path or a URL object.
597    *
598    * @param string|\Drupal\Core\Url $path
599    *   A system path or a URL.
600    * @param array $options
601    *   Options to be passed to Url::fromUri().
602    *
603    * @return string
604    *   An absolute URL stsring.
605    */
606   protected function buildUrl($path, array $options = []) {
607     if ($path instanceof Url) {
608       $url_options = $path->getOptions();
609       $options = $url_options + $options;
610       $path->setOptions($options);
611       return $path->setAbsolute()->toString();
612     }
613     // The URL generator service is not necessarily available yet; e.g., in
614     // interactive installer tests.
615     elseif ($this->container->has('url_generator')) {
616       $force_internal = isset($options['external']) && $options['external'] == FALSE;
617       if (!$force_internal && UrlHelper::isExternal($path)) {
618         return Url::fromUri($path, $options)->toString();
619       }
620       else {
621         $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path;
622         // Path processing is needed for language prefixing.  Skip it when a
623         // path that may look like an external URL is being used as internal.
624         $options['path_processing'] = !$force_internal;
625         return Url::fromUri($uri, $options)
626           ->setAbsolute()
627           ->toString();
628       }
629     }
630     else {
631       return $this->getAbsoluteUrl($path);
632     }
633   }
634
635   /**
636    * Retrieves a Drupal path or an absolute path.
637    *
638    * @param string|\Drupal\Core\Url $path
639    *   Drupal path or URL to load into Mink controlled browser.
640    * @param array $options
641    *   (optional) Options to be forwarded to the url generator.
642    * @param string[] $headers
643    *   An array containing additional HTTP request headers, the array keys are
644    *   the header names and the array values the header values. This is useful
645    *   to set for example the "Accept-Language" header for requesting the page
646    *   in a different language. Note that not all headers are supported, for
647    *   example the "Accept" header is always overridden by the browser. For
648    *   testing REST APIs it is recommended to directly use an HTTP client such
649    *   as Guzzle instead.
650    *
651    * @return string
652    *   The retrieved HTML string, also available as $this->getRawContent()
653    */
654   protected function drupalGet($path, array $options = [], array $headers = []) {
655     $options['absolute'] = TRUE;
656     $url = $this->buildUrl($path, $options);
657
658     $session = $this->getSession();
659
660     $this->prepareRequest();
661     foreach ($headers as $header_name => $header_value) {
662       $session->setRequestHeader($header_name, $header_value);
663     }
664
665     $session->visit($url);
666     $out = $session->getPage()->getContent();
667
668     // Ensure that any changes to variables in the other thread are picked up.
669     $this->refreshVariables();
670
671     // Replace original page output with new output from redirected page(s).
672     if ($new = $this->checkForMetaRefresh()) {
673       $out = $new;
674       // We are finished with all meta refresh redirects, so reset the counter.
675       $this->metaRefreshCount = 0;
676     }
677
678     // Log only for JavascriptTestBase tests because for Goutte we log with
679     // ::getResponseLogHandler.
680     if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
681       $html_output = 'GET request to: ' . $url .
682         '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
683       $html_output .= '<hr />' . $out;
684       $html_output .= $this->getHtmlOutputHeaders();
685       $this->htmlOutput($html_output);
686     }
687
688     return $out;
689   }
690
691   /**
692    * Takes a path and returns an absolute path.
693    *
694    * @param string $path
695    *   A path from the Mink controlled browser content.
696    *
697    * @return string
698    *   The $path with $base_url prepended, if necessary.
699    */
700   protected function getAbsoluteUrl($path) {
701     global $base_url, $base_path;
702
703     $parts = parse_url($path);
704     if (empty($parts['host'])) {
705       // Ensure that we have a string (and no xpath object).
706       $path = (string) $path;
707       // Strip $base_path, if existent.
708       $length = strlen($base_path);
709       if (substr($path, 0, $length) === $base_path) {
710         $path = substr($path, $length);
711       }
712       // Ensure that we have an absolute path.
713       if (empty($path) || $path[0] !== '/') {
714         $path = '/' . $path;
715       }
716       // Finally, prepend the $base_url.
717       $path = $base_url . $path;
718     }
719     return $path;
720   }
721
722   /**
723    * Logs in a user using the Mink controlled browser.
724    *
725    * If a user is already logged in, then the current user is logged out before
726    * logging in the specified user.
727    *
728    * Please note that neither the current user nor the passed-in user object is
729    * populated with data of the logged in user. If you need full access to the
730    * user object after logging in, it must be updated manually. If you also need
731    * access to the plain-text password of the user (set by drupalCreateUser()),
732    * e.g. to log in the same user again, then it must be re-assigned manually.
733    * For example:
734    * @code
735    *   // Create a user.
736    *   $account = $this->drupalCreateUser(array());
737    *   $this->drupalLogin($account);
738    *   // Load real user object.
739    *   $pass_raw = $account->passRaw;
740    *   $account = User::load($account->id());
741    *   $account->passRaw = $pass_raw;
742    * @endcode
743    *
744    * @param \Drupal\Core\Session\AccountInterface $account
745    *   User object representing the user to log in.
746    *
747    * @see drupalCreateUser()
748    */
749   protected function drupalLogin(AccountInterface $account) {
750     if ($this->loggedInUser) {
751       $this->drupalLogout();
752     }
753
754     $this->drupalGet('user/login');
755     $this->submitForm([
756       'name' => $account->getUsername(),
757       'pass' => $account->passRaw,
758     ], t('Log in'));
759
760     // @see BrowserTestBase::drupalUserIsLoggedIn()
761     $account->sessionId = $this->getSession()->getCookie($this->getSessionName());
762     $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()]));
763
764     $this->loggedInUser = $account;
765     $this->container->get('current_user')->setAccount($account);
766   }
767
768   /**
769    * Logs a user out of the Mink controlled browser and confirms.
770    *
771    * Confirms logout by checking the login page.
772    */
773   protected function drupalLogout() {
774     // Make a request to the logout page, and redirect to the user page, the
775     // idea being if you were properly logged out you should be seeing a login
776     // screen.
777     $assert_session = $this->assertSession();
778     $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]);
779     $assert_session->fieldExists('name');
780     $assert_session->fieldExists('pass');
781
782     // @see BrowserTestBase::drupalUserIsLoggedIn()
783     unset($this->loggedInUser->sessionId);
784     $this->loggedInUser = FALSE;
785     $this->container->get('current_user')->setAccount(new AnonymousUserSession());
786   }
787
788   /**
789    * Fills and submits a form.
790    *
791    * @param array $edit
792    *   Field data in an associative array. Changes the current input fields
793    *   (where possible) to the values indicated.
794    *
795    *   A checkbox can be set to TRUE to be checked and should be set to FALSE to
796    *   be unchecked.
797    * @param string $submit
798    *   Value of the submit button whose click is to be emulated. For example,
799    *   'Save'. The processing of the request depends on this value. For example,
800    *   a form may have one button with the value 'Save' and another button with
801    *   the value 'Delete', and execute different code depending on which one is
802    *   clicked.
803    * @param string $form_html_id
804    *   (optional) HTML ID of the form to be submitted. On some pages
805    *   there are many identical forms, so just using the value of the submit
806    *   button is not enough. For example: 'trigger-node-presave-assign-form'.
807    *   Note that this is not the Drupal $form_id, but rather the HTML ID of the
808    *   form, which is typically the same thing but with hyphens replacing the
809    *   underscores.
810    */
811   protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
812     $assert_session = $this->assertSession();
813
814     // Get the form.
815     if (isset($form_html_id)) {
816       $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']");
817       $submit_button = $assert_session->buttonExists($submit, $form);
818       $action = $form->getAttribute('action');
819     }
820     else {
821       $submit_button = $assert_session->buttonExists($submit);
822       $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button);
823       $action = $form->getAttribute('action');
824     }
825
826     // Edit the form values.
827     foreach ($edit as $name => $value) {
828       $field = $assert_session->fieldExists($name, $form);
829
830       // Provide support for the values '1' and '0' for checkboxes instead of
831       // TRUE and FALSE.
832       // @todo Get rid of supporting 1/0 by converting all tests cases using
833       // this to boolean values.
834       $field_type = $field->getAttribute('type');
835       if ($field_type === 'checkbox') {
836         $value = (bool) $value;
837       }
838
839       $field->setValue($value);
840     }
841
842     // Submit form.
843     $this->prepareRequest();
844     $submit_button->press();
845
846     // Ensure that any changes to variables in the other thread are picked up.
847     $this->refreshVariables();
848
849     // Check if there are any meta refresh redirects (like Batch API pages).
850     if ($this->checkForMetaRefresh()) {
851       // We are finished with all meta refresh redirects, so reset the counter.
852       $this->metaRefreshCount = 0;
853     }
854
855     // Log only for JavascriptTestBase tests because for Goutte we log with
856     // ::getResponseLogHandler.
857     if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
858       $out = $this->getSession()->getPage()->getContent();
859       $html_output = 'POST request to: ' . $action .
860         '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
861       $html_output .= '<hr />' . $out;
862       $html_output .= $this->getHtmlOutputHeaders();
863       $this->htmlOutput($html_output);
864     }
865
866   }
867
868   /**
869    * Executes a form submission.
870    *
871    * It will be done as usual POST request with Mink.
872    *
873    * @param \Drupal\Core\Url|string $path
874    *   Location of the post form. Either a Drupal path or an absolute path or
875    *   NULL to post to the current page. For multi-stage forms you can set the
876    *   path to NULL and have it post to the last received page. Example:
877    *
878    *   @code
879    *   // First step in form.
880    *   $edit = array(...);
881    *   $this->drupalPostForm('some_url', $edit, 'Save');
882    *
883    *   // Second step in form.
884    *   $edit = array(...);
885    *   $this->drupalPostForm(NULL, $edit, 'Save');
886    *   @endcode
887    * @param array $edit
888    *   Field data in an associative array. Changes the current input fields
889    *   (where possible) to the values indicated.
890    *
891    *   When working with form tests, the keys for an $edit element should match
892    *   the 'name' parameter of the HTML of the form. For example, the 'body'
893    *   field for a node has the following HTML:
894    *   @code
895    *   <textarea id="edit-body-und-0-value" class="text-full form-textarea
896    *    resize-vertical" placeholder="" cols="60" rows="9"
897    *    name="body[0][value]"></textarea>
898    *   @endcode
899    *   When testing this field using an $edit parameter, the code becomes:
900    *   @code
901    *   $edit["body[0][value]"] = 'My test value';
902    *   @endcode
903    *
904    *   A checkbox can be set to TRUE to be checked and should be set to FALSE to
905    *   be unchecked. Multiple select fields can be tested using 'name[]' and
906    *   setting each of the desired values in an array:
907    *   @code
908    *   $edit = array();
909    *   $edit['name[]'] = array('value1', 'value2');
910    *   @endcode
911    *   @todo change $edit to disallow NULL as a value for Drupal 9.
912    *     https://www.drupal.org/node/2802401
913    * @param string $submit
914    *   Value of the submit button whose click is to be emulated. For example,
915    *   'Save'. The processing of the request depends on this value. For example,
916    *   a form may have one button with the value 'Save' and another button with
917    *   the value 'Delete', and execute different code depending on which one is
918    *   clicked.
919    *
920    *   This function can also be called to emulate an Ajax submission. In this
921    *   case, this value needs to be an array with the following keys:
922    *   - path: A path to submit the form values to for Ajax-specific processing.
923    *   - triggering_element: If the value for the 'path' key is a generic Ajax
924    *     processing path, this needs to be set to the name of the element. If
925    *     the name doesn't identify the element uniquely, then this should
926    *     instead be an array with a single key/value pair, corresponding to the
927    *     element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder
928    *     uses this to find the #ajax information for the element, including
929    *     which specific callback to use for processing the request.
930    *
931    *   This can also be set to NULL in order to emulate an Internet Explorer
932    *   submission of a form with a single text field, and pressing ENTER in that
933    *   textfield: under these conditions, no button information is added to the
934    *   POST data.
935    * @param array $options
936    *   Options to be forwarded to the url generator.
937    *
938    * @return string
939    *   (deprecated) The response content after submit form. It is necessary for
940    *   backwards compatibility and will be removed before Drupal 9.0. You should
941    *   just use the webAssert object for your assertions.
942    */
943   protected function drupalPostForm($path, $edit, $submit, array $options = []) {
944     if (is_object($submit)) {
945       // Cast MarkupInterface objects to string.
946       $submit = (string) $submit;
947     }
948     if ($edit === NULL) {
949       $edit = [];
950     }
951     if (is_array($edit)) {
952       $edit = $this->castSafeStrings($edit);
953     }
954
955     if (isset($path)) {
956       $this->drupalGet($path, $options);
957     }
958
959     $this->submitForm($edit, $submit);
960
961     return $this->getSession()->getPage()->getContent();
962   }
963
964   /**
965    * Helper function to get the options of select field.
966    *
967    * @param \Behat\Mink\Element\NodeElement|string $select
968    *   Name, ID, or Label of select field to assert.
969    * @param \Behat\Mink\Element\Element $container
970    *   (optional) Container element to check against. Defaults to current page.
971    *
972    * @return array
973    *   Associative array of option keys and values.
974    */
975   protected function getOptions($select, Element $container = NULL) {
976     if (is_string($select)) {
977       $select = $this->assertSession()->selectExists($select, $container);
978     }
979     $options = [];
980     /* @var \Behat\Mink\Element\NodeElement $option */
981     foreach ($select->findAll('xpath', '//option') as $option) {
982       $label = $option->getText();
983       $value = $option->getAttribute('value') ?: $label;
984       $options[$value] = $label;
985     }
986     return $options;
987   }
988
989   /**
990    * Installs Drupal into the Simpletest site.
991    */
992   public function installDrupal() {
993     $this->initUserSession();
994     $this->prepareSettings();
995     $this->doInstall();
996     $this->initSettings();
997     $container = $this->initKernel(\Drupal::request());
998     $this->initConfig($container);
999     $this->installModulesFromClassProperty($container);
1000     $this->rebuildAll();
1001   }
1002
1003   /**
1004    * Returns whether a given user account is logged in.
1005    *
1006    * @param \Drupal\Core\Session\AccountInterface $account
1007    *   The user account object to check.
1008    *
1009    * @return bool
1010    *   Return TRUE if the user is logged in, FALSE otherwise.
1011    */
1012   protected function drupalUserIsLoggedIn(AccountInterface $account) {
1013     $logged_in = FALSE;
1014
1015     if (isset($account->sessionId)) {
1016       $session_handler = $this->container->get('session_handler.storage');
1017       $logged_in = (bool) $session_handler->read($account->sessionId);
1018     }
1019
1020     return $logged_in;
1021   }
1022
1023   /**
1024    * Clicks the element with the given CSS selector.
1025    *
1026    * @param string $css_selector
1027    *   The CSS selector identifying the element to click.
1028    */
1029   protected function click($css_selector) {
1030     $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector));
1031   }
1032
1033   /**
1034    * Prevents serializing any properties.
1035    *
1036    * Browser tests are run in a separate process. To do this PHPUnit creates a
1037    * script to run the test. If it fails, the test result object will contain a
1038    * stack trace which includes the test object. It will attempt to serialize
1039    * it. Returning an empty array prevents it from serializing anything it
1040    * should not.
1041    *
1042    * @return array
1043    *   An empty array.
1044    *
1045    * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist
1046    */
1047   public function __sleep() {
1048     return [];
1049   }
1050
1051   /**
1052    * Logs a HTML output message in a text file.
1053    *
1054    * The link to the HTML output message will be printed by the results printer.
1055    *
1056    * @param string $message
1057    *   The HTML output message to be stored.
1058    *
1059    * @see \Drupal\Tests\Listeners\VerbosePrinter::printResult()
1060    */
1061   protected function htmlOutput($message) {
1062     if (!$this->htmlOutputEnabled) {
1063       return;
1064     }
1065     $message = '<hr />ID #' . $this->htmlOutputCounter . ' (<a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter - 1) . '-' . $this->htmlOutputTestId . '.html">Previous</a> | <a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter + 1) . '-' . $this->htmlOutputTestId . '.html">Next</a>)<hr />' . $message;
1066     $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html';
1067     file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message);
1068     file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++);
1069     file_put_contents($this->htmlOutputFile, file_create_url('sites/simpletest/browser_output/' . $html_output_filename) . "\n", FILE_APPEND);
1070   }
1071
1072   /**
1073    * Returns headers in HTML output format.
1074    *
1075    * @return string
1076    *   HTML output headers.
1077    */
1078   protected function getHtmlOutputHeaders() {
1079     return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders());
1080   }
1081
1082   /**
1083    * Formats HTTP headers as string for HTML output logging.
1084    *
1085    * @param array[] $headers
1086    *   Headers that should be formatted.
1087    *
1088    * @return string
1089    *   The formatted HTML string.
1090    */
1091   protected function formatHtmlOutputHeaders(array $headers) {
1092     $flattened_headers = array_map(function ($header) {
1093       if (is_array($header)) {
1094         return implode(';', array_map('trim', $header));
1095       }
1096       else {
1097         return $header;
1098       }
1099     }, $headers);
1100     return '<hr />Headers: <pre>' . Html::escape(var_export($flattened_headers, TRUE)) . '</pre>';
1101   }
1102
1103   /**
1104    * Translates a CSS expression to its XPath equivalent.
1105    *
1106    * The search is relative to the root element (HTML tag normally) of the page.
1107    *
1108    * @param string $selector
1109    *   CSS selector to use in the search.
1110    * @param bool $html
1111    *   (optional) Enables HTML support. Disable it for XML documents.
1112    * @param string $prefix
1113    *   (optional) The prefix for the XPath expression.
1114    *
1115    * @return string
1116    *   The equivalent XPath of a CSS expression.
1117    */
1118   protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descendant-or-self::') {
1119     return (new CssSelectorConverter($html))->toXPath($selector, $prefix);
1120   }
1121
1122   /**
1123    * Searches elements using a CSS selector in the raw content.
1124    *
1125    * The search is relative to the root element (HTML tag normally) of the page.
1126    *
1127    * @param string $selector
1128    *   CSS selector to use in the search.
1129    *
1130    * @return \Behat\Mink\Element\NodeElement[]
1131    *   The list of elements on the page that match the selector.
1132    */
1133   protected function cssSelect($selector) {
1134     return $this->getSession()->getPage()->findAll('css', $selector);
1135   }
1136
1137   /**
1138    * Follows a link by complete name.
1139    *
1140    * Will click the first link found with this link text.
1141    *
1142    * If the link is discovered and clicked, the test passes. Fail otherwise.
1143    *
1144    * @param string|\Drupal\Component\Render\MarkupInterface $label
1145    *   Text between the anchor tags.
1146    * @param int $index
1147    *   (optional) The index number for cases where multiple links have the same
1148    *   text. Defaults to 0.
1149    */
1150   protected function clickLink($label, $index = 0) {
1151     $label = (string) $label;
1152     $links = $this->getSession()->getPage()->findAll('named', ['link', $label]);
1153     $links[$index]->click();
1154   }
1155
1156   /**
1157    * Retrieves the plain-text content from the current page.
1158    */
1159   protected function getTextContent() {
1160     return $this->getSession()->getPage()->getText();
1161   }
1162
1163   /**
1164    * Performs an xpath search on the contents of the internal browser.
1165    *
1166    * The search is relative to the root element (HTML tag normally) of the page.
1167    *
1168    * @param string $xpath
1169    *   The xpath string to use in the search.
1170    * @param array $arguments
1171    *   An array of arguments with keys in the form ':name' matching the
1172    *   placeholders in the query. The values may be either strings or numeric
1173    *   values.
1174    *
1175    * @return \Behat\Mink\Element\NodeElement[]
1176    *   The list of elements matching the xpath expression.
1177    */
1178   protected function xpath($xpath, array $arguments = []) {
1179     $xpath = $this->assertSession()->buildXPathQuery($xpath, $arguments);
1180     return $this->getSession()->getPage()->findAll('xpath', $xpath);
1181   }
1182
1183   /**
1184    * Configuration accessor for tests. Returns non-overridden configuration.
1185    *
1186    * @param string $name
1187    *   Configuration name.
1188    *
1189    * @return \Drupal\Core\Config\Config
1190    *   The configuration object with original configuration data.
1191    */
1192   protected function config($name) {
1193     return $this->container->get('config.factory')->getEditable($name);
1194   }
1195
1196   /**
1197    * Returns all response headers.
1198    *
1199    * @return array
1200    *   The HTTP headers values.
1201    *
1202    * @deprecated Scheduled for removal in Drupal 9.0.0.
1203    *   Use $this->getSession()->getResponseHeaders() instead.
1204    */
1205   protected function drupalGetHeaders() {
1206     return $this->getSession()->getResponseHeaders();
1207   }
1208
1209   /**
1210    * Gets the value of an HTTP response header.
1211    *
1212    * If multiple requests were required to retrieve the page, only the headers
1213    * from the last request will be checked by default.
1214    *
1215    * @param string $name
1216    *   The name of the header to retrieve. Names are case-insensitive (see RFC
1217    *   2616 section 4.2).
1218    *
1219    * @return string|null
1220    *   The HTTP header value or NULL if not found.
1221    */
1222   protected function drupalGetHeader($name) {
1223     return $this->getSession()->getResponseHeader($name);
1224   }
1225
1226   /**
1227    * Get the current URL from the browser.
1228    *
1229    * @return string
1230    *   The current URL.
1231    */
1232   protected function getUrl() {
1233     return $this->getSession()->getCurrentUrl();
1234   }
1235
1236   /**
1237    * Gets the JavaScript drupalSettings variable for the currently-loaded page.
1238    *
1239    * @return array
1240    *   The JSON decoded drupalSettings value from the current page.
1241    */
1242   protected function getDrupalSettings() {
1243     $html = $this->getSession()->getPage()->getHtml();
1244     if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) {
1245       return Json::decode($matches[1]);
1246     }
1247     return [];
1248   }
1249
1250   /**
1251    * {@inheritdoc}
1252    */
1253   public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
1254     // Cast objects implementing MarkupInterface to string instead of
1255     // relying on PHP casting them to string depending on what they are being
1256     // comparing with.
1257     $expected = static::castSafeStrings($expected);
1258     $actual = static::castSafeStrings($actual);
1259     parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
1260   }
1261
1262   /**
1263    * Retrieves the current calling line in the class under test.
1264    *
1265    * @return array
1266    *   An associative array with keys 'file', 'line' and 'function'.
1267    */
1268   protected function getTestMethodCaller() {
1269     $backtrace = debug_backtrace();
1270     // Find the test class that has the test method.
1271     while ($caller = Error::getLastCaller($backtrace)) {
1272       if (isset($caller['class']) && $caller['class'] === get_class($this)) {
1273         break;
1274       }
1275       // If the test method is implemented by a test class's parent then the
1276       // class name of $this will not be part of the backtrace.
1277       // In that case we process the backtrace until the caller is not a
1278       // subclass of $this and return the previous caller.
1279       if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) {
1280         // Return the last caller since that has to be the test class.
1281         $caller = $last_caller;
1282         break;
1283       }
1284       // Otherwise we have not reached our test class yet: save the last caller
1285       // and remove an element from to backtrace to process the next call.
1286       $last_caller = $caller;
1287       array_shift($backtrace);
1288     }
1289
1290     return $caller;
1291   }
1292
1293   /**
1294    * Checks for meta refresh tag and if found call drupalGet() recursively.
1295    *
1296    * This function looks for the http-equiv attribute to be set to "Refresh" and
1297    * is case-insensitive.
1298    *
1299    * @return string|false
1300    *   Either the new page content or FALSE.
1301    */
1302   protected function checkForMetaRefresh() {
1303     $refresh = $this->cssSelect('meta[http-equiv="Refresh"], meta[http-equiv="refresh"]');
1304     if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) {
1305       // Parse the content attribute of the meta tag for the format:
1306       // "[delay]: URL=[page_to_redirect_to]".
1307       if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]->getAttribute('content'), $match)) {
1308         $this->metaRefreshCount++;
1309         return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url'])));
1310       }
1311     }
1312     return FALSE;
1313   }
1314
1315 }