Commit 9d5b5a53 authored by catch's avatar catch

Issue #2469721 by larowlan, alexpott, dawehner, jibran: Add functionality to...

Issue #2469721 by larowlan, alexpott, dawehner, jibran: Add functionality to store browser output to BrowserTestBase
parent dd8056fb
......@@ -2,38 +2,11 @@
namespace Drupal\simpletest;
use Behat\Mink\Driver\GoutteDriver;
use Behat\Mink\Element\Element;
use Behat\Mink\Mink;
use Behat\Mink\Session;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\ConnectionNotDefinedException;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\Test\TestRunnerKernel;
use Drupal\Core\Url;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use Symfony\Component\CssSelector\CssSelector;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Tests\BrowserTestBase as BaseBrowserTestBase;
/**
* 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:
* - verbose output - see https://www.drupal.org/node/2469721
* - ajax form emulation - see https://www.drupal.org/node/2469713
*
* Tests extending BrowserTestBase must exist in the
* Drupal\Tests\yourmodule\Functional namespace and live in the
* modules/yourmodule/Tests/Functional directory.
......@@ -41,1369 +14,10 @@
* @ingroup testing
*
* @see \Drupal\simpletest\WebTestBase
* @see \Drupal\Tests\BrowserTestBase
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.
* Use Drupal\Tests\BrowserTestBase instead.
*/
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 = GoutteDriver::class;
/*
* 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;
/**
* {@inheritdoc}
*
* Browser tests are run in separate processes to prevent collisions between
* code that may be loaded by tests.
*/
protected $runTestInSeparateProcess = TRUE;
/**
* {@inheritdoc}
*/
protected $preserveGlobalState = FALSE;
/**
* The base URL.
*
* @var string
*/
protected $baseUrl;
/**
* Initializes Mink sessions.
*
* @return \Behat\Mink\Session
* The mink session.
*/
protected function initMink() {
$driver = $this->getDefaultDriverInstance();
if ($driver instanceof GoutteDriver) {
$driver->getClient()->setClient(\Drupal::httpClient());
}
$session = new Session($driver);
$this->mink = new Mink();
$this->mink->registerSession('default', $session);
$this->mink->setDefaultSessionName('default');
$this->registerSessions();
// According to the W3C WebDriver specification a cookie can only be set if
// the cookie domain is equal to the domain of the active document. When the
// browser starts up the active document is not our domain but 'about:blank'
// or similar. To be able to set our User-Agent and Xdebug cookies at the
// start of the test we now do a request to the front page so the active
// document matches the domain.
// @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
// @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
$session = $this->getSession();
$session->visit($this->baseUrl);
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) {
throw new \Exception(
'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;
$this->baseUrl = $base_url;
// 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 = User::create($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 = Role::create(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.
*