3 namespace Drupal\DrupalExtension\Context;
5 use Behat\MinkExtension\Context\RawMinkContext;
6 use Behat\Mink\Exception\DriverException;
7 use Behat\Testwork\Hook\HookDispatcher;
9 use Drupal\DrupalDriverManager;
11 use Drupal\DrupalExtension\Hook\Scope\AfterLanguageEnableScope;
12 use Drupal\DrupalExtension\Hook\Scope\AfterNodeCreateScope;
13 use Drupal\DrupalExtension\Hook\Scope\AfterTermCreateScope;
14 use Drupal\DrupalExtension\Hook\Scope\AfterUserCreateScope;
15 use Drupal\DrupalExtension\Hook\Scope\BaseEntityScope;
16 use Drupal\DrupalExtension\Hook\Scope\BeforeLanguageEnableScope;
17 use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
18 use Drupal\DrupalExtension\Hook\Scope\BeforeUserCreateScope;
19 use Drupal\DrupalExtension\Hook\Scope\BeforeTermCreateScope;
23 * Provides the raw functionality for interacting with Drupal.
25 class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface {
28 * Drupal driver manager.
30 * @var \Drupal\DrupalDriverManager
39 private $drupalParameters;
42 * Event dispatcher object.
44 * @var \Behat\Testwork\Hook\HookDispatcher
46 protected $dispatcher;
49 * Keep track of nodes so they can be cleaned up.
53 protected $nodes = array();
56 * Current authenticated user.
58 * A value of FALSE denotes an anonymous user.
65 * Keep track of all users that are created so they can easily be removed.
69 protected $users = array();
72 * Keep track of all terms that are created so they can easily be removed.
76 protected $terms = array();
79 * Keep track of any roles that are created so they can easily be removed.
83 protected $roles = array();
86 * Keep track of any languages that are created so they can easily be removed.
90 protected $languages = array();
95 public function setDrupal(DrupalDriverManager $drupal) {
96 $this->drupal = $drupal;
102 public function getDrupal() {
103 return $this->drupal;
109 public function setDispatcher(HookDispatcher $dispatcher) {
110 $this->dispatcher = $dispatcher;
114 * Set parameters provided for Drupal.
116 public function setDrupalParameters(array $parameters) {
117 $this->drupalParameters = $parameters;
121 * Returns a specific Drupal parameter.
123 * @param string $name
128 public function getDrupalParameter($name) {
129 return isset($this->drupalParameters[$name]) ? $this->drupalParameters[$name] : NULL;
133 * Returns a specific Drupal text value.
135 * @param string $name
136 * Text value name, such as 'log_out', which corresponds to the default 'Log
141 public function getDrupalText($name) {
142 $text = $this->getDrupalParameter('text');
143 if (!isset($text[$name])) {
144 throw new \Exception(sprintf('No such Drupal string: %s', $name));
150 * Returns a specific css selector.
153 * string CSS selector name
155 public function getDrupalSelector($name) {
156 $text = $this->getDrupalParameter('selectors');
157 if (!isset($text[$name])) {
158 throw new \Exception(sprintf('No such selector configured: %s', $name));
164 * Get active Drupal Driver.
166 * @return \Drupal\Driver\DrupalDriver
168 public function getDriver($name = NULL) {
169 return $this->getDrupal()->getDriver($name);
173 * Get driver's random generator.
175 public function getRandom() {
176 return $this->getDriver()->getRandom();
180 * Massage node values to match the expectations on different Drupal versions.
184 public static function alterNodeParameters(BeforeNodeCreateScope $scope) {
185 $node = $scope->getEntity();
187 // Get the Drupal API version if available. This is not available when
188 // using e.g. the BlackBoxDriver or DrushDriver.
190 $driver = $scope->getContext()->getDrupal()->getDriver();
191 if ($driver instanceof \Drupal\Driver\DrupalDriver) {
192 $api_version = $scope->getContext()->getDrupal()->getDriver()->version;
195 // On Drupal 8 the timestamps should be in UNIX time.
196 switch ($api_version) {
198 foreach (array('changed', 'created', 'revision_timestamp') as $field) {
199 if (!empty($node->$field) && !is_numeric($node->$field)) {
200 $node->$field = strtotime($node->$field);
208 * Remove any created nodes.
212 public function cleanNodes() {
213 // Remove any nodes that were created.
214 foreach ($this->nodes as $node) {
215 $this->getDriver()->nodeDelete($node);
217 $this->nodes = array();
221 * Remove any created users.
225 public function cleanUsers() {
226 // Remove any users that were created.
227 if (!empty($this->users)) {
228 foreach ($this->users as $user) {
229 $this->getDriver()->userDelete($user);
231 $this->getDriver()->processBatch();
232 $this->users = array();
234 if ($this->loggedIn()) {
241 * Remove any created terms.
245 public function cleanTerms() {
246 // Remove any terms that were created.
247 foreach ($this->terms as $term) {
248 $this->getDriver()->termDelete($term);
250 $this->terms = array();
254 * Remove any created roles.
258 public function cleanRoles() {
259 // Remove any roles that were created.
260 foreach ($this->roles as $rid) {
261 $this->getDriver()->roleDelete($rid);
263 $this->roles = array();
267 * Remove any created languages.
271 public function cleanLanguages() {
272 // Delete any languages that were created.
273 foreach ($this->languages as $language) {
274 $this->getDriver()->languageDelete($language);
275 unset($this->languages[$language->langcode]);
280 * Clear static caches.
282 * @AfterScenario @api
284 public function clearStaticCaches() {
285 $this->getDriver()->clearStaticCaches();
289 * Dispatch scope hooks.
291 * @param string $scope
292 * The entity scope to dispatch.
293 * @param \stdClass $entity
296 protected function dispatchHooks($scopeType, \stdClass $entity) {
297 $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
298 $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
299 $callResults = $this->dispatcher->dispatchScopeHooks($scope);
301 // The dispatcher suppresses exceptions, throw them here if there are any.
302 foreach ($callResults as $result) {
303 if ($result->hasException()) {
304 $exception = $result->getException();
316 public function nodeCreate($node) {
317 $this->dispatchHooks('BeforeNodeCreateScope', $node);
318 $this->parseEntityFields('node', $node);
319 $saved = $this->getDriver()->createNode($node);
320 $this->dispatchHooks('AfterNodeCreateScope', $saved);
321 $this->nodes[] = $saved;
326 * Parse multi-value fields. Possible formats:
328 * A - B, C - D, E - F
330 * @param string $entity_type
332 * @param \stdClass $entity
333 * An object containing the entity properties and fields as properties.
335 public function parseEntityFields($entity_type, \stdClass $entity) {
336 $multicolumn_field = '';
337 $multicolumn_fields = array();
339 foreach (clone $entity as $field => $field_value) {
340 // Reset the multicolumn field if the field name does not contain a column.
341 if (strpos($field, ':') === FALSE) {
342 $multicolumn_field = '';
344 // Start tracking a new multicolumn field if the field name contains a ':'
345 // which is preceded by at least 1 character.
346 elseif (strpos($field, ':', 1) !== FALSE) {
347 list($multicolumn_field, $multicolumn_column) = explode(':', $field);
349 // If a field name starts with a ':' but we are not yet tracking a
350 // multicolumn field we don't know to which field this belongs.
351 elseif (empty($multicolumn_field)) {
352 throw new \Exception('Field name missing for ' . $field);
354 // Update the column name if the field name starts with a ':' and we are
355 // already tracking a multicolumn field.
357 $multicolumn_column = substr($field, 1);
360 $is_multicolumn = $multicolumn_field && $multicolumn_column;
361 $field_name = $multicolumn_field ?: $field;
362 if ($this->getDriver()->isField($entity_type, $field_name)) {
363 // Split up multiple values in multi-value fields.
365 foreach (explode(', ', $field_value) as $key => $value) {
367 // Split up field columns if the ' - ' separator is present.
368 if (strstr($value, ' - ') !== FALSE) {
370 foreach (explode(' - ', $value) as $column) {
371 // Check if it is an inline named column.
372 if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) {
373 list ($key, $column) = explode(': ', $column);
374 $columns[$key] = $column;
377 $columns[] = $column;
381 // Use the column name if we are tracking a multicolumn field.
382 if ($is_multicolumn) {
383 $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
384 unset($entity->$field);
387 $values[] = $columns;
390 // Replace regular fields inline in the entity after parsing.
391 if (!$is_multicolumn) {
392 $entity->$field_name = $values;
393 // Don't specify any value if the step author has left it blank.
394 if ($field_value === '') {
395 unset($entity->$field_name);
401 // Add the multicolumn fields to the entity.
402 foreach ($multicolumn_fields as $field_name => $columns) {
403 // Don't specify any value if the step author has left it blank.
404 if (count(array_filter($columns, function ($var) {
405 return ($var !== '');
407 $entity->$field_name = $columns;
418 public function userCreate($user) {
419 $this->dispatchHooks('BeforeUserCreateScope', $user);
420 $this->parseEntityFields('user', $user);
421 $this->getDriver()->userCreate($user);
422 $this->dispatchHooks('AfterUserCreateScope', $user);
423 $this->users[$user->name] = $this->user = $user;
433 public function termCreate($term) {
434 $this->dispatchHooks('BeforeTermCreateScope', $term);
435 $this->parseEntityFields('taxonomy_term', $term);
436 $saved = $this->getDriver()->createTerm($term);
437 $this->dispatchHooks('AfterTermCreateScope', $saved);
438 $this->terms[] = $saved;
443 * Creates a language.
445 * @param \stdClass $language
446 * An object with the following properties:
447 * - langcode: the langcode of the language to create.
449 * @return object|FALSE
450 * The created language, or FALSE if the language was already created.
452 public function languageCreate(\stdClass $language) {
453 $this->dispatchHooks('BeforeLanguageCreateScope', $language);
454 $language = $this->getDriver()->languageCreate($language);
456 $this->dispatchHooks('AfterLanguageCreateScope', $language);
457 $this->languages[$language->langcode] = $language;
463 * Log-in the current user.
465 public function login() {
466 // Check if logged in.
467 if ($this->loggedIn()) {
472 throw new \Exception('Tried to login without a user.');
475 $this->getSession()->visit($this->locatePath('/user'));
476 $element = $this->getSession()->getPage();
477 $element->fillField($this->getDrupalText('username_field'), $this->user->name);
478 $element->fillField($this->getDrupalText('password_field'), $this->user->pass);
479 $submit = $element->findButton($this->getDrupalText('log_in'));
480 if (empty($submit)) {
481 throw new \Exception(sprintf("No submit button at %s", $this->getSession()->getCurrentUrl()));
487 if (!$this->loggedIn()) {
488 if (isset($this->user->role)) {
489 throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s' with role '%s'", $this->user->name, $this->user->role));
492 throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s'", $this->user->name));
498 * Logs the current user out.
500 public function logout() {
501 $this->getSession()->visit($this->locatePath('/user/logout'));
505 * Determine if the a user is already logged in.
508 * Returns TRUE if a user is logged in for this session.
510 public function loggedIn() {
511 $session = $this->getSession();
513 // If the session has not been started yet, or no page has yet been loaded,
514 // then this is a brand new test session and the user is not logged in.
515 if (!$session->isStarted() || !$page = $session->getPage()) {
519 // Look for a css selector to determine if a user is logged in.
520 // Default is the logged-in class on the body tag.
521 // Which should work with almost any theme.
523 if ($page->has('css', $this->getDrupalSelector('logged_in_selector'))) {
526 } catch (DriverException $e) {
527 // This test may fail if the driver did not load any site yet.
530 // Some themes do not add that class to the body, so lets check if the
531 // login form is displayed on /user/login.
532 $session->visit($this->locatePath('/user/login'));
533 if (!$page->has('css', $this->getDrupalSelector('login_form_selector'))) {
537 $session->visit($this->locatePath('/'));
539 // As a last resort, if a logout link is found, we are logged in. While not
540 // perfect, this is how Drupal SimpleTests currently work as well.
541 return $page->findLink($this->getDrupalText('log_out'));
545 * User with a given role is already logged in.
547 * @param string $role
548 * A single role, or multiple comma-separated roles in a single string.
551 * Returns TRUE if the current logged in user has this role (or roles).
553 public function loggedInWithRole($role) {
554 return $this->loggedIn() && $this->user && isset($this->user->role) && $this->user->role == $role;