Completed
Pull Request — master (#237)
by Fabian
02:16
created

RawDrupalContext::setDrupalParameters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Drupal\DrupalExtension\Context;
4
5
use Behat\MinkExtension\Context\RawMinkContext;
6
use Behat\Mink\Exception\DriverException;
7
use Behat\Testwork\Hook\HookDispatcher;
8
9
use Drupal\DrupalDriverManager;
10
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;
20
21
22
/**
23
 * Provides the raw functionality for interacting with Drupal.
24
 */
25
class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface {
26
27
  /**
28
   * Drupal driver manager.
29
   *
30
   * @var \Drupal\DrupalDriverManager
31
   */
32
  private $drupal;
33
34
  /**
35
   * Test parameters.
36
   *
37
   * @var array
38
   */
39
  private $drupalParameters;
40
41
  /**
42
   * Event dispatcher object.
43
   *
44
   * @var \Behat\Testwork\Hook\HookDispatcher
45
   */
46
  protected $dispatcher;
47
48
  /**
49
   * Keep track of nodes so they can be cleaned up.
50
   *
51
   * @var array
52
   */
53
  protected $nodes = array();
54
55
  /**
56
   * Current authenticated user.
57
   *
58
   * A value of FALSE denotes an anonymous user.
59
   *
60
   * @var stdClass|bool
61
   */
62
  public $user = FALSE;
63
64
  /**
65
   * Keep track of all users that are created so they can easily be removed.
66
   *
67
   * @var array
68
   */
69
  protected $users = array();
70
71
  /**
72
   * Keep track of all terms that are created so they can easily be removed.
73
   *
74
   * @var array
75
   */
76
  protected $terms = array();
77
78
  /**
79
   * Keep track of any roles that are created so they can easily be removed.
80
   *
81
   * @var array
82
   */
83
  protected $roles = array();
84
85
  /**
86
   * Keep track of any languages that are created so they can easily be removed.
87
   *
88
   * @var array
89
   */
90
  protected $languages = array();
91
92
  /**
93
   * {@inheritDoc}
94
   */
95
  public function setDrupal(DrupalDriverManager $drupal) {
96
    $this->drupal = $drupal;
97
  }
98
99
  /**
100
   * {@inheritDoc}
101
   */
102
  public function getDrupal() {
103
    return $this->drupal;
104
  }
105
106
  /**
107
   * {@inheritDoc}
108
   */
109
  public function setDispatcher(HookDispatcher $dispatcher) {
110
    $this->dispatcher = $dispatcher;
111
  }
112
113
  /**
114
   * Set parameters provided for Drupal.
115
   */
116
  public function setDrupalParameters(array $parameters) {
117
    $this->drupalParameters = $parameters;
118
  }
119
120
  /**
121
   * Returns a specific Drupal parameter.
122
   *
123
   * @param string $name
124
   *   Parameter name.
125
   *
126
   * @return mixed
127
   */
128
  public function getDrupalParameter($name) {
129
    return isset($this->drupalParameters[$name]) ? $this->drupalParameters[$name] : NULL;
130
  }
131
132
  /**
133
   * Returns a specific Drupal text value.
134
   *
135
   * @param string $name
136
   *   Text value name, such as 'log_out', which corresponds to the default 'Log
137
   *   out' link text.
138
   * @throws \Exception
139
   * @return
140
   */
141 View Code Duplication
  public function getDrupalText($name) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
142
    $text = $this->getDrupalParameter('text');
143
    if (!isset($text[$name])) {
144
      throw new \Exception(sprintf('No such Drupal string: %s', $name));
145
    }
146
    return $text[$name];
147
  }
148
149
  /**
150
   * Returns a specific css selector.
151
   *
152
   * @param $name
153
   *   string CSS selector name
154
   */
155 View Code Duplication
  public function getDrupalSelector($name) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
156
    $text = $this->getDrupalParameter('selectors');
157
    if (!isset($text[$name])) {
158
      throw new \Exception(sprintf('No such selector configured: %s', $name));
159
    }
160
    return $text[$name];
161
  }
162
163
  /**
164
   * Get active Drupal Driver.
165
   */
166
  public function getDriver($name = NULL) {
167
    return $this->getDrupal()->getDriver($name);
168
  }
169
170
  /**
171
   * Get driver's random generator.
172
   */
173
  public function getRandom() {
174
    return $this->getDriver()->getRandom();
175
  }
176
177
  /**
178
   * Massage node values to match the expectations on different Drupal versions.
179
   *
180
   * @beforeNodeCreate
181
   */
182
  public function alterNodeParameters(BeforeNodeCreateScope $scope) {
183
    $node = $scope->getEntity();
184
185
    // Get the Drupal API version if available. This is not available when
186
    // using e.g. the BlackBoxDriver or DrushDriver.
187
    $api_version = NULL;
188
    $driver = $scope->getContext()->getDrupal()->getDriver();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\Context as the method getDrupal() does only exist in the following implementations of said interface: Drupal\DrupalExtension\Context\DrupalContext, Drupal\DrupalExtension\C...xt\DrupalSubContextBase, Drupal\DrupalExtension\Context\DrushContext, Drupal\DrupalExtension\Context\MessageContext, Drupal\DrupalExtension\Context\RawDrupalContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
189
    if ($driver instanceof \Drupal\Driver\DrupalDriver) {
190
      $api_version = $scope->getContext()->getDrupal()->getDriver()->version;
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\Context as the method getDrupal() does only exist in the following implementations of said interface: Drupal\DrupalExtension\Context\DrupalContext, Drupal\DrupalExtension\C...xt\DrupalSubContextBase, Drupal\DrupalExtension\Context\DrushContext, Drupal\DrupalExtension\Context\MessageContext, Drupal\DrupalExtension\Context\RawDrupalContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
191
    }
192
193
    // On Drupal 8 the timestamps should be in UNIX time.
194
    switch ($api_version) {
195
      case 8:
196
        foreach (array('changed', 'created', 'revision_timestamp') as $field) {
197
          if (!empty($node->$field) && !is_numeric($node->$field)) {
198
            $node->$field = strtotime($node->$field);
199
          }
200
        }
201
      break;
202
    }
203
  }
204
205
  /**
206
   * Remove any created nodes.
207
   *
208
   * @AfterScenario
209
   */
210
  public function cleanNodes() {
211
    // Remove any nodes that were created.
212
    foreach ($this->nodes as $node) {
213
      $this->getDriver()->nodeDelete($node);
214
    }
215
    $this->nodes = array();
216
  }
217
218
  /**
219
   * Remove any created users.
220
   *
221
   * @AfterScenario
222
   */
223
  public function cleanUsers() {
224
    // Remove any users that were created.
225
    if (!empty($this->users)) {
226
      foreach ($this->users as $user) {
227
        $this->getDriver()->userDelete($user);
228
      }
229
      $this->getDriver()->processBatch();
230
      $this->users = array();
231
    }
232
  }
233
234
  /**
235
   * Remove any created terms.
236
   *
237
   * @AfterScenario
238
   */
239
  public function cleanTerms() {
240
    // Remove any terms that were created.
241
    foreach ($this->terms as $term) {
242
      $this->getDriver()->termDelete($term);
243
    }
244
    $this->terms = array();
245
  }
246
247
  /**
248
   * Remove any created roles.
249
   *
250
   * @AfterScenario
251
   */
252
  public function cleanRoles() {
253
    // Remove any roles that were created.
254
    foreach ($this->roles as $rid) {
255
      $this->getDriver()->roleDelete($rid);
256
    }
257
    $this->roles = array();
258
  }
259
260
  /**
261
   * Remove any created languages.
262
   *
263
   * @AfterScenario
264
   */
265
  public function cleanLanguages() {
266
    // Delete any languages that were created.
267
    foreach ($this->languages as $language) {
268
      $this->getDriver()->languageDelete($language);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Drupal\Driver\DriverInterface as the method languageDelete() does only exist in the following implementations of said interface: Drupal\Driver\DrupalDriver.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
269
      unset($this->languages[$language->langcode]);
270
    }
271
  }
272
273
  /**
274
   * Clear static caches.
275
   *
276
   * @AfterScenario @api
277
   */
278
  public function clearStaticCaches() {
279
    $this->getDriver()->clearStaticCaches();
280
  }
281
282
  /**
283
   * Dispatch scope hooks.
284
   *
285
   * @param string $scope
0 ignored issues
show
Bug introduced by
There is no parameter named $scope. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
286
   *   The entity scope to dispatch.
287
   * @param stdClass $entity
288
   *   The entity.
289
   */
290
  protected function dispatchHooks($scopeType, \stdClass $entity) {
291
    $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
292
    $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
293
    $callResults = $this->dispatcher->dispatchScopeHooks($scope);
294
295
    // The dispatcher suppresses exceptions, throw them here if there are any.
296
    foreach ($callResults as $result) {
297
      if ($result->hasException()) {
298
        $exception = $result->getException();
299
        throw $exception;
300
      }
301
    }
302
  }
303
304
  /**
305
   * Create a node.
306
   *
307
   * @return object
308
   *   The created node.
309
   */
310 View Code Duplication
  public function nodeCreate($node) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
311
    $this->dispatchHooks('BeforeNodeCreateScope', $node);
312
    $this->parseEntityFields('node', $node);
313
    $saved = $this->getDriver()->createNode($node);
314
    $this->dispatchHooks('AfterNodeCreateScope', $saved);
315
    $this->nodes[] = $saved;
316
    return $saved;
317
  }
318
319
  /**
320
   * Parse multi-value fields. Possible formats:
321
   *    A, B, C
322
   *    A - B, C - D, E - F
323
   *
324
   * @param string $entity_type
325
   *   The entity type.
326
   * @param \stdClass $entity
327
   *   An object containing the entity properties and fields as properties.
328
   */
329
  public function parseEntityFields($entity_type, \stdClass $entity) {
330
    $multicolumn_field = '';
331
    $multicolumn_fields = array();
332
333
    foreach ($entity as $field => $field_value) {
0 ignored issues
show
Bug introduced by
The expression $entity of type object<stdClass> is not traversable.
Loading history...
334
      // Reset the multicolumn field if the field name does not contain a column.
335
      if (strpos($field, ':') === FALSE) {
336
        $multicolumn_field = '';
337
      }
338
      // Start tracking a new multicolumn field if the field name contains a ':'
339
      // which is preceded by at least 1 character.
340
      elseif (strpos($field, ':', 1) !== FALSE) {
341
        list($multicolumn_field, $multicolumn_column) = explode(':', $field);
342
      }
343
      // If a field name starts with a ':' but we are not yet tracking a
344
      // multicolumn field we don't know to which field this belongs.
345
      elseif (empty($multicolumn_field)) {
346
        throw new \Exception('Field name missing for ' . $field);
347
      }
348
      // Update the column name if the field name starts with a ':' and we are
349
      // already tracking a multicolumn field.
350
      else {
351
        $multicolumn_column = substr($field, 1);
352
      }
353
354
      $is_multicolumn = $multicolumn_field && $multicolumn_column;
0 ignored issues
show
Bug introduced by
The variable $multicolumn_column does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
355
      $field_name = $multicolumn_field ?: $field;
356
      if ($this->getDriver()->isField($entity_type, $field_name)) {
357
        // Split up multiple values in multi-value fields.
358
        $values = array();
359
        foreach (explode(', ', $field_value) as $key => $value) {
360
          $columns = $value;
361
          // Split up field columns if the ' - ' separator is present.
362
          if (strstr($value, ' - ') !== FALSE) {
363
            $columns = array();
364
            foreach (explode(' - ', $value) as $column) {
365
              // Check if it is an inline named column.
366
              if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) {
367
                list ($key, $column) = explode(': ', $column);
368
                $columns[$key] = $column;
369
              }
370
              else {
371
                $columns[] = $column;
372
              }
373
            }
374
          }
375
          // Use the column name if we are tracking a multicolumn field.
376
          if ($is_multicolumn) {
377
            $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
378
            unset($entity->$field);
379
          }
380
          else {
381
            $values[] = $columns;
382
          }
383
        }
384
        // Replace regular fields inline in the entity after parsing.
385
        if (!$is_multicolumn) {
386
          $entity->$field_name = $values;
387
        }
388
      }
389
    }
390
391
    // Add the multicolumn fields to the entity.
392
    foreach ($multicolumn_fields as $field_name => $columns) {
393
      $entity->$field_name = $columns;
394
    }
395
  }
396
397
  /**
398
   * Create a user.
399
   *
400
   * @return object
401
   *   The created user.
402
   */
403 View Code Duplication
  public function userCreate($user) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
404
    $this->dispatchHooks('BeforeUserCreateScope', $user);
405
    $this->parseEntityFields('user', $user);
406
    $this->getDriver()->userCreate($user);
407
    $this->dispatchHooks('AfterUserCreateScope', $user);
408
    $this->users[$user->name] = $this->user = $user;
409
    return $user;
410
  }
411
412
  /**
413
   * Create a term.
414
   *
415
   * @return object
416
   *   The created term.
417
   */
418 View Code Duplication
  public function termCreate($term) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
419
    $this->dispatchHooks('BeforeTermCreateScope', $term);
420
    $this->parseEntityFields('taxonomy_term', $term);
421
    $saved = $this->getDriver()->createTerm($term);
422
    $this->dispatchHooks('AfterTermCreateScope', $saved);
423
    $this->terms[] = $saved;
424
    return $saved;
425
  }
426
427
  /**
428
   * Creates a language.
429
   *
430
   * @param \stdClass $language
431
   *   An object with the following properties:
432
   *   - langcode: the langcode of the language to create.
433
   *
434
   * @return object|FALSE
435
   *   The created language, or FALSE if the language was already created.
436
   */
437
  public function languageCreate(\stdClass $language) {
438
    $this->dispatchHooks('BeforeLanguageCreateScope', $language);
439
    $language = $this->getDriver()->languageCreate($language);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Drupal\Driver\DriverInterface as the method languageCreate() does only exist in the following implementations of said interface: Drupal\Driver\DrupalDriver.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
440
    if ($language) {
441
      $this->dispatchHooks('AfterLanguageCreateScope', $language);
442
      $this->languages[$language->langcode] = $language;
443
    }
444
    return $language;
445
  }
446
447
  /**
448
   * Log-in the current user.
449
   */
450
  public function login() {
451
    // Check if logged in.
452
    if ($this->loggedIn()) {
453
      $this->logout();
454
    }
455
456
    if (!$this->user) {
457
      throw new \Exception('Tried to login without a user.');
458
    }
459
460
    $this->getSession()->visit($this->locatePath('/user'));
461
    $element = $this->getSession()->getPage();
462
    $element->fillField($this->getDrupalText('username_field'), $this->user->name);
463
    $element->fillField($this->getDrupalText('password_field'), $this->user->pass);
464
    $submit = $element->findButton($this->getDrupalText('log_in'));
465
    if (empty($submit)) {
466
      throw new \Exception(sprintf("No submit button at %s", $this->getSession()->getCurrentUrl()));
467
    }
468
469
    // Log in.
470
    $submit->click();
471
472
    if (!$this->loggedIn()) {
473
      if (isset($this->user->role)) {
474
        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));
475
      }
476
      else {
477
        throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s'", $this->user->name));
478
      }
479
    }
480
  }
481
482
  /**
483
   * Logs the current user out.
484
   */
485
  public function logout() {
486
    $this->getSession()->visit($this->locatePath('/user/logout'));
487
  }
488
489
  /**
490
   * Determine if the a user is already logged in.
491
   *
492
   * @return boolean
493
   *   Returns TRUE if a user is logged in for this session.
494
   */
495
  public function loggedIn() {
496
    $session = $this->getSession();
497
    $page = $session->getPage();
498
499
    // Look for a css selector to determine if a user is logged in.
500
    // Default is the logged-in class on the body tag.
501
    // Which should work with almost any theme.
502
    try {
503
      if ($page->has('css', $this->getDrupalSelector('logged_in_selector'))) {
504
        return TRUE;
505
      }
506
    } catch (DriverException $e) {
507
      // This test may fail if the driver did not load any site yet.
508
    }
509
510
    // Some themes do not add that class to the body, so lets check if the
511
    // login form is displayed on /user/login.
512
    $session->visit($this->locatePath('/user/login'));
513
    if (!$page->has('css', $this->getDrupalSelector('login_form_selector'))) {
514
      return TRUE;
515
    }
516
517
    $session->visit($this->locatePath('/'));
518
519
    // As a last resort, if a logout link is found, we are logged in. While not
520
    // perfect, this is how Drupal SimpleTests currently work as well.
521
    return $page->findLink($this->getDrupalText('log_out'));
522
  }
523
524
  /**
525
   * User with a given role is already logged in.
526
   *
527
   * @param string $role
528
   *   A single role, or multiple comma-separated roles in a single string.
529
   *
530
   * @return boolean
531
   *   Returns TRUE if the current logged in user has this role (or roles).
532
   */
533
  public function loggedInWithRole($role) {
534
    return $this->loggedIn() && $this->user && isset($this->user->role) && $this->user->role == $role;
535
  }
536
537
}
538