You are here

abstract class BrowserTestBase in Zircon Profile 8

Same name and namespace in other branches
  1. 8.0 core/modules/simpletest/src/BrowserTestBase.php \Drupal\simpletest\BrowserTestBase

Provides a test case for functional Drupal tests.

Note that this class does not yet have feature parity with WebTestBase, so WebTestBase should be used where possible. In particular, this class does not yet have the following features:

Tests extending BrowserTestBase must exist in the Drupal\Tests\yourmodule\Functional namespace and live in the modules/yourmodule/Tests/Functional directory.

All BrowserTestBase tests must have two annotations to ensure process isolation:

  • @runTestsInSeparateProcesses
  • @preserveGlobalState disabled

Hierarchy

Expanded class hierarchy of BrowserTestBase

See also

\Drupal\simpletest\WebTestBase

Related topics

1 file declares its use of BrowserTestBase
BrowserTestBaseTest.php in core/modules/simpletest/tests/src/Functional/BrowserTestBaseTest.php
Contains \Drupal\Tests\simpletest\Functional\BrowserTestBaseTest.

File

core/modules/simpletest/src/BrowserTestBase.php, line 53
Contains \Drupal\simpletest\BrowserTestBase.

Namespace

Drupal\simpletest
View source
abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase {
  use RandomGeneratorTrait;
  use SessionTestTrait;

  /**
   * Class loader.
   *
   * @var object
   */
  protected $classLoader;

  /**
   * The site directory of this test run.
   *
   * @var string
   */
  protected $siteDirectory;

  /**
   * The database prefix of this test run.
   *
   * @var string
   */
  protected $databasePrefix;

  /**
   * The site directory of the original parent site.
   *
   * @var string
   */
  protected $originalSiteDirectory;

  /**
   * Time limit in seconds for the test.
   *
   * @var int
   */
  protected $timeLimit = 500;

  /**
   * The public file directory for the test environment.
   *
   * This is set in BrowserTestBase::prepareEnvironment().
   *
   * @var string
   */
  protected $publicFilesDirectory;

  /**
   * The private file directory for the test environment.
   *
   * This is set in BrowserTestBase::prepareEnvironment().
   *
   * @var string
   */
  protected $privateFilesDirectory;

  /**
   * The temp file directory for the test environment.
   *
   * This is set in BrowserTestBase::prepareEnvironment().
   *
   * @var string
   */
  protected $tempFilesDirectory;

  /**
   * The translation file directory for the test environment.
   *
   * This is set in BrowserTestBase::prepareEnvironment().
   *
   * @var string
   */
  protected $translationFilesDirectory;

  /**
   * The DrupalKernel instance used in the test.
   *
   * @var \Drupal\Core\DrupalKernel
   */
  protected $kernel;

  /**
   * The dependency injection container used in the test.
   *
   * @var \Symfony\Component\DependencyInjection\ContainerInterface
   */
  protected $container;

  /**
   * The config importer that can be used in a test.
   *
   * @var \Drupal\Core\Config\ConfigImporter
   */
  protected $configImporter;

  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
  protected $profile = 'testing';

  /**
   * The current user logged in using the Mink controlled browser.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $loggedInUser = FALSE;

  /**
   * The root user.
   *
   * @var \Drupal\Core\Session\UserSession
   */
  protected $rootUser;

  /**
   * The config directories used in this test.
   *
   * @var array
   */
  protected $configDirectories = array();

  /**
   * An array of custom translations suitable for drupal_rewrite_settings().
   *
   * @var array
   */
  protected $customTranslations;

  /*
   * Mink class for the default driver to use.
   *
   * Shoud be a fully qualified class name that implements
   * Behat\Mink\Driver\DriverInterface.
   *
   * Value can be overridden using the environment variable MINK_DRIVER_CLASS.
   *
   * @var string.
   */
  protected $minkDefaultDriverClass = '\\Behat\\Mink\\Driver\\GoutteDriver';

  /*
   * Mink default driver params.
   *
   * If it's an array its contents are used as constructor params when default
   * Mink driver class is instantiated.
   *
   * Can be overridden using the environment variable MINK_DRIVER_ARGS. In this
   * case that variable should be a JSON array, for example:
   * '["firefox", null, "http://localhost:4444/wd/hub"]'.
   *
   *
   * @var array
   */
  protected $minkDefaultDriverArgs;

  /**
   * Mink session manager.
   *
   * @var \Behat\Mink\Mink
   */
  protected $mink;

  /**
   * Initializes Mink sessions.
   */
  protected function initMink() {
    $driver = $this
      ->getDefaultDriverInstance();
    $session = new Session($driver);
    $this->mink = new Mink();
    $this->mink
      ->registerSession('default', $session);
    $this->mink
      ->setDefaultSessionName('default');
    $this
      ->registerSessions();
    return $session;
  }

  /**
   * Gets an instance of the default Mink driver.
   *
   * @return Behat\Mink\Driver\DriverInterface
   *   Instance of default Mink driver.
   *
   * @throws \InvalidArgumentException
   *   When provided default Mink driver class can't be instantiated.
   */
  protected function getDefaultDriverInstance() {

    // Get default driver params from environment if availables.
    if ($arg_json = getenv('MINK_DRIVER_ARGS')) {
      $this->minkDefaultDriverArgs = json_decode($arg_json);
    }

    // Get and check default driver class from environment if availables.
    if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) {
      if (class_exists($minkDriverClass)) {
        $this->minkDefaultDriverClass = $minkDriverClass;
      }
      else {
        throw new \InvalidArgumentException("Can't instantiate provided {$minkDriverClass} class by environment as default driver class.");
      }
    }
    if (is_array($this->minkDefaultDriverArgs)) {

      // Use ReflectionClass to instantiate class with received params.
      $reflector = new ReflectionClass($this->minkDefaultDriverClass);
      $driver = $reflector
        ->newInstanceArgs($this->minkDefaultDriverArgs);
    }
    else {
      $driver = new $this->minkDefaultDriverClass();
    }
    return $driver;
  }

  /**
   * Registers additional Mink sessions.
   *
   * Tests wishing to use a different driver or change the default driver should
   * override this method.
   *
   * @code
   *   // Register a new session that uses the MinkPonyDriver.
   *   $pony = new MinkPonyDriver();
   *   $session = new Session($pony);
   *   $this->mink->registerSession('pony', $session);
   * @endcode
   */
  protected function registerSessions() {
  }

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    global $base_url;
    parent::setUp();

    // Get and set the domain of the environment we are running our test
    // coverage against.
    $base_url = getenv('SIMPLETEST_BASE_URL');
    if (!$base_url) {
      $this
        ->markTestSkipped('You must provide a SIMPLETEST_BASE_URL environment variable to run some PHPUnit based functional tests.');
    }

    // Setup $_SERVER variable.
    $parsed_url = parse_url($base_url);
    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
    $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
    $port = isset($parsed_url['port']) ? $parsed_url['port'] : 80;

    // If the passed URL schema is 'https' then setup the $_SERVER variables
    // properly so that testing will run under HTTPS.
    if ($parsed_url['scheme'] === 'https') {
      $_SERVER['HTTPS'] = 'on';
    }
    $_SERVER['HTTP_HOST'] = $host;
    $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
    $_SERVER['SERVER_ADDR'] = '127.0.0.1';
    $_SERVER['SERVER_PORT'] = $port;
    $_SERVER['SERVER_SOFTWARE'] = NULL;
    $_SERVER['SERVER_NAME'] = 'localhost';
    $_SERVER['REQUEST_URI'] = $path . '/';
    $_SERVER['REQUEST_METHOD'] = 'GET';
    $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
    $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
    $_SERVER['PHP_SELF'] = $path . '/index.php';
    $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';

    // Install Drupal test site.
    $this
      ->prepareEnvironment();
    $this
      ->installDrupal();

    // Setup Mink.
    $session = $this
      ->initMink();

    // In order to debug web tests you need to either set a cookie, have the
    // Xdebug session in the URL or set an environment variable in case of CLI
    // requests. If the developer listens to connection when running tests, by
    // default the cookie is not forwarded to the client side, so you cannot
    // debug the code running on the test site. In order to make debuggers work
    // this bit of information is forwarded. Make sure that the debugger listens
    // to at least three external connections.
    $request = \Drupal::request();
    $cookie_params = $request->cookies;
    if ($cookie_params
      ->has('XDEBUG_SESSION')) {
      $session
        ->setCookie('XDEBUG_SESSION', $cookie_params
        ->get('XDEBUG_SESSION'));
    }

    // For CLI requests, the information is stored in $_SERVER.
    $server = $request->server;
    if ($server
      ->has('XDEBUG_CONFIG')) {

      // $_SERVER['XDEBUG_CONFIG'] has the form "key1=value1 key2=value2 ...".
      $pairs = explode(' ', $server
        ->get('XDEBUG_CONFIG'));
      foreach ($pairs as $pair) {
        list($key, $value) = explode('=', $pair);

        // Account for key-value pairs being separated by multiple spaces.
        if (trim($key) == 'idekey') {
          $session
            ->setCookie('XDEBUG_SESSION', trim($value));
        }
      }
    }
  }

  /**
   * Ensures test files are deletable within file_unmanaged_delete_recursive().
   *
   * Some tests chmod generated files to be read only. During
   * BrowserTestBase::cleanupEnvironment() and other cleanup operations,
   * these files need to get deleted too.
   *
   * @param string $path
   *   The file path.
   */
  public static function filePreDeleteCallback($path) {
    $success = @chmod($path, 0700);
    if (!$success) {
      trigger_error("Can not make {$path} writable whilst cleaning up test directory. The webserver and phpunit are probably not being run by the same user.");
    }
  }

  /**
   * Clean up the Simpletest environment.
   */
  protected function cleanupEnvironment() {

    // Remove all prefixed tables.
    $original_connection_info = Database::getConnectionInfo('simpletest_original_default');
    $original_prefix = $original_connection_info['default']['prefix']['default'];
    $test_connection_info = Database::getConnectionInfo('default');
    $test_prefix = $test_connection_info['default']['prefix']['default'];
    if ($original_prefix != $test_prefix) {
      $tables = Database::getConnection()
        ->schema()
        ->findTables('%');
      foreach ($tables as $table) {
        if (Database::getConnection()
          ->schema()
          ->dropTable($table)) {
          unset($tables[$table]);
        }
      }
    }

    // Delete test site directory.
    file_unmanaged_delete_recursive($this->siteDirectory, array(
      $this,
      'filePreDeleteCallback',
    ));
  }

  /**
   * {@inheritdoc}
   */
  protected function tearDown() {
    parent::tearDown();

    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this
        ->cleanupEnvironment();
      $this->kernel
        ->shutdown();
    }

    // Ensure that internal logged in variable is reset.
    $this->loggedInUser = FALSE;
    if ($this->mink) {
      $this->mink
        ->stopSessions();
    }
  }

  /**
   * Returns Mink session.
   *
   * @param string $name
   *   (optional) Name of the session. Defaults to the active session.
   *
   * @return \Behat\Mink\Session
   *   The active Mink session object.
   */
  public function getSession($name = NULL) {
    return $this->mink
      ->getSession($name);
  }

  /**
   * Returns WebAssert object.
   *
   * @param string $name
   *   (optional) Name of the session. Defaults to the active session.
   *
   * @return \Drupal\simpletest\WebAssert
   *   A new web-assert option for asserting the presence of elements with.
   */
  public function assertSession($name = NULL) {
    return new WebAssert($this
      ->getSession($name));
  }

  /**
   * Prepare for a request to testing site.
   *
   * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is
   * checked by drupal_valid_test_ua().
   *
   * @see drupal_valid_test_ua()
   */
  protected function prepareRequest() {
    $session = $this
      ->getSession();
    $session
      ->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix));
  }

  /**
   * Retrieves a Drupal path or an absolute path.
   *
   * @param string $path
   *   Drupal path or URL to load into Mink controlled browser.
   * @param array $options
   *   (optional) Options to be forwarded to the url generator.
   *
   * @return string
   *   The retrieved HTML string, also available as $this->getRawContent()
   */
  protected function drupalGet($path, array $options = array()) {
    $options['absolute'] = TRUE;

    // The URL generator service is not necessarily available yet; e.g., in
    // interactive installer tests.
    if ($this->container
      ->has('url_generator')) {
      if (UrlHelper::isExternal($path)) {
        $url = Url::fromUri($path, $options)
          ->toString();
      }
      else {

        // This is needed for language prefixing.
        $options['path_processing'] = TRUE;
        $url = Url::fromUri('base:/' . $path, $options)
          ->toString();
      }
    }
    else {
      $url = $this
        ->getAbsoluteUrl($path);
    }
    $session = $this
      ->getSession();
    $this
      ->prepareRequest();
    $session
      ->visit($url);
    $out = $session
      ->getPage()
      ->getContent();

    // Ensure that any changes to variables in the other thread are picked up.
    $this
      ->refreshVariables();
    return $out;
  }

  /**
   * Takes a path and returns an absolute path.
   *
   * @param string $path
   *   A path from the Mink controlled browser content.
   *
   * @return string
   *   The $path with $base_url prepended, if necessary.
   */
  protected function getAbsoluteUrl($path) {
    global $base_url, $base_path;
    $parts = parse_url($path);
    if (empty($parts['host'])) {

      // Ensure that we have a string (and no xpath object).
      $path = (string) $path;

      // Strip $base_path, if existent.
      $length = strlen($base_path);
      if (substr($path, 0, $length) === $base_path) {
        $path = substr($path, $length);
      }

      // Ensure that we have an absolute path.
      if (empty($path) || $path[0] !== '/') {
        $path = '/' . $path;
      }

      // Finally, prepend the $base_url.
      $path = $base_url . $path;
    }
    return $path;
  }

  /**
   * Creates a user with a given set of permissions.
   *
   * @param array $permissions
   *   (optional) Array of permission names to assign to user. Note that the
   *   user always has the default permissions derived from the
   *   "authenticated users" role.
   * @param string $name
   *   (optional) The user name.
   *
   * @return \Drupal\user\Entity\User|false
   *   A fully loaded user object with passRaw property, or FALSE if account
   *   creation fails.
   */
  protected function drupalCreateUser(array $permissions = array(), $name = NULL) {

    // Create a role with the given permission set, if any.
    $rid = FALSE;
    if ($permissions) {
      $rid = $this
        ->drupalCreateRole($permissions);
      if (!$rid) {
        return FALSE;
      }
    }

    // Create a user assigned to that role.
    $edit = array();
    $edit['name'] = !empty($name) ? $name : $this
      ->randomMachineName();
    $edit['mail'] = $edit['name'] . '@example.com';
    $edit['pass'] = user_password();
    $edit['status'] = 1;
    if ($rid) {
      $edit['roles'] = array(
        $rid,
      );
    }
    $account = entity_create('user', $edit);
    $account
      ->save();
    $this
      ->assertNotNull($account
      ->id(), SafeMarkup::format('User created with name %name and pass %pass', array(
      '%name' => $edit['name'],
      '%pass' => $edit['pass'],
    )));
    if (!$account
      ->id()) {
      return FALSE;
    }

    // Add the raw password so that we can log in as this user.
    $account->passRaw = $edit['pass'];
    return $account;
  }

  /**
   * Creates a role with specified permissions.
   *
   * @param array $permissions
   *   Array of permission names to assign to role.
   * @param string $rid
   *   (optional) The role ID (machine name). Defaults to a random name.
   * @param string $name
   *   (optional) The label for the role. Defaults to a random string.
   * @param int $weight
   *   (optional) The weight for the role. Defaults NULL so that entity_create()
   *   sets the weight to maximum + 1.
   *
   * @return string
   *   Role ID of newly created role, or FALSE if role creation failed.
   */
  protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {

    // Generate a random, lowercase machine name if none was passed.
    if (!isset($rid)) {
      $rid = strtolower($this
        ->randomMachineName(8));
    }

    // Generate a random label.
    if (!isset($name)) {

      // In the role UI role names are trimmed and random string can start or
      // end with a space.
      $name = trim($this
        ->randomString(8));
    }

    // Check the all the permissions strings are valid.
    if (!$this
      ->checkPermissions($permissions)) {
      return FALSE;
    }

    // Create new role.

    /* @var \Drupal\user\RoleInterface $role */
    $role = entity_create('user_role', array(
      'id' => $rid,
      'label' => $name,
    ));
    if (!is_null($weight)) {
      $role
        ->set('weight', $weight);
    }
    $result = $role
      ->save();
    $this
      ->assertSame($result, SAVED_NEW, SafeMarkup::format('Created role ID @rid with name @name.', array(
      '@name' => var_export($role
        ->label(), TRUE),
      '@rid' => var_export($role
        ->id(), TRUE),
    )));
    if ($result === SAVED_NEW) {

      // Grant the specified permissions to the role, if any.
      if (!empty($permissions)) {
        user_role_grant_permissions($role
          ->id(), $permissions);
        $assigned_permissions = entity_load('user_role', $role
          ->id())
          ->getPermissions();
        $missing_permissions = array_diff($permissions, $assigned_permissions);
        if ($missing_permissions) {
          $this
            ->fail(SafeMarkup::format('Failed to create permissions: @perms', array(
            '@perms' => implode(', ', $missing_permissions),
          )));
        }
      }
      return $role
        ->id();
    }
    return FALSE;
  }

  /**
   * Checks whether a given list of permission names is valid.
   *
   * @param array $permissions
   *   The permission names to check.
   *
   * @return bool
   *   TRUE if the permissions are valid, FALSE otherwise.
   */
  protected function checkPermissions(array $permissions) {
    $available = array_keys(\Drupal::service('user.permissions')
      ->getPermissions());
    $valid = TRUE;
    foreach ($permissions as $permission) {
      if (!in_array($permission, $available)) {
        $this
          ->fail(SafeMarkup::format('Invalid permission %permission.', array(
          '%permission' => $permission,
        )));
        $valid = FALSE;
      }
    }
    return $valid;
  }

  /**
   * Logs in a user using the Mink controlled browser.
   *
   * If a user is already logged in, then the current user is logged out before
   * logging in the specified user.
   *
   * Please note that neither the current user nor the passed-in user object is
   * populated with data of the logged in user. If you need full access to the
   * user object after logging in, it must be updated manually. If you also need
   * access to the plain-text password of the user (set by drupalCreateUser()),
   * e.g. to log in the same user again, then it must be re-assigned manually.
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->passRaw;
   *   $account = User::load($account->id());
   *   $account->passRaw = $pass_raw;
   * @endcode
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   User object representing the user to log in.
   *
   * @see drupalCreateUser()
   */
  protected function drupalLogin(AccountInterface $account) {
    if ($this->loggedInUser) {
      $this
        ->drupalLogout();
    }
    $this
      ->drupalGet('user');
    $this
      ->assertSession()
      ->statusCodeEquals(200);
    $this
      ->submitForm(array(
      'name' => $account
        ->getUsername(),
      'pass' => $account->passRaw,
    ), t('Log in'));

    // @see BrowserTestBase::drupalUserIsLoggedIn()
    $account->sessionId = $this
      ->getSession()
      ->getCookie($this
      ->getSessionName());
    $this
      ->assertTrue($this
      ->drupalUserIsLoggedIn($account), SafeMarkup::format('User %name successfully logged in.', array(
      'name' => $account
        ->getUsername(),
    )));
    $this->loggedInUser = $account;
    $this->container
      ->get('current_user')
      ->setAccount($account);
  }

  /**
   * Logs a user out of the Mink controlled browser and confirms.
   *
   * Confirms logout by checking the login page.
   */
  protected function drupalLogout() {

    // Make a request to the logout page, and redirect to the user page, the
    // idea being if you were properly logged out you should be seeing a login
    // screen.
    $assert_session = $this
      ->assertSession();
    $this
      ->drupalGet('user/logout', array(
      'query' => array(
        'destination' => 'user',
      ),
    ));
    $assert_session
      ->statusCodeEquals(200);
    $assert_session
      ->fieldExists('name');
    $assert_session
      ->fieldExists('pass');

    // @see BrowserTestBase::drupalUserIsLoggedIn()
    unset($this->loggedInUser->sessionId);
    $this->loggedInUser = FALSE;
    $this->container
      ->get('current_user')
      ->setAccount(new AnonymousUserSession());
  }

  /**
   * Fills and submits a form.
   *
   * @param array $edit
   *   Field data in an associative array. Changes the current input fields
   *   (where possible) to the values indicated.
   *
   *   A checkbox can be set to TRUE to be checked and should be set to FALSE to
   *   be unchecked.
   * @param string $submit
   *   Value of the submit button whose click is to be emulated. For example,
   *   t('Save'). The processing of the request depends on this value. For
   *   example, a form may have one button with the value t('Save') and another
   *   button with the value t('Delete'), and execute different code depending
   *   on which one is clicked.
   * @param string $form_html_id
   *   (optional) HTML ID of the form to be submitted. On some pages
   *   there are many identical forms, so just using the value of the submit
   *   button is not enough. For example: 'trigger-node-presave-assign-form'.
   *   Note that this is not the Drupal $form_id, but rather the HTML ID of the
   *   form, which is typically the same thing but with hyphens replacing the
   *   underscores.
   */
  protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
    $assert_session = $this
      ->assertSession();

    // Get the form.
    if (isset($form_html_id)) {
      $form = $assert_session
        ->elementExists('xpath', "//form[@id='{$form_html_id}']");
      $submit_button = $assert_session
        ->buttonExists($submit, $form);
    }
    else {
      $submit_button = $assert_session
        ->buttonExists($submit);
      $form = $assert_session
        ->elementExists('xpath', './ancestor::form', $submit_button);
    }

    // Edit the form values.
    foreach ($edit as $name => $value) {
      $field = $assert_session
        ->fieldExists($name, $form);
      $field
        ->setValue($value);
    }

    // Submit form.
    $this
      ->prepareRequest();
    $submit_button
      ->press();

    // Ensure that any changes to variables in the other thread are picked up.
    $this
      ->refreshVariables();
  }

  /**
   * Helper function to get the options of select field.
   *
   * @param \Behat\Mink\Element\NodeElement|string $select
   *   Name, ID, or Label of select field to assert.
   * @param \Behat\Mink\Element\Element $container
   *   (optional) Container element to check against. Defaults to current page.
   *
   * @return array
   *   Associative array of option keys and values.
   */
  protected function getOptions($select, Element $container = NULL) {
    if (is_string($select)) {
      $select = $this
        ->assertSession()
        ->selectExists($select, $container);
    }
    $options = [];

    /* @var \Behat\Mink\Element\NodeElement $option */
    foreach ($select
      ->findAll('xpath', '//option') as $option) {
      $label = $option
        ->getText();
      $value = $option
        ->getAttribute('value') ?: $label;
      $options[$value] = $label;
    }
    return $options;
  }

  /**
   * Override to use Mink exceptions.
   *
   * @return mixed
   *   Either a test result or NULL.
   *
   * @throws \PHPUnit_Framework_AssertionFailedError
   *   When exception was thrown inside the test.
   */
  protected function runTest() {
    try {
      return parent::runTest();
    } catch (Exception $e) {
      throw new \PHPUnit_Framework_AssertionFailedError($e
        ->getMessage());
    }
  }

  /**
   * Installs Drupal into the Simpletest site.
   */
  public function installDrupal() {

    // Define information about the user 1 account.
    $this->rootUser = new UserSession(array(
      'uid' => 1,
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'passRaw' => $this
        ->randomMachineName(),
    ));

    // The child site derives its session name from the database prefix when
    // running web tests.
    $this
      ->generateSessionName($this->databasePrefix);

    // Get parameters for install_drupal() before removing global variables.
    $parameters = $this
      ->installParameters();

    // Prepare installer settings that are not install_drupal() parameters.
    // Copy and prepare an actual settings.php, so as to resemble a regular
    // installation.
    // Not using File API; a potential error must trigger a PHP warning.
    $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php');

    // All file system paths are created by System module during installation.
    // @see system_requirements()
    // @see TestBase::prepareEnvironment()
    $settings['settings']['file_public_path'] = (object) array(
      'value' => $this->publicFilesDirectory,
      'required' => TRUE,
    );
    $this
      ->writeSettings($settings);

    // Allow for test-specific overrides.
    $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSiteDirectory . '/settings.testing.php';
    if (file_exists($settings_testing_file)) {

      // Copy the testing-specific settings.php overrides in place.
      copy($settings_testing_file, $directory . '/settings.testing.php');

      // Add the name of the testing class to settings.php and include the
      // testing specific overrides.
      file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) . "';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' . "\n", FILE_APPEND);
    }
    $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSiteDirectory . '/testing.services.yml';
    if (file_exists($settings_services_file)) {

      // Copy the testing-specific service overrides in place.
      copy($settings_services_file, $directory . '/services.yml');
    }

    // Since Drupal is bootstrapped already, install_begin_request() will not
    // bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to
    // reload the newly written custom settings.php manually.
    Settings::initialize(DRUPAL_ROOT, $directory, $this->classLoader);

    // Execute the non-interactive installer.
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
    install_drupal($parameters);

    // Import new settings.php written by the installer.
    Settings::initialize(DRUPAL_ROOT, $directory, $this->classLoader);
    foreach ($GLOBALS['config_directories'] as $type => $path) {
      $this->configDirectories[$type] = $path;
    }

    // After writing settings.php, the installer removes write permissions from
    // the site directory. To allow drupal_generate_test_ua() to write a file
    // containing the private key for drupal_valid_test_ua(), the site directory
    // has to be writable.
    // TestBase::restoreEnvironment() will delete the entire site directory. Not
    // using File API; a potential error must trigger a PHP warning.
    chmod($directory, 0777);
    $request = \Drupal::request();
    $this->kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod', TRUE);
    $this->kernel
      ->prepareLegacyRequest($request);

    // Force the container to be built from scratch instead of loaded from the
    // disk. This forces us to not accidentally load the parent site.
    $container = $this->kernel
      ->rebuildContainer();
    $config = $container
      ->get('config.factory');

    // Manually create and configure private and temporary files directories.
    // While these could be preset/enforced in settings.php like the public
    // files directory above, some tests expect them to be configurable in the
    // UI. If declared in settings.php, they would no longer be configurable.
    file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY);
    file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY);
    $config
      ->getEditable('system.file')
      ->set('path.private', $this->privateFilesDirectory)
      ->set('path.temporary', $this->tempFilesDirectory)
      ->save();

    // Manually configure the test mail collector implementation to prevent
    // tests from sending out emails and collect them in state instead.
    // While this should be enforced via settings.php prior to installation,
    // some tests expect to be able to test mail system implementations.
    $config
      ->getEditable('system.mail')
      ->set('interface.default', 'test_mail_collector')
      ->save();

    // By default, verbosely display all errors and disable all production
    // environment optimizations for all tests to avoid needless overhead and
    // ensure a sane default experience for test authors.
    // @see https://www.drupal.org/node/2259167
    $config
      ->getEditable('system.logging')
      ->set('error_level', 'verbose')
      ->save();
    $config
      ->getEditable('system.performance')
      ->set('css.preprocess', FALSE)
      ->set('js.preprocess', FALSE)
      ->save();

    // Collect modules to install.
    $class = get_class($this);
    $modules = array();
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
    if ($modules) {
      $modules = array_unique($modules);
      $success = $container
        ->get('module_installer')
        ->install($modules, TRUE);
      $this
        ->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', array(
        '%modules' => implode(', ', $modules),
      )));
      $this
        ->rebuildContainer();
    }

    // Reset/rebuild all data structures after enabling the modules, primarily
    // to synchronize all data structures and caches between the test runner and
    // the child site.
    // Affects e.g. StreamWrapperManagerInterface::getWrappers().
    // @see \Drupal\Core\DrupalKernel::bootCode()
    // @todo Test-specific setUp() methods may set up further fixtures; find a
    //   way to execute this after setUp() is done, or to eliminate it entirely.
    $this
      ->resetAll();
    $this->kernel
      ->prepareLegacyRequest($request);
  }

  /**
   * Returns the parameters that will be used when Simpletest installs Drupal.
   *
   * @see install_drupal()
   * @see install_state_defaults()
   */
  protected function installParameters() {
    $connection_info = Database::getConnectionInfo();
    $driver = $connection_info['default']['driver'];
    $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
    unset($connection_info['default']['driver']);
    unset($connection_info['default']['namespace']);
    unset($connection_info['default']['pdo']);
    unset($connection_info['default']['init_commands']);
    $parameters = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
        'install_settings_form' => array(
          'driver' => $driver,
          $driver => $connection_info['default'],
        ),
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
            'name' => $this->rootUser->name,
            'mail' => $this->rootUser
              ->getEmail(),
            'pass' => array(
              'pass1' => $this->rootUser->passRaw,
              'pass2' => $this->rootUser->passRaw,
            ),
          ),
          // form_type_checkboxes_value() requires NULL instead of FALSE values
          // for programmatic form submissions to disable a checkbox.
          'update_status_module' => array(
            1 => NULL,
            2 => NULL,
          ),
        ),
      ),
    );
    return $parameters;
  }

  /**
   * Generates a database prefix for running tests.
   *
   * The database prefix is used by prepareEnvironment() to setup a public files
   * directory for the test to be run, which also contains the PHP error log,
   * which is written to in case of a fatal error. Since that directory is based
   * on the database prefix, all tests (even unit tests) need to have one, in
   * order to access and read the error log.
   *
   * The generated database table prefix is used for the Drupal installation
   * being performed for the test. It is also used by the cookie value of
   * SIMPLETEST_USER_AGENT by the Mink controlled browser. During early Drupal
   * bootstrap, the cookie is parsed, and if it matches, all database queries
   * use the database table prefix that has been generated here.
   *
   * @see drupal_valid_test_ua()
   * @see BrowserTestBase::prepareEnvironment()
   */
  private function prepareDatabasePrefix() {

    // Ensure that the generated test site directory does not exist already,
    // which may happen with a large amount of concurrent threads and
    // long-running tests.
    do {
      $suffix = mt_rand(100000, 999999);
      $this->siteDirectory = 'sites/simpletest/' . $suffix;
      $this->databasePrefix = 'simpletest' . $suffix;
    } while (is_dir(DRUPAL_ROOT . '/' . $this->siteDirectory));
  }

  /**
   * Changes the database connection to the prefixed one.
   *
   * @see BrowserTestBase::prepareEnvironment()
   */
  private function changeDatabasePrefix() {
    if (empty($this->databasePrefix)) {
      $this
        ->prepareDatabasePrefix();
    }

    // If the test is run with argument dburl then use it.
    $db_url = getenv('SIMPLETEST_DB');
    if (!empty($db_url)) {
      $database = Database::convertDbUrlToConnectionInfo($db_url, DRUPAL_ROOT);
      Database::addConnectionInfo('default', 'default', $database);
    }

    // Clone the current connection and replace the current prefix.
    $connection_info = Database::getConnectionInfo('default');
    if (is_null($connection_info)) {
      throw new \InvalidArgumentException('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh.');
    }
    else {
      Database::renameConnection('default', 'simpletest_original_default');
      foreach ($connection_info as $target => $value) {

        // Replace the full table prefix definition to ensure that no table
        // prefixes of the test runner leak into the test.
        $connection_info[$target]['prefix'] = array(
          'default' => $value['prefix']['default'] . $this->databasePrefix,
        );
      }
      Database::addConnectionInfo('default', 'default', $connection_info['default']);
    }
  }

  /**
   * Prepares the current environment for running the test.
   *
   * Also sets up new resources for the testing environment, such as the public
   * filesystem and configuration directories.
   *
   * This method is private as it must only be called once by
   * BrowserTestBase::setUp() (multiple invocations for the same test would have
   * unpredictable consequences) and it must not be callable or overridable by
   * test classes.
   */
  protected function prepareEnvironment() {

    // Bootstrap Drupal so we can use Drupal's built in functions.
    $this->classLoader = (require __DIR__ . '/../../../../autoload.php');
    $request = Request::createFromGlobals();
    $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader);

    // TestRunnerKernel expects the working directory to be DRUPAL_ROOT.
    chdir(DRUPAL_ROOT);
    $kernel
      ->prepareLegacyRequest($request);
    $this
      ->prepareDatabasePrefix();
    $this->originalSiteDirectory = $kernel
      ->findSitePath($request);

    // Create test directory ahead of installation so fatal errors and debug
    // information can be logged during installation process.
    file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);

    // Prepare filesystem directory paths.
    $this->publicFilesDirectory = $this->siteDirectory . '/files';
    $this->privateFilesDirectory = $this->siteDirectory . '/private';
    $this->tempFilesDirectory = $this->siteDirectory . '/temp';
    $this->translationFilesDirectory = $this->siteDirectory . '/translations';

    // Ensure the configImporter is refreshed for each test.
    $this->configImporter = NULL;

    // Unregister all custom stream wrappers of the parent site.
    $wrappers = \Drupal::service('stream_wrapper_manager')
      ->getWrappers(StreamWrapperInterface::ALL);
    foreach ($wrappers as $scheme => $info) {
      stream_wrapper_unregister($scheme);
    }

    // Reset statics.
    drupal_static_reset();

    // Ensure there is no service container.
    $this->container = NULL;
    \Drupal::unsetContainer();

    // Unset globals.
    unset($GLOBALS['config_directories']);
    unset($GLOBALS['config']);
    unset($GLOBALS['conf']);

    // Log fatal errors.
    ini_set('log_errors', 1);
    ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');

    // Change the database prefix.
    $this
      ->changeDatabasePrefix();

    // After preparing the environment and changing the database prefix, we are
    // in a valid test environment.
    drupal_valid_test_ua($this->databasePrefix);

    // Reset settings.
    new Settings(array(
      // For performance, simply use the database prefix as hash salt.
      'hash_salt' => $this->databasePrefix,
    ));
    drupal_set_time_limit($this->timeLimit);
  }

  /**
   * Returns the database connection to the site running Simpletest.
   *
   * @return \Drupal\Core\Database\Connection
   *   The database connection to use for inserting assertions.
   */
  public static function getDatabaseConnection() {

    // Check whether there is a test runner connection.
    // @see run-tests.sh
    try {
      $connection = Database::getConnection('default', 'test-runner');
    } catch (ConnectionNotDefinedException $e) {

      // Check whether there is a backup of the original default connection.
      // @see BrowserTestBase::prepareEnvironment()
      try {
        $connection = Database::getConnection('default', 'simpletest_original_default');
      } catch (ConnectionNotDefinedException $e) {

        // If BrowserTestBase::prepareEnvironment() or
        // BrowserTestBase::restoreEnvironment() failed, the test-specific
        // database connection does not exist yet/anymore, so fall back to the
        // default of the (UI) test runner.
        $connection = Database::getConnection('default', 'default');
      }
    }
    return $connection;
  }

  /**
   * Rewrites the settings.php file of the test site.
   *
   * @param array $settings
   *   An array of settings to write out, in the format expected by
   *   drupal_rewrite_settings().
   *
   * @see drupal_rewrite_settings()
   */
  protected function writeSettings(array $settings) {
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
    $filename = $this->siteDirectory . '/settings.php';

    // system_requirements() removes write permissions from settings.php
    // whenever it is invoked.
    // Not using File API; a potential error must trigger a PHP warning.
    chmod($filename, 0666);
    drupal_rewrite_settings($settings, $filename);
  }

  /**
   * Rebuilds \Drupal::getContainer().
   *
   * Use this to build a new kernel and service container. For example, when the
   * list of enabled modules is changed via the Mink controlled browser, in
   * which case the test process still contains an old kernel and service
   * container with an old module list.
   *
   * @see BrowserTestBase::prepareEnvironment()
   * @see BrowserTestBase::restoreEnvironment()
   *
   * @todo Fix https://www.drupal.org/node/2021959 so that module enable/disable
   *   changes are immediately reflected in \Drupal::getContainer(). Until then,
   *   tests can invoke this workaround when requiring services from newly
   *   enabled modules to be immediately available in the same request.
   */
  protected function rebuildContainer() {

    // Rebuild the kernel and bring it back to a fully bootstrapped state.
    $this->container = $this->kernel
      ->rebuildContainer();

    // Make sure the url generator has a request object, otherwise calls to
    // $this->drupalGet() will fail.
    $this
      ->prepareRequestForGenerator();
  }

  /**
   * Creates a mock request and sets it on the generator.
   *
   * This is used to manipulate how the generator generates paths during tests.
   * It also ensures that calls to $this->drupalGet() will work when running
   * from run-tests.sh because the url generator no longer looks at the global
   * variables that are set there but relies on getting this information from a
   * request object.
   *
   * @param bool $clean_urls
   *   Whether to mock the request using clean urls.
   * @param array $override_server_vars
   *   An array of server variables to override.
   *
   * @return Request
   *   The mocked request object.
   */
  protected function prepareRequestForGenerator($clean_urls = TRUE, $override_server_vars = array()) {
    $request = Request::createFromGlobals();
    $server = $request->server
      ->all();
    if (basename($server['SCRIPT_FILENAME']) != basename($server['SCRIPT_NAME'])) {

      // We need this for when the test is executed by run-tests.sh.
      // @todo Remove this once run-tests.sh has been converted to use a Request
      //   object.
      $cwd = getcwd();
      $server['SCRIPT_FILENAME'] = $cwd . '/' . basename($server['SCRIPT_NAME']);
      $base_path = rtrim($server['REQUEST_URI'], '/');
    }
    else {
      $base_path = $request
        ->getBasePath();
    }
    if ($clean_urls) {
      $request_path = $base_path ? $base_path . '/user' : 'user';
    }
    else {
      $request_path = $base_path ? $base_path . '/index.php/user' : '/index.php/user';
    }
    $server = array_merge($server, $override_server_vars);
    $request = Request::create($request_path, 'GET', array(), array(), array(), $server);

    // Ensure the request time is REQUEST_TIME to ensure that API calls
    // in the test use the right timestamp.
    $request->server
      ->set('REQUEST_TIME', REQUEST_TIME);
    $this->container
      ->get('request_stack')
      ->push($request);

    // The request context is normally set by the router_listener from within
    // its KernelEvents::REQUEST listener. In the Simpletest parent site this
    // event is not fired, therefore it is necessary to updated the request
    // context manually here.
    $this->container
      ->get('router.request_context')
      ->fromRequest($request);
    return $request;
  }

  /**
   * Resets all data structures after having enabled new modules.
   *
   * This method is called by \Drupal\simpletest\BrowserTestBase::setUp() after
   * enabling the requested modules. It must be called again when additional
   * modules are enabled later.
   */
  protected function resetAll() {

    // Clear all database and static caches and rebuild data structures.
    drupal_flush_all_caches();
    $this->container = \Drupal::getContainer();

    // Reset static variables and reload permissions.
    $this
      ->refreshVariables();
  }

  /**
   * Refreshes in-memory configuration and state information.
   *
   * Useful after a page request is made that changes configuration or state in
   * a different thread.
   *
   * In other words calling a settings page with $this->submitForm() with a
   * changed value would update configuration to reflect that change, but in the
   * thread that made the call (thread running the test) the changed values
   * would not be picked up.
   *
   * This method clears the cache and loads a fresh copy.
   */
  protected function refreshVariables() {

    // Clear the tag cache.
    // @todo Replace drupal_static() usage within classes and provide a
    //   proper interface for invoking reset() on a cache backend:
    //   https://www.drupal.org/node/2311945.
    drupal_static_reset('Drupal\\Core\\Cache\\CacheBackendInterface::tagCache');
    drupal_static_reset('Drupal\\Core\\Cache\\DatabaseBackend::deletedTags');
    drupal_static_reset('Drupal\\Core\\Cache\\DatabaseBackend::invalidatedTags');
    foreach (Cache::getBins() as $backend) {
      if (is_callable(array(
        $backend,
        'reset',
      ))) {
        $backend
          ->reset();
      }
    }
    $this->container
      ->get('config.factory')
      ->reset();
    $this->container
      ->get('state')
      ->resetCache();
  }

  /**
   * Returns whether a given user account is logged in.
   *
   * @param \Drupal\user\UserInterface $account
   *   The user account object to check.
   *
   * @return bool
   *   Return TRUE if the user is logged in, FALSE otherwise.
   */
  protected function drupalUserIsLoggedIn(UserInterface $account) {
    $logged_in = FALSE;
    if (isset($account->sessionId)) {
      $session_handler = $this->container
        ->get('session_handler.storage');
      $logged_in = (bool) $session_handler
        ->read($account->sessionId);
    }
    return $logged_in;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
BrowserTestBase::$classLoader protected property Class loader.
BrowserTestBase::$configDirectories protected property The config directories used in this test.
BrowserTestBase::$configImporter protected property The config importer that can be used in a test.
BrowserTestBase::$container protected property The dependency injection container used in the test.
BrowserTestBase::$customTranslations protected property An array of custom translations suitable for drupal_rewrite_settings().
BrowserTestBase::$databasePrefix protected property The database prefix of this test run.
BrowserTestBase::$kernel protected property The DrupalKernel instance used in the test.
BrowserTestBase::$loggedInUser protected property The current user logged in using the Mink controlled browser.
BrowserTestBase::$mink protected property Mink session manager.
BrowserTestBase::$minkDefaultDriverArgs protected property
BrowserTestBase::$minkDefaultDriverClass protected property
BrowserTestBase::$originalSiteDirectory protected property The site directory of the original parent site.
BrowserTestBase::$privateFilesDirectory protected property The private file directory for the test environment.
BrowserTestBase::$profile protected property The profile to install as a basis for testing.
BrowserTestBase::$publicFilesDirectory protected property The public file directory for the test environment.
BrowserTestBase::$rootUser protected property The root user.
BrowserTestBase::$siteDirectory protected property The site directory of this test run.
BrowserTestBase::$tempFilesDirectory protected property The temp file directory for the test environment.
BrowserTestBase::$timeLimit protected property Time limit in seconds for the test.
BrowserTestBase::$translationFilesDirectory protected property The translation file directory for the test environment.
BrowserTestBase::assertSession public function Returns WebAssert object.
BrowserTestBase::changeDatabasePrefix private function Changes the database connection to the prefixed one.
BrowserTestBase::checkPermissions protected function Checks whether a given list of permission names is valid.
BrowserTestBase::cleanupEnvironment protected function Clean up the Simpletest environment.
BrowserTestBase::drupalCreateRole protected function Creates a role with specified permissions.
BrowserTestBase::drupalCreateUser protected function Creates a user with a given set of permissions.
BrowserTestBase::drupalGet protected function Retrieves a Drupal path or an absolute path.
BrowserTestBase::drupalLogin protected function Logs in a user using the Mink controlled browser.
BrowserTestBase::drupalLogout protected function Logs a user out of the Mink controlled browser and confirms.
BrowserTestBase::drupalUserIsLoggedIn protected function Returns whether a given user account is logged in.
BrowserTestBase::filePreDeleteCallback public static function Ensures test files are deletable within file_unmanaged_delete_recursive().
BrowserTestBase::getAbsoluteUrl protected function Takes a path and returns an absolute path.
BrowserTestBase::getDatabaseConnection public static function Returns the database connection to the site running Simpletest.
BrowserTestBase::getDefaultDriverInstance protected function Gets an instance of the default Mink driver.
BrowserTestBase::getOptions protected function Helper function to get the options of select field.
BrowserTestBase::getSession public function Returns Mink session.
BrowserTestBase::initMink protected function Initializes Mink sessions.
BrowserTestBase::installDrupal public function Installs Drupal into the Simpletest site.
BrowserTestBase::installParameters protected function Returns the parameters that will be used when Simpletest installs Drupal.
BrowserTestBase::prepareDatabasePrefix private function Generates a database prefix for running tests.
BrowserTestBase::prepareEnvironment protected function Prepares the current environment for running the test.
BrowserTestBase::prepareRequest protected function Prepare for a request to testing site.
BrowserTestBase::prepareRequestForGenerator protected function Creates a mock request and sets it on the generator.
BrowserTestBase::rebuildContainer protected function Rebuilds \Drupal::getContainer().
BrowserTestBase::refreshVariables protected function Refreshes in-memory configuration and state information.
BrowserTestBase::registerSessions protected function Registers additional Mink sessions.
BrowserTestBase::resetAll protected function Resets all data structures after having enabled new modules.
BrowserTestBase::runTest protected function Override to use Mink exceptions.
BrowserTestBase::setUp protected function
BrowserTestBase::submitForm protected function Fills and submits a form.
BrowserTestBase::tearDown protected function
BrowserTestBase::writeSettings protected function Rewrites the settings.php file of the test site.
RandomGeneratorTrait::$randomGenerator protected property The random generator.
RandomGeneratorTrait::getRandomGenerator protected function Gets the random generator for the utility methods.
RandomGeneratorTrait::randomMachineName protected function Generates a unique random string containing letters and numbers.
RandomGeneratorTrait::randomObject public function Generates a random PHP object.
RandomGeneratorTrait::randomString public function Generates a pseudo-random string of ASCII characters of codes 32 to 126.
RandomGeneratorTrait::randomStringValidate public function Callback for random string validation.
SessionTestTrait::$sessionName protected property The name of the session cookie.
SessionTestTrait::generateSessionName protected function Generates a session cookie name.
SessionTestTrait::getSessionName protected function Returns the session name in use on the child site.