drupal = $drupal; } /** * {@inheritDoc} */ public function getDrupal() { return $this->drupal; } /** * {@inheritDoc} */ public function setDispatcher(HookDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } /** * Set parameters provided for Drupal. */ public function setDrupalParameters(array $parameters) { $this->drupalParameters = $parameters; } /** * Returns a specific Drupal parameter. * * @param string $name * Parameter name. * * @return mixed */ public function getDrupalParameter($name) { return isset($this->drupalParameters[$name]) ? $this->drupalParameters[$name] : NULL; } /** * Returns a specific Drupal text value. * * @param string $name * Text value name, such as 'log_out', which corresponds to the default 'Log * out' link text. * @throws \Exception * @return */ public function getDrupalText($name) { $text = $this->getDrupalParameter('text'); if (!isset($text[$name])) { throw new \Exception(sprintf('No such Drupal string: %s', $name)); } return $text[$name]; } /** * Returns a specific css selector. * * @param $name * string CSS selector name */ public function getDrupalSelector($name) { $text = $this->getDrupalParameter('selectors'); if (!isset($text[$name])) { throw new \Exception(sprintf('No such selector configured: %s', $name)); } return $text[$name]; } /** * Get active Drupal Driver. * * @return \Drupal\Driver\DrupalDriver */ public function getDriver($name = NULL) { return $this->getDrupal()->getDriver($name); } /** * Get driver's random generator. */ public function getRandom() { return $this->getDriver()->getRandom(); } /** * Massage node values to match the expectations on different Drupal versions. * * @beforeNodeCreate */ public static function alterNodeParameters(BeforeNodeCreateScope $scope) { $node = $scope->getEntity(); // Get the Drupal API version if available. This is not available when // using e.g. the BlackBoxDriver or DrushDriver. $api_version = NULL; $driver = $scope->getContext()->getDrupal()->getDriver(); if ($driver instanceof \Drupal\Driver\DrupalDriver) { $api_version = $scope->getContext()->getDrupal()->getDriver()->version; } // On Drupal 8 the timestamps should be in UNIX time. switch ($api_version) { case 8: foreach (array('changed', 'created', 'revision_timestamp') as $field) { if (!empty($node->$field) && !is_numeric($node->$field)) { $node->$field = strtotime($node->$field); } } break; } } /** * Remove any created nodes. * * @AfterScenario */ public function cleanNodes() { // Remove any nodes that were created. foreach ($this->nodes as $node) { $this->getDriver()->nodeDelete($node); } $this->nodes = array(); } /** * Remove any created users. * * @AfterScenario */ public function cleanUsers() { // Remove any users that were created. if (!empty($this->users)) { foreach ($this->users as $user) { $this->getDriver()->userDelete($user); } $this->getDriver()->processBatch(); $this->users = array(); $this->user = FALSE; if ($this->loggedIn()) { $this->logout(); } } } /** * Remove any created terms. * * @AfterScenario */ public function cleanTerms() { // Remove any terms that were created. foreach ($this->terms as $term) { $this->getDriver()->termDelete($term); } $this->terms = array(); } /** * Remove any created roles. * * @AfterScenario */ public function cleanRoles() { // Remove any roles that were created. foreach ($this->roles as $rid) { $this->getDriver()->roleDelete($rid); } $this->roles = array(); } /** * Remove any created languages. * * @AfterScenario */ public function cleanLanguages() { // Delete any languages that were created. foreach ($this->languages as $language) { $this->getDriver()->languageDelete($language); unset($this->languages[$language->langcode]); } } /** * Clear static caches. * * @AfterScenario @api */ public function clearStaticCaches() { $this->getDriver()->clearStaticCaches(); } /** * Dispatch scope hooks. * * @param string $scope * The entity scope to dispatch. * @param \stdClass $entity * The entity. */ protected function dispatchHooks($scopeType, \stdClass $entity) { $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType; $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity); $callResults = $this->dispatcher->dispatchScopeHooks($scope); // The dispatcher suppresses exceptions, throw them here if there are any. foreach ($callResults as $result) { if ($result->hasException()) { $exception = $result->getException(); throw $exception; } } } /** * Create a node. * * @return object * The created node. */ public function nodeCreate($node) { $this->dispatchHooks('BeforeNodeCreateScope', $node); $this->parseEntityFields('node', $node); $saved = $this->getDriver()->createNode($node); $this->dispatchHooks('AfterNodeCreateScope', $saved); $this->nodes[] = $saved; return $saved; } /** * Parse multi-value fields. Possible formats: * A, B, C * A - B, C - D, E - F * * @param string $entity_type * The entity type. * @param \stdClass $entity * An object containing the entity properties and fields as properties. */ public function parseEntityFields($entity_type, \stdClass $entity) { $multicolumn_field = ''; $multicolumn_fields = array(); foreach (clone $entity as $field => $field_value) { // Reset the multicolumn field if the field name does not contain a column. if (strpos($field, ':') === FALSE) { $multicolumn_field = ''; } // Start tracking a new multicolumn field if the field name contains a ':' // which is preceded by at least 1 character. elseif (strpos($field, ':', 1) !== FALSE) { list($multicolumn_field, $multicolumn_column) = explode(':', $field); } // If a field name starts with a ':' but we are not yet tracking a // multicolumn field we don't know to which field this belongs. elseif (empty($multicolumn_field)) { throw new \Exception('Field name missing for ' . $field); } // Update the column name if the field name starts with a ':' and we are // already tracking a multicolumn field. else { $multicolumn_column = substr($field, 1); } $is_multicolumn = $multicolumn_field && $multicolumn_column; $field_name = $multicolumn_field ?: $field; if ($this->getDriver()->isField($entity_type, $field_name)) { // Split up multiple values in multi-value fields. $values = array(); foreach (explode(', ', $field_value) as $key => $value) { $columns = $value; // Split up field columns if the ' - ' separator is present. if (strstr($value, ' - ') !== FALSE) { $columns = array(); foreach (explode(' - ', $value) as $column) { // Check if it is an inline named column. if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) { list ($key, $column) = explode(': ', $column); $columns[$key] = $column; } else { $columns[] = $column; } } } // Use the column name if we are tracking a multicolumn field. if ($is_multicolumn) { $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns; unset($entity->$field); } else { $values[] = $columns; } } // Replace regular fields inline in the entity after parsing. if (!$is_multicolumn) { $entity->$field_name = $values; // Don't specify any value if the step author has left it blank. if ($field_value === '') { unset($entity->$field_name); } } } } // Add the multicolumn fields to the entity. foreach ($multicolumn_fields as $field_name => $columns) { // Don't specify any value if the step author has left it blank. if (count(array_filter($columns, function ($var) { return ($var !== ''); })) > 0) { $entity->$field_name = $columns; } } } /** * Create a user. * * @return object * The created user. */ public function userCreate($user) { $this->dispatchHooks('BeforeUserCreateScope', $user); $this->parseEntityFields('user', $user); $this->getDriver()->userCreate($user); $this->dispatchHooks('AfterUserCreateScope', $user); $this->users[$user->name] = $this->user = $user; return $user; } /** * Create a term. * * @return object * The created term. */ public function termCreate($term) { $this->dispatchHooks('BeforeTermCreateScope', $term); $this->parseEntityFields('taxonomy_term', $term); $saved = $this->getDriver()->createTerm($term); $this->dispatchHooks('AfterTermCreateScope', $saved); $this->terms[] = $saved; return $saved; } /** * Creates a language. * * @param \stdClass $language * An object with the following properties: * - langcode: the langcode of the language to create. * * @return object|FALSE * The created language, or FALSE if the language was already created. */ public function languageCreate(\stdClass $language) { $this->dispatchHooks('BeforeLanguageCreateScope', $language); $language = $this->getDriver()->languageCreate($language); if ($language) { $this->dispatchHooks('AfterLanguageCreateScope', $language); $this->languages[$language->langcode] = $language; } return $language; } /** * Log-in the current user. */ public function login() { // Check if logged in. if ($this->loggedIn()) { $this->logout(); } if (!$this->user) { throw new \Exception('Tried to login without a user.'); } $this->getSession()->visit($this->locatePath('/user')); $element = $this->getSession()->getPage(); $element->fillField($this->getDrupalText('username_field'), $this->user->name); $element->fillField($this->getDrupalText('password_field'), $this->user->pass); $submit = $element->findButton($this->getDrupalText('log_in')); if (empty($submit)) { throw new \Exception(sprintf("No submit button at %s", $this->getSession()->getCurrentUrl())); } // Log in. $submit->click(); if (!$this->loggedIn()) { if (isset($this->user->role)) { 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)); } else { throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s'", $this->user->name)); } } } /** * Logs the current user out. */ public function logout() { $this->getSession()->visit($this->locatePath('/user/logout')); } /** * Determine if the a user is already logged in. * * @return boolean * Returns TRUE if a user is logged in for this session. */ public function loggedIn() { $session = $this->getSession(); // If the session has not been started yet, or no page has yet been loaded, // then this is a brand new test session and the user is not logged in. if (!$session->isStarted() || !$page = $session->getPage()) { return FALSE; } // Look for a css selector to determine if a user is logged in. // Default is the logged-in class on the body tag. // Which should work with almost any theme. try { if ($page->has('css', $this->getDrupalSelector('logged_in_selector'))) { return TRUE; } } catch (DriverException $e) { // This test may fail if the driver did not load any site yet. } // Some themes do not add that class to the body, so lets check if the // login form is displayed on /user/login. $session->visit($this->locatePath('/user/login')); if (!$page->has('css', $this->getDrupalSelector('login_form_selector'))) { return TRUE; } $session->visit($this->locatePath('/')); // As a last resort, if a logout link is found, we are logged in. While not // perfect, this is how Drupal SimpleTests currently work as well. return $page->findLink($this->getDrupalText('log_out')); } /** * User with a given role is already logged in. * * @param string $role * A single role, or multiple comma-separated roles in a single string. * * @return boolean * Returns TRUE if the current logged in user has this role (or roles). */ public function loggedInWithRole($role) { return $this->loggedIn() && $this->user && isset($this->user->role) && $this->user->role == $role; } }