3 namespace Drupal\Tests;
5 use Behat\Mink\Driver\GoutteDriver;
6 use Behat\Mink\Element\Element;
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\Site\Settings;
18 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
19 use Drupal\Core\Test\FunctionalTestSetupTrait;
20 use Drupal\Core\Test\TestRunnerKernel;
21 use Drupal\Core\Test\TestSetupTrait;
23 use Drupal\Core\Utility\Error;
24 use Drupal\FunctionalTests\AssertLegacyTrait;
25 use Drupal\simpletest\AssertHelperTrait;
26 use Drupal\simpletest\ContentTypeCreationTrait;
27 use Drupal\simpletest\BlockCreationTrait;
28 use Drupal\simpletest\NodeCreationTrait;
29 use Drupal\simpletest\UserCreationTrait;
30 use Symfony\Component\CssSelector\CssSelectorConverter;
31 use Symfony\Component\HttpFoundation\Request;
32 use Psr\Http\Message\RequestInterface;
33 use Psr\Http\Message\ResponseInterface;
36 * Provides a test case for functional Drupal tests.
38 * Tests extending BrowserTestBase must exist in the
39 * Drupal\Tests\yourmodule\Functional namespace and live in the
40 * modules/yourmodule/tests/src/Functional directory.
44 abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase {
46 use FunctionalTestSetupTrait;
48 use AssertHelperTrait;
49 use BlockCreationTrait {
50 placeBlock as drupalPlaceBlock;
52 use AssertLegacyTrait;
53 use RandomGeneratorTrait;
55 use NodeCreationTrait {
56 getNodeByTitle as drupalGetNodeByTitle;
57 createNode as drupalCreateNode;
59 use ContentTypeCreationTrait {
60 createContentType as drupalCreateContentType;
63 use UserCreationTrait {
64 createRole as drupalCreateRole;
65 createUser as drupalCreateUser;
67 use XdebugRequestTrait;
70 * The database prefix of this test run.
74 protected $databasePrefix;
77 * Time limit in seconds for the test.
81 protected $timeLimit = 500;
84 * The translation file directory for the test environment.
86 * This is set in BrowserTestBase::prepareEnvironment().
90 protected $translationFilesDirectory;
93 * The config importer that can be used in a test.
95 * @var \Drupal\Core\Config\ConfigImporter
97 protected $configImporter;
102 * The test runner will merge the $modules lists from this class, the class
103 * it extends, and so on up the class hierarchy. It is not necessary to
104 * include modules in your list that a parent class has already declared.
108 * @see \Drupal\Tests\BrowserTestBase::installDrupal()
110 protected static $modules = [];
113 * The profile to install as a basis for testing.
117 protected $profile = 'testing';
120 * The current user logged in using the Mink controlled browser.
122 * @var \Drupal\user\UserInterface
124 protected $loggedInUser = FALSE;
127 * An array of custom translations suitable for drupal_rewrite_settings().
131 protected $customTranslations;
134 * Mink class for the default driver to use.
136 * Shoud be a fully qualified class name that implements
137 * Behat\Mink\Driver\DriverInterface.
139 * Value can be overridden using the environment variable MINK_DRIVER_CLASS.
143 protected $minkDefaultDriverClass = GoutteDriver::class;
146 * Mink default driver params.
148 * If it's an array its contents are used as constructor params when default
149 * Mink driver class is instantiated.
151 * Can be overridden using the environment variable MINK_DRIVER_ARGS. In this
152 * case that variable should be a JSON array, for example:
153 * '["firefox", null, "http://localhost:4444/wd/hub"]'.
158 protected $minkDefaultDriverArgs;
161 * Mink session manager.
163 * This will not be initialized if there was an error during the test setup.
165 * @var \Behat\Mink\Mink|null
172 * Browser tests are run in separate processes to prevent collisions between
173 * code that may be loaded by tests.
175 protected $runTestInSeparateProcess = TRUE;
180 protected $preserveGlobalState = FALSE;
183 * Class name for HTML output logging.
187 protected $htmlOutputClassName;
190 * Directory name for HTML output logging.
194 protected $htmlOutputDirectory;
197 * Counter storage for HTML output logging.
201 protected $htmlOutputCounterStorage;
204 * Counter for HTML output logging.
208 protected $htmlOutputCounter = 1;
211 * HTML output output enabled.
215 protected $htmlOutputEnabled = FALSE;
218 * The file name to write the list of URLs to.
220 * This file is read by the PHPUnit result printer.
224 * @see \Drupal\Tests\Listeners\HtmlOutputPrinter
226 protected $htmlOutputFile;
229 * HTML output test ID.
233 protected $htmlOutputTestId;
243 * The original array of shutdown function callbacks.
247 protected $originalShutdownCallbacks = [];
250 * The number of meta refresh redirects to follow, or NULL if unlimited.
254 protected $maximumMetaRefreshCount = NULL;
257 * The number of meta refresh redirects followed during ::drupalGet().
261 protected $metaRefreshCount = 0;
264 * Initializes Mink sessions.
266 protected function initMink() {
267 $driver = $this->getDefaultDriverInstance();
269 if ($driver instanceof GoutteDriver) {
270 // Turn off curl timeout. Having a timeout is not a problem in a normal
271 // test running, but it is a problem when debugging. Also, disable SSL
272 // peer verification so that testing under HTTPS always works.
273 /** @var \GuzzleHttp\Client $client */
274 $client = $this->container->get('http_client_factory')->fromOptions([
279 // Inject a Guzzle middleware to generate debug output for every request
280 // performed in the test.
281 $handler_stack = $client->getConfig('handler');
282 $handler_stack->push($this->getResponseLogHandler());
284 $driver->getClient()->setClient($client);
287 $selectors_handler = new SelectorsHandler([
288 'hidden_field_selector' => new HiddenFieldSelector()
290 $session = new Session($driver, $selectors_handler);
291 $this->mink = new Mink();
292 $this->mink->registerSession('default', $session);
293 $this->mink->setDefaultSessionName('default');
294 $this->registerSessions();
296 // According to the W3C WebDriver specification a cookie can only be set if
297 // the cookie domain is equal to the domain of the active document. When the
298 // browser starts up the active document is not our domain but 'about:blank'
299 // or similar. To be able to set our User-Agent and Xdebug cookies at the
300 // start of the test we now do a request to the front page so the active
301 // document matches the domain.
302 // @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
303 // @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
304 $session = $this->getSession();
305 $session->visit($this->baseUrl);
311 * Gets an instance of the default Mink driver.
313 * @return Behat\Mink\Driver\DriverInterface
314 * Instance of default Mink driver.
316 * @throws \InvalidArgumentException
317 * When provided default Mink driver class can't be instantiated.
319 protected function getDefaultDriverInstance() {
320 // Get default driver params from environment if availables.
321 if ($arg_json = getenv('MINK_DRIVER_ARGS')) {
322 $this->minkDefaultDriverArgs = json_decode($arg_json);
325 // Get and check default driver class from environment if availables.
326 if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) {
327 if (class_exists($minkDriverClass)) {
328 $this->minkDefaultDriverClass = $minkDriverClass;
331 throw new \InvalidArgumentException("Can't instantiate provided $minkDriverClass class by environment as default driver class.");
335 if (is_array($this->minkDefaultDriverArgs)) {
336 // Use ReflectionClass to instantiate class with received params.
337 $reflector = new \ReflectionClass($this->minkDefaultDriverClass);
338 $driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs);
341 $driver = new $this->minkDefaultDriverClass();
347 * Provides a Guzzle middleware handler to log every response received.
350 * The callable handler that will do the logging.
352 protected function getResponseLogHandler() {
353 return function (callable $handler) {
354 return function (RequestInterface $request, array $options) use ($handler) {
355 return $handler($request, $options)
356 ->then(function (ResponseInterface $response) use ($request) {
357 if ($this->htmlOutputEnabled) {
359 $caller = $this->getTestMethodCaller();
360 $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line'];
361 $html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri();
363 // On redirect responses (status code starting with '3') we need
364 // to remove the meta tag that would do a browser refresh. We
365 // don't want to redirect developers away when they look at the
366 // debug output file in their browser.
367 $body = $response->getBody();
368 $status_code = (string) $response->getStatusCode();
369 if ($status_code[0] === '3') {
370 $body = preg_replace('#<meta http-equiv="refresh" content=.+/>#', '', $body, 1);
372 $html_output .= '<hr />' . $body;
373 $html_output .= $this->formatHtmlOutputHeaders($response->getHeaders());
375 $this->htmlOutput($html_output);
384 * Registers additional Mink sessions.
386 * Tests wishing to use a different driver or change the default driver should
387 * override this method.
390 * // Register a new session that uses the MinkPonyDriver.
391 * $pony = new MinkPonyDriver();
392 * $session = new Session($pony);
393 * $this->mink->registerSession('pony', $session);
396 protected function registerSessions() {}
401 protected function setUp() {
405 // Get and set the domain of the environment we are running our test
407 $base_url = getenv('SIMPLETEST_BASE_URL');
409 throw new \Exception(
410 'You must provide a SIMPLETEST_BASE_URL environment variable to run some PHPUnit based functional tests.'
414 // Setup $_SERVER variable.
415 $parsed_url = parse_url($base_url);
416 $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
417 $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
418 $port = isset($parsed_url['port']) ? $parsed_url['port'] : 80;
420 $this->baseUrl = $base_url;
422 // If the passed URL schema is 'https' then setup the $_SERVER variables
423 // properly so that testing will run under HTTPS.
424 if ($parsed_url['scheme'] === 'https') {
425 $_SERVER['HTTPS'] = 'on';
427 $_SERVER['HTTP_HOST'] = $host;
428 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
429 $_SERVER['SERVER_ADDR'] = '127.0.0.1';
430 $_SERVER['SERVER_PORT'] = $port;
431 $_SERVER['SERVER_SOFTWARE'] = NULL;
432 $_SERVER['SERVER_NAME'] = 'localhost';
433 $_SERVER['REQUEST_URI'] = $path . '/';
434 $_SERVER['REQUEST_METHOD'] = 'GET';
435 $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
436 $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
437 $_SERVER['PHP_SELF'] = $path . '/index.php';
438 $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
440 // Install Drupal test site.
441 $this->prepareEnvironment();
442 $this->installDrupal();
445 $session = $this->initMink();
447 $cookies = $this->extractCookiesFromRequest(\Drupal::request());
448 foreach ($cookies as $cookie_name => $values) {
449 foreach ($values as $value) {
450 $session->setCookie($cookie_name, $value);
454 // Creates the directory to store browser output in if a file to write
455 // URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter.
456 $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE');
457 $this->htmlOutputEnabled = is_file($browser_output_file);
458 if ($this->htmlOutputEnabled) {
459 $this->htmlOutputFile = $browser_output_file;
460 $this->htmlOutputClassName = str_replace("\\", "_", get_called_class());
461 $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output';
462 if (file_prepare_directory($this->htmlOutputDirectory, FILE_CREATE_DIRECTORY) && !file_exists($this->htmlOutputDirectory . '/.htaccess')) {
463 file_put_contents($this->htmlOutputDirectory . '/.htaccess', "<IfModule mod_expires.c>\nExpiresActive Off\n</IfModule>\n");
465 $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter';
466 $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory);
467 if (is_file($this->htmlOutputCounterStorage)) {
468 $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1;
474 * Ensures test files are deletable within file_unmanaged_delete_recursive().
476 * Some tests chmod generated files to be read only. During
477 * BrowserTestBase::cleanupEnvironment() and other cleanup operations,
478 * these files need to get deleted too.
480 * @param string $path
483 public static function filePreDeleteCallback($path) {
484 // When the webserver runs with the same system user as phpunit, we can
485 // make read-only files writable again. If not, chmod will fail while the
486 // file deletion still works if file permissions have been configured
487 // correctly. Thus, we ignore any problems while running chmod.
492 * Clean up the Simpletest environment.
494 protected function cleanupEnvironment() {
495 // Remove all prefixed tables.
496 $original_connection_info = Database::getConnectionInfo('simpletest_original_default');
497 $original_prefix = $original_connection_info['default']['prefix']['default'];
498 $test_connection_info = Database::getConnectionInfo('default');
499 $test_prefix = $test_connection_info['default']['prefix']['default'];
500 if ($original_prefix != $test_prefix) {
501 $tables = Database::getConnection()->schema()->findTables('%');
502 foreach ($tables as $table) {
503 if (Database::getConnection()->schema()->dropTable($table)) {
504 unset($tables[$table]);
509 // Delete test site directory.
510 file_unmanaged_delete_recursive($this->siteDirectory, [$this, 'filePreDeleteCallback']);
516 protected function tearDown() {
519 // Destroy the testing kernel.
520 if (isset($this->kernel)) {
521 $this->cleanupEnvironment();
522 $this->kernel->shutdown();
525 // Ensure that internal logged in variable is reset.
526 $this->loggedInUser = FALSE;
529 $this->mink->stopSessions();
532 // Restore original shutdown callbacks.
533 if (function_exists('drupal_register_shutdown_function')) {
534 $callbacks = &drupal_register_shutdown_function();
535 $callbacks = $this->originalShutdownCallbacks;
540 * Returns Mink session.
542 * @param string $name
543 * (optional) Name of the session. Defaults to the active session.
545 * @return \Behat\Mink\Session
546 * The active Mink session object.
548 public function getSession($name = NULL) {
549 return $this->mink->getSession($name);
553 * Returns WebAssert object.
555 * @param string $name
556 * (optional) Name of the session. Defaults to the active session.
558 * @return \Drupal\Tests\WebAssert
559 * A new web-assert option for asserting the presence of elements with.
561 public function assertSession($name = NULL) {
562 return new WebAssert($this->getSession($name), $this->baseUrl);
566 * Prepare for a request to testing site.
568 * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is
569 * checked by drupal_valid_test_ua().
571 * @see drupal_valid_test_ua()
573 protected function prepareRequest() {
574 $session = $this->getSession();
575 $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix));
579 * Builds an a absolute URL from a system path or a URL object.
581 * @param string|\Drupal\Core\Url $path
582 * A system path or a URL.
583 * @param array $options
584 * Options to be passed to Url::fromUri().
587 * An absolute URL stsring.
589 protected function buildUrl($path, array $options = []) {
590 if ($path instanceof Url) {
591 $url_options = $path->getOptions();
592 $options = $url_options + $options;
593 $path->setOptions($options);
594 return $path->setAbsolute()->toString();
596 // The URL generator service is not necessarily available yet; e.g., in
597 // interactive installer tests.
598 elseif ($this->container->has('url_generator')) {
599 $force_internal = isset($options['external']) && $options['external'] == FALSE;
600 if (!$force_internal && UrlHelper::isExternal($path)) {
601 return Url::fromUri($path, $options)->toString();
604 $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path;
605 // Path processing is needed for language prefixing. Skip it when a
606 // path that may look like an external URL is being used as internal.
607 $options['path_processing'] = !$force_internal;
608 return Url::fromUri($uri, $options)
614 return $this->getAbsoluteUrl($path);
619 * Retrieves a Drupal path or an absolute path.
621 * @param string|\Drupal\Core\Url $path
622 * Drupal path or URL to load into Mink controlled browser.
623 * @param array $options
624 * (optional) Options to be forwarded to the url generator.
625 * @param string[] $headers
626 * An array containing additional HTTP request headers, the array keys are
627 * the header names and the array values the header values. This is useful
628 * to set for example the "Accept-Language" header for requesting the page
629 * in a different language. Note that not all headers are supported, for
630 * example the "Accept" header is always overridden by the browser. For
631 * testing REST APIs it is recommended to directly use an HTTP client such
635 * The retrieved HTML string, also available as $this->getRawContent()
637 protected function drupalGet($path, array $options = [], array $headers = []) {
638 $options['absolute'] = TRUE;
639 $url = $this->buildUrl($path, $options);
641 $session = $this->getSession();
643 $this->prepareRequest();
644 foreach ($headers as $header_name => $header_value) {
645 $session->setRequestHeader($header_name, $header_value);
648 $session->visit($url);
649 $out = $session->getPage()->getContent();
651 // Ensure that any changes to variables in the other thread are picked up.
652 $this->refreshVariables();
654 // Replace original page output with new output from redirected page(s).
655 if ($new = $this->checkForMetaRefresh()) {
657 // We are finished with all meta refresh redirects, so reset the counter.
658 $this->metaRefreshCount = 0;
661 // Log only for JavascriptTestBase tests because for Goutte we log with
662 // ::getResponseLogHandler.
663 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
664 $html_output = 'GET request to: ' . $url .
665 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
666 $html_output .= '<hr />' . $out;
667 $html_output .= $this->getHtmlOutputHeaders();
668 $this->htmlOutput($html_output);
675 * Takes a path and returns an absolute path.
677 * @param string $path
678 * A path from the Mink controlled browser content.
681 * The $path with $base_url prepended, if necessary.
683 protected function getAbsoluteUrl($path) {
684 global $base_url, $base_path;
686 $parts = parse_url($path);
687 if (empty($parts['host'])) {
688 // Ensure that we have a string (and no xpath object).
689 $path = (string) $path;
690 // Strip $base_path, if existent.
691 $length = strlen($base_path);
692 if (substr($path, 0, $length) === $base_path) {
693 $path = substr($path, $length);
695 // Ensure that we have an absolute path.
696 if (empty($path) || $path[0] !== '/') {
699 // Finally, prepend the $base_url.
700 $path = $base_url . $path;
706 * Logs in a user using the Mink controlled browser.
708 * If a user is already logged in, then the current user is logged out before
709 * logging in the specified user.
711 * Please note that neither the current user nor the passed-in user object is
712 * populated with data of the logged in user. If you need full access to the
713 * user object after logging in, it must be updated manually. If you also need
714 * access to the plain-text password of the user (set by drupalCreateUser()),
715 * e.g. to log in the same user again, then it must be re-assigned manually.
719 * $account = $this->drupalCreateUser(array());
720 * $this->drupalLogin($account);
721 * // Load real user object.
722 * $pass_raw = $account->passRaw;
723 * $account = User::load($account->id());
724 * $account->passRaw = $pass_raw;
727 * @param \Drupal\Core\Session\AccountInterface $account
728 * User object representing the user to log in.
730 * @see drupalCreateUser()
732 protected function drupalLogin(AccountInterface $account) {
733 if ($this->loggedInUser) {
734 $this->drupalLogout();
737 $this->drupalGet('user/login');
738 $this->assertSession()->statusCodeEquals(200);
740 'name' => $account->getUsername(),
741 'pass' => $account->passRaw,
744 // @see BrowserTestBase::drupalUserIsLoggedIn()
745 $account->sessionId = $this->getSession()->getCookie($this->getSessionName());
746 $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()]));
748 $this->loggedInUser = $account;
749 $this->container->get('current_user')->setAccount($account);
753 * Logs a user out of the Mink controlled browser and confirms.
755 * Confirms logout by checking the login page.
757 protected function drupalLogout() {
758 // Make a request to the logout page, and redirect to the user page, the
759 // idea being if you were properly logged out you should be seeing a login
761 $assert_session = $this->assertSession();
762 $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]);
763 $assert_session->statusCodeEquals(200);
764 $assert_session->fieldExists('name');
765 $assert_session->fieldExists('pass');
767 // @see BrowserTestBase::drupalUserIsLoggedIn()
768 unset($this->loggedInUser->sessionId);
769 $this->loggedInUser = FALSE;
770 $this->container->get('current_user')->setAccount(new AnonymousUserSession());
774 * Fills and submits a form.
777 * Field data in an associative array. Changes the current input fields
778 * (where possible) to the values indicated.
780 * A checkbox can be set to TRUE to be checked and should be set to FALSE to
782 * @param string $submit
783 * Value of the submit button whose click is to be emulated. For example,
784 * t('Save'). The processing of the request depends on this value. For
785 * example, a form may have one button with the value t('Save') and another
786 * button with the value t('Delete'), and execute different code depending
787 * on which one is clicked.
788 * @param string $form_html_id
789 * (optional) HTML ID of the form to be submitted. On some pages
790 * there are many identical forms, so just using the value of the submit
791 * button is not enough. For example: 'trigger-node-presave-assign-form'.
792 * Note that this is not the Drupal $form_id, but rather the HTML ID of the
793 * form, which is typically the same thing but with hyphens replacing the
796 protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
797 $assert_session = $this->assertSession();
800 if (isset($form_html_id)) {
801 $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']");
802 $submit_button = $assert_session->buttonExists($submit, $form);
803 $action = $form->getAttribute('action');
806 $submit_button = $assert_session->buttonExists($submit);
807 $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button);
808 $action = $form->getAttribute('action');
811 // Edit the form values.
812 foreach ($edit as $name => $value) {
813 $field = $assert_session->fieldExists($name, $form);
815 // Provide support for the values '1' and '0' for checkboxes instead of
817 // @todo Get rid of supporting 1/0 by converting all tests cases using
818 // this to boolean values.
819 $field_type = $field->getAttribute('type');
820 if ($field_type === 'checkbox') {
821 $value = (bool) $value;
824 $field->setValue($value);
828 $this->prepareRequest();
829 $submit_button->press();
831 // Ensure that any changes to variables in the other thread are picked up.
832 $this->refreshVariables();
834 // Check if there are any meta refresh redirects (like Batch API pages).
835 if ($this->checkForMetaRefresh()) {
836 // We are finished with all meta refresh redirects, so reset the counter.
837 $this->metaRefreshCount = 0;
840 // Log only for JavascriptTestBase tests because for Goutte we log with
841 // ::getResponseLogHandler.
842 if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
843 $out = $this->getSession()->getPage()->getContent();
844 $html_output = 'POST request to: ' . $action .
845 '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
846 $html_output .= '<hr />' . $out;
847 $html_output .= $this->getHtmlOutputHeaders();
848 $this->htmlOutput($html_output);
854 * Executes a form submission.
856 * It will be done as usual POST request with Mink.
858 * @param \Drupal\Core\Url|string $path
859 * Location of the post form. Either a Drupal path or an absolute path or
860 * NULL to post to the current page. For multi-stage forms you can set the
861 * path to NULL and have it post to the last received page. Example:
864 * // First step in form.
865 * $edit = array(...);
866 * $this->drupalPostForm('some_url', $edit, t('Save'));
868 * // Second step in form.
869 * $edit = array(...);
870 * $this->drupalPostForm(NULL, $edit, t('Save'));
873 * Field data in an associative array. Changes the current input fields
874 * (where possible) to the values indicated.
876 * When working with form tests, the keys for an $edit element should match
877 * the 'name' parameter of the HTML of the form. For example, the 'body'
878 * field for a node has the following HTML:
880 * <textarea id="edit-body-und-0-value" class="text-full form-textarea
881 * resize-vertical" placeholder="" cols="60" rows="9"
882 * name="body[0][value]"></textarea>
884 * When testing this field using an $edit parameter, the code becomes:
886 * $edit["body[0][value]"] = 'My test value';
889 * A checkbox can be set to TRUE to be checked and should be set to FALSE to
890 * be unchecked. Multiple select fields can be tested using 'name[]' and
891 * setting each of the desired values in an array:
894 * $edit['name[]'] = array('value1', 'value2');
896 * @todo change $edit to disallow NULL as a value for Drupal 9.
897 * https://www.drupal.org/node/2802401
898 * @param string $submit
899 * Value of the submit button whose click is to be emulated. For example,
900 * t('Save'). The processing of the request depends on this value. For
901 * example, a form may have one button with the value t('Save') and another
902 * button with the value t('Delete'), and execute different code depending
903 * on which one is clicked.
905 * This function can also be called to emulate an Ajax submission. In this
906 * case, this value needs to be an array with the following keys:
907 * - path: A path to submit the form values to for Ajax-specific processing.
908 * - triggering_element: If the value for the 'path' key is a generic Ajax
909 * processing path, this needs to be set to the name of the element. If
910 * the name doesn't identify the element uniquely, then this should
911 * instead be an array with a single key/value pair, corresponding to the
912 * element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder
913 * uses this to find the #ajax information for the element, including
914 * which specific callback to use for processing the request.
916 * This can also be set to NULL in order to emulate an Internet Explorer
917 * submission of a form with a single text field, and pressing ENTER in that
918 * textfield: under these conditions, no button information is added to the
920 * @param array $options
921 * Options to be forwarded to the url generator.
923 protected function drupalPostForm($path, $edit, $submit, array $options = []) {
924 if (is_object($submit)) {
925 // Cast MarkupInterface objects to string.
926 $submit = (string) $submit;
928 if ($edit === NULL) {
931 if (is_array($edit)) {
932 $edit = $this->castSafeStrings($edit);
936 $this->drupalGet($path, $options);
939 $this->submitForm($edit, $submit);
943 * Helper function to get the options of select field.
945 * @param \Behat\Mink\Element\NodeElement|string $select
946 * Name, ID, or Label of select field to assert.
947 * @param \Behat\Mink\Element\Element $container
948 * (optional) Container element to check against. Defaults to current page.
951 * Associative array of option keys and values.
953 protected function getOptions($select, Element $container = NULL) {
954 if (is_string($select)) {
955 $select = $this->assertSession()->selectExists($select, $container);
958 /* @var \Behat\Mink\Element\NodeElement $option */
959 foreach ($select->findAll('xpath', '//option') as $option) {
960 $label = $option->getText();
961 $value = $option->getAttribute('value') ?: $label;
962 $options[$value] = $label;
968 * Installs Drupal into the Simpletest site.
970 public function installDrupal() {
971 $this->initUserSession();
972 $this->prepareSettings();
974 $this->initSettings();
975 $container = $this->initKernel(\Drupal::request());
976 $this->initConfig($container);
977 $this->installModulesFromClassProperty($container);
982 * Returns the parameters that will be used when Simpletest installs Drupal.
984 * @see install_drupal()
985 * @see install_state_defaults()
987 protected function installParameters() {
988 $connection_info = Database::getConnectionInfo();
989 $driver = $connection_info['default']['driver'];
990 $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
991 unset($connection_info['default']['driver']);
992 unset($connection_info['default']['namespace']);
993 unset($connection_info['default']['pdo']);
994 unset($connection_info['default']['init_commands']);
996 'interactive' => FALSE,
998 'profile' => $this->profile,
1002 'install_settings_form' => [
1003 'driver' => $driver,
1004 $driver => $connection_info['default'],
1006 'install_configure_form' => [
1007 'site_name' => 'Drupal',
1008 'site_mail' => 'simpletest@example.com',
1010 'name' => $this->rootUser->name,
1011 'mail' => $this->rootUser->getEmail(),
1013 'pass1' => $this->rootUser->pass_raw,
1014 'pass2' => $this->rootUser->pass_raw,
1017 // form_type_checkboxes_value() requires NULL instead of FALSE values
1018 // for programmatic form submissions to disable a checkbox.
1019 'enable_update_status_module' => NULL,
1020 'enable_update_status_emails' => NULL,
1028 * Prepares the current environment for running the test.
1030 * Also sets up new resources for the testing environment, such as the public
1031 * filesystem and configuration directories.
1033 * This method is private as it must only be called once by
1034 * BrowserTestBase::setUp() (multiple invocations for the same test would have
1035 * unpredictable consequences) and it must not be callable or overridable by
1038 protected function prepareEnvironment() {
1039 // Bootstrap Drupal so we can use Drupal's built in functions.
1040 $this->classLoader = require __DIR__ . '/../../../../autoload.php';
1041 $request = Request::createFromGlobals();
1042 $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader);
1043 // TestRunnerKernel expects the working directory to be DRUPAL_ROOT.
1045 $kernel->prepareLegacyRequest($request);
1046 $this->prepareDatabasePrefix();
1048 $this->originalSite = $kernel->findSitePath($request);
1050 // Create test directory ahead of installation so fatal errors and debug
1051 // information can be logged during installation process.
1052 file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
1054 // Prepare filesystem directory paths.
1055 $this->publicFilesDirectory = $this->siteDirectory . '/files';
1056 $this->privateFilesDirectory = $this->siteDirectory . '/private';
1057 $this->tempFilesDirectory = $this->siteDirectory . '/temp';
1058 $this->translationFilesDirectory = $this->siteDirectory . '/translations';
1060 // Ensure the configImporter is refreshed for each test.
1061 $this->configImporter = NULL;
1063 // Unregister all custom stream wrappers of the parent site.
1064 $wrappers = \Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::ALL);
1065 foreach ($wrappers as $scheme => $info) {
1066 stream_wrapper_unregister($scheme);
1070 drupal_static_reset();
1072 // Ensure there is no service container.
1073 $this->container = NULL;
1074 \Drupal::unsetContainer();
1077 unset($GLOBALS['config_directories']);
1078 unset($GLOBALS['config']);
1079 unset($GLOBALS['conf']);
1081 // Log fatal errors.
1082 ini_set('log_errors', 1);
1083 ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
1085 // Change the database prefix.
1086 $this->changeDatabasePrefix();
1088 // After preparing the environment and changing the database prefix, we are
1089 // in a valid test environment.
1090 drupal_valid_test_ua($this->databasePrefix);
1094 // For performance, simply use the database prefix as hash salt.
1095 'hash_salt' => $this->databasePrefix,
1098 drupal_set_time_limit($this->timeLimit);
1100 // Save and clean the shutdown callbacks array because it is static cached
1101 // and will be changed by the test run. Otherwise it will contain callbacks
1102 // from both environments and the testing environment will try to call the
1103 // handlers defined by the original one.
1104 $callbacks = &drupal_register_shutdown_function();
1105 $this->originalShutdownCallbacks = $callbacks;
1110 * Returns whether a given user account is logged in.
1112 * @param \Drupal\Core\Session\AccountInterface $account
1113 * The user account object to check.
1116 * Return TRUE if the user is logged in, FALSE otherwise.
1118 protected function drupalUserIsLoggedIn(AccountInterface $account) {
1121 if (isset($account->sessionId)) {
1122 $session_handler = $this->container->get('session_handler.storage');
1123 $logged_in = (bool) $session_handler->read($account->sessionId);
1130 * Clicks the element with the given CSS selector.
1132 * @param string $css_selector
1133 * The CSS selector identifying the element to click.
1135 protected function click($css_selector) {
1136 $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector));
1140 * Prevents serializing any properties.
1142 * Browser tests are run in a separate process. To do this PHPUnit creates a
1143 * script to run the test. If it fails, the test result object will contain a
1144 * stack trace which includes the test object. It will attempt to serialize
1145 * it. Returning an empty array prevents it from serializing anything it
1151 * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist
1153 public function __sleep() {
1158 * Logs a HTML output message in a text file.
1160 * The link to the HTML output message will be printed by the results printer.
1162 * @param string $message
1163 * The HTML output message to be stored.
1165 * @see \Drupal\Tests\Listeners\VerbosePrinter::printResult()
1167 protected function htmlOutput($message) {
1168 if (!$this->htmlOutputEnabled) {
1171 $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;
1172 $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html';
1173 file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message);
1174 file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++);
1175 file_put_contents($this->htmlOutputFile, file_create_url('sites/simpletest/browser_output/' . $html_output_filename) . "\n", FILE_APPEND);
1179 * Returns headers in HTML output format.
1182 * HTML output headers.
1184 protected function getHtmlOutputHeaders() {
1185 return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders());
1189 * Formats HTTP headers as string for HTML output logging.
1191 * @param array[] $headers
1192 * Headers that should be formatted.
1195 * The formatted HTML string.
1197 protected function formatHtmlOutputHeaders(array $headers) {
1198 $flattened_headers = array_map(function($header) {
1199 if (is_array($header)) {
1200 return implode(';', array_map('trim', $header));
1206 return '<hr />Headers: <pre>' . Html::escape(var_export($flattened_headers, TRUE)) . '</pre>';
1210 * Translates a CSS expression to its XPath equivalent.
1212 * The search is relative to the root element (HTML tag normally) of the page.
1214 * @param string $selector
1215 * CSS selector to use in the search.
1217 * (optional) Enables HTML support. Disable it for XML documents.
1218 * @param string $prefix
1219 * (optional) The prefix for the XPath expression.
1222 * The equivalent XPath of a CSS expression.
1224 protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descendant-or-self::') {
1225 return (new CssSelectorConverter($html))->toXPath($selector, $prefix);
1229 * Searches elements using a CSS selector in the raw content.
1231 * The search is relative to the root element (HTML tag normally) of the page.
1233 * @param string $selector
1234 * CSS selector to use in the search.
1236 * @return \Behat\Mink\Element\NodeElement[]
1237 * The list of elements on the page that match the selector.
1239 protected function cssSelect($selector) {
1240 return $this->getSession()->getPage()->findAll('css', $selector);
1244 * Follows a link by complete name.
1246 * Will click the first link found with this link text.
1248 * If the link is discovered and clicked, the test passes. Fail otherwise.
1250 * @param string|\Drupal\Component\Render\MarkupInterface $label
1251 * Text between the anchor tags.
1253 * (optional) The index number for cases where multiple links have the same
1254 * text. Defaults to 0.
1256 protected function clickLink($label, $index = 0) {
1257 $label = (string) $label;
1258 $links = $this->getSession()->getPage()->findAll('named', ['link', $label]);
1259 $links[$index]->click();
1263 * Retrieves the plain-text content from the current page.
1265 protected function getTextContent() {
1266 return $this->getSession()->getPage()->getText();
1270 * Performs an xpath search on the contents of the internal browser.
1272 * The search is relative to the root element (HTML tag normally) of the page.
1274 * @param string $xpath
1275 * The xpath string to use in the search.
1276 * @param array $arguments
1277 * An array of arguments with keys in the form ':name' matching the
1278 * placeholders in the query. The values may be either strings or numeric
1281 * @return \Behat\Mink\Element\NodeElement[]
1282 * The list of elements matching the xpath expression.
1284 protected function xpath($xpath, array $arguments = []) {
1285 $xpath = $this->assertSession()->buildXPathQuery($xpath, $arguments);
1286 return $this->getSession()->getPage()->findAll('xpath', $xpath);
1290 * Configuration accessor for tests. Returns non-overridden configuration.
1292 * @param string $name
1293 * Configuration name.
1295 * @return \Drupal\Core\Config\Config
1296 * The configuration object with original configuration data.
1298 protected function config($name) {
1299 return $this->container->get('config.factory')->getEditable($name);
1303 * Returns all response headers.
1306 * The HTTP headers values.
1308 * @deprecated Scheduled for removal in Drupal 9.0.0.
1309 * Use $this->getSession()->getResponseHeaders() instead.
1311 protected function drupalGetHeaders() {
1312 return $this->getSession()->getResponseHeaders();
1316 * Gets the value of an HTTP response header.
1318 * If multiple requests were required to retrieve the page, only the headers
1319 * from the last request will be checked by default.
1321 * @param string $name
1322 * The name of the header to retrieve. Names are case-insensitive (see RFC
1323 * 2616 section 4.2).
1325 * @return string|null
1326 * The HTTP header value or NULL if not found.
1328 protected function drupalGetHeader($name) {
1329 return $this->getSession()->getResponseHeader($name);
1333 * Get the current URL from the browser.
1338 protected function getUrl() {
1339 return $this->getSession()->getCurrentUrl();
1343 * Gets the JavaScript drupalSettings variable for the currently-loaded page.
1346 * The JSON decoded drupalSettings value from the current page.
1348 protected function getDrupalSettings() {
1349 $html = $this->getSession()->getPage()->getHtml();
1350 if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) {
1351 return Json::decode($matches[1]);
1359 public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
1360 // Cast objects implementing MarkupInterface to string instead of
1361 // relying on PHP casting them to string depending on what they are being
1363 $expected = static::castSafeStrings($expected);
1364 $actual = static::castSafeStrings($actual);
1365 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
1369 * Retrieves the current calling line in the class under test.
1372 * An associative array with keys 'file', 'line' and 'function'.
1374 protected function getTestMethodCaller() {
1375 $backtrace = debug_backtrace();
1376 // Find the test class that has the test method.
1377 while ($caller = Error::getLastCaller($backtrace)) {
1378 if (isset($caller['class']) && $caller['class'] === get_class($this)) {
1381 // If the test method is implemented by a test class's parent then the
1382 // class name of $this will not be part of the backtrace.
1383 // In that case we process the backtrace until the caller is not a
1384 // subclass of $this and return the previous caller.
1385 if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) {
1386 // Return the last caller since that has to be the test class.
1387 $caller = $last_caller;
1390 // Otherwise we have not reached our test class yet: save the last caller
1391 // and remove an element from to backtrace to process the next call.
1392 $last_caller = $caller;
1393 array_shift($backtrace);
1400 * Checks for meta refresh tag and if found call drupalGet() recursively.
1402 * This function looks for the http-equiv attribute to be set to "Refresh" and
1403 * is case-sensitive.
1405 * @return string|false
1406 * Either the new page content or FALSE.
1408 protected function checkForMetaRefresh() {
1409 $refresh = $this->cssSelect('meta[http-equiv="Refresh"]');
1410 if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) {
1411 // Parse the content attribute of the meta tag for the format:
1412 // "[delay]: URL=[page_to_redirect_to]".
1413 if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]->getAttribute('content'), $match)) {
1414 $this->metaRefreshCount++;
1415 return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url'])));