Completed
Pull Request — master (#123)
by Damian
02:14
created

BasicContext::spin()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 4
eloc 12
c 4
b 2
f 0
nc 4
nop 2
dl 0
loc 22
rs 8.9197
1
<?php
2
3
namespace SilverStripe\BehatExtension\Context;
4
5
use Behat\Behat\Context\BehatContext;
6
use Behat\Behat\Context\Step;
7
use Behat\Behat\Event\StepEvent;
8
use Behat\Behat\Event\ScenarioEvent;
9
10
use Behat\Mink\Driver\Selenium2Driver;
11
12
use Behat\Gherkin\Node\PyStringNode;
13
use Behat\Gherkin\Node\TableNode;
14
15
// PHPUnit
16
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
17
18
/**
19
 * BasicContext
20
 *
21
 * Context used to define generic steps like following anchors or pressing buttons.
22
 * Handles timeouts.
23
 * Handles redirections.
24
 * Handles AJAX enabled links, buttons and forms - jQuery is assumed.
25
 */
26
class BasicContext extends BehatContext
27
{
28
    protected $context;
29
30
    /**
31
     * Date format in date() syntax
32
     * @var String
33
     */
34
    protected $dateFormat = 'Y-m-d';
35
36
    /**
37
     * Time format in date() syntax
38
     * @var String
39
     */
40
    protected $timeFormat = 'H:i:s';
41
42
    /**
43
     * Date/time format in date() syntax
44
     * @var String
45
     */
46
    protected $datetimeFormat = 'Y-m-d H:i:s';
47
48
    /**
49
     * Initializes context.
50
     * Every scenario gets it's own context object.
51
     *
52
     * @param   array   $parameters     context parameters (set them up through behat.yml)
53
     */
54
    public function __construct(array $parameters)
55
    {
56
        // Initialize your context here
57
        $this->context = $parameters;
58
    }
59
60
    /**
61
     * Get Mink session from MinkContext
62
     *
63
     * @return \Behat\Mink\Session
64
     */
65
    public function getSession($name = null)
66
    {
67
        return $this->getMainContext()->getSession($name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method getSession() does only exist in the following implementations of said interface: Behat\MinkExtension\Context\MinkContext, Behat\MinkExtension\Context\RawMinkContext, SilverStripe\BehatExtension\Context\BasicContext, SilverStripe\BehatExtension\Context\EmailContext, SilverStripe\BehatExtension\Context\FixtureContext, SilverStripe\BehatExtension\Context\LoginContext, SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
68
    }
69
70
    /**
71
     * @AfterStep ~@modal
72
     *
73
     * Excluding scenarios with @modal tag is required,
74
     * because modal dialogs stop any JS interaction
75
     */
76
    public function appendErrorHandlerBeforeStep(StepEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
77
    {
78
        try {
79
            $javascript = <<<JS
80
window.onerror = function(message, file, line, column, error) {
81
    var body = document.getElementsByTagName('body')[0];
82
	var msg = message + " in " + file + ":" + line + ":" + column;
83
	if(error !== undefined && error.stack !== undefined) {
84
		msg += "\\nSTACKTRACE:\\n" + error.stack;
85
	}
86
	body.setAttribute('data-jserrors', '[captured JavaScript error] ' + msg);
87
}
88
if ('undefined' !== typeof window.jQuery) {
89
    window.jQuery('body').ajaxError(function(event, jqxhr, settings, exception) {
90
        if ('abort' === exception) return;
91
        window.onerror(event.type + ': ' + settings.type + ' ' + settings.url + ' ' + exception + ' ' + jqxhr.responseText);
92
    });
93
}
94
JS;
95
96
            $this->getSession()->executeScript($javascript);
97
        } catch (\WebDriver\Exception $e) {
98
            $this->logException($e);
99
        }
100
    }
101
102
    /**
103
     * @AfterStep ~@modal
104
     *
105
     * Excluding scenarios with @modal tag is required,
106
     * because modal dialogs stop any JS interaction
107
     */
108
    public function readErrorHandlerAfterStep(StepEvent $event)
109
    {
110
        try {
111
            $page = $this->getSession()->getPage();
112
113
            $jserrors = $page->find('xpath', '//body[@data-jserrors]');
114
            if (null !== $jserrors) {
115
                $this->takeScreenshot($event);
116
                file_put_contents('php://stderr', $jserrors->getAttribute('data-jserrors') . PHP_EOL);
117
            }
118
119
            $javascript = <<<JS
120
if ('undefined' !== typeof window.jQuery) {
121
	window.jQuery(document).ready(function() {
122
		window.jQuery('body').removeAttr('data-jserrors');
123
	});
124
}
125
JS;
126
127
            $this->getSession()->executeScript($javascript);
128
        } catch (\WebDriver\Exception $e) {
129
            $this->logException($e);
130
        }
131
    }
132
133
    /**
134
     * Hook into jQuery ajaxStart, ajaxSuccess and ajaxComplete events.
135
     * Prepare __ajaxStatus() functions and attach them to these handlers.
136
     * Event handlers are removed after one run.
137
     *
138
     * @BeforeStep
139
     */
140 View Code Duplication
    public function handleAjaxBeforeStep(StepEvent $event)
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...
141
    {
142
        try {
143
            $ajaxEnabledSteps = $this->getMainContext()->getAjaxSteps();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method getAjaxSteps() does only exist in the following implementations of said interface: SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
144
            $ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
145
146
            if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText())) {
147
                return;
148
            }
149
150
            $javascript = <<<JS
151
if ('undefined' !== typeof window.jQuery && 'undefined' !== typeof window.jQuery.fn.on) {
152
    window.jQuery(document).on('ajaxStart.ss.test.behaviour', function(){
153
        window.__ajaxStatus = function() {
154
            return 'waiting';
155
        };
156
    });
157
    window.jQuery(document).on('ajaxComplete.ss.test.behaviour', function(e, jqXHR){
158
        if (null === jqXHR.getResponseHeader('X-ControllerURL')) {
159
            window.__ajaxStatus = function() {
160
                return 'no ajax';
161
            };
162
        }
163
    });
164
    window.jQuery(document).on('ajaxSuccess.ss.test.behaviour', function(e, jqXHR){
165
        if (null === jqXHR.getResponseHeader('X-ControllerURL')) {
166
            window.__ajaxStatus = function() {
167
                return 'success';
168
            };
169
        }
170
    });
171
}
172
JS;
173
            $this->getSession()->wait(500); // give browser a chance to process and render response
174
            $this->getSession()->executeScript($javascript);
175
        } catch (\WebDriver\Exception $e) {
176
            $this->logException($e);
177
        }
178
    }
179
180
    /**
181
     * Wait for the __ajaxStatus()to return anything but 'waiting'.
182
     * Don't wait longer than 5 seconds.
183
     *
184
     * Don't unregister handler if we're dealing with modal windows
185
     *
186
     * @AfterStep ~@modal
187
     */
188 View Code Duplication
    public function handleAjaxAfterStep(StepEvent $event)
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...
189
    {
190
        try {
191
            $ajaxEnabledSteps = $this->getMainContext()->getAjaxSteps();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method getAjaxSteps() does only exist in the following implementations of said interface: SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
192
            $ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
193
194
            if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText())) {
195
                return;
196
            }
197
198
            $this->handleAjaxTimeout();
199
200
            $javascript = <<<JS
201
if ('undefined' !== typeof window.jQuery && 'undefined' !== typeof window.jQuery.fn.off) {
202
window.jQuery(document).off('ajaxStart.ss.test.behaviour');
203
window.jQuery(document).off('ajaxComplete.ss.test.behaviour');
204
window.jQuery(document).off('ajaxSuccess.ss.test.behaviour');
205
}
206
JS;
207
            $this->getSession()->executeScript($javascript);
208
        } catch (\WebDriver\Exception $e) {
209
            $this->logException($e);
210
        }
211
    }
212
213
    public function handleAjaxTimeout()
214
    {
215
        $timeoutMs = $this->getMainContext()->getAjaxTimeout();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method getAjaxTimeout() does only exist in the following implementations of said interface: SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
216
217
        // Wait for an ajax request to complete, but only for a maximum of 5 seconds to avoid deadlocks
218
        $this->getSession()->wait($timeoutMs,
219
            "(typeof window.__ajaxStatus !== 'undefined' ? window.__ajaxStatus() : 'no ajax') !== 'waiting'"
220
        );
221
222
        // wait additional 100ms to allow DOM to update
223
        $this->getSession()->wait(100);
224
    }
225
226
    /**
227
     * Take screenshot when step fails.
228
     * Works only with Selenium2Driver.
229
     *
230
     * @AfterStep
231
     */
232
    public function takeScreenshotAfterFailedStep(StepEvent $event)
233
    {
234
        if (4 === $event->getResult()) {
235
            try {
236
                $this->takeScreenshot($event);
237
            } catch (\WebDriver\Exception $e) {
238
                $this->logException($e);
239
            }
240
        }
241
    }
242
243
    /**
244
     * Close modal dialog if test scenario fails on CMS page
245
     *
246
     * @AfterScenario
247
     */
248
    public function closeModalDialog(ScenarioEvent $event)
249
    {
250
        try {
251
            // Only for failed tests on CMS page
252
            if (4 === $event->getResult()) {
253
                $cmsElement = $this->getSession()->getPage()->find('css', '.cms');
254
                if ($cmsElement) {
255
                    try {
256
                        // Navigate away triggered by reloading the page
257
                        $this->getSession()->reload();
258
                        $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Mink\Driver\DriverInterface as the method getWebDriverSession() does only exist in the following implementations of said interface: Behat\Mink\Driver\Selenium2Driver.

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...
259
                    } catch (\WebDriver\Exception $e) {
260
                        // no-op, alert might not be present
261
                    }
262
                }
263
            }
264
        } catch (\WebDriver\Exception $e) {
265
            $this->logException($e);
266
        }
267
    }
268
269
    /**
270
     * Delete any created files and folders from assets directory
271
     *
272
     * @AfterScenario @assets
273
     */
274
    public function cleanAssetsAfterScenario(ScenarioEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
275
    {
276
        foreach (\File::get() as $file) {
277
            $file->delete();
278
        }
279
        \Filesystem::removeFolder(ASSETS_PATH, true);
280
    }
281
282
    public function takeScreenshot(StepEvent $event)
283
    {
284
        $driver = $this->getSession()->getDriver();
285
        // quit silently when unsupported
286
        if (!($driver instanceof Selenium2Driver)) {
287
            return;
288
        }
289
290
        $parent = $event->getLogicalParent();
291
        $feature = $parent->getFeature();
292
        $step = $event->getStep();
293
        $screenshotPath = null;
0 ignored issues
show
Unused Code introduced by
$screenshotPath is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
294
295
        $path = $this->getMainContext()->getScreenshotPath();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method getScreenshotPath() does only exist in the following implementations of said interface: SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
296
        if (!$path) {
297
            return;
298
        } // quit silently when path is not set
299
300
        \Filesystem::makeFolder($path);
301
        $path = realpath($path);
302
303 View Code Duplication
        if (!file_exists($path)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
304
            file_put_contents('php://stderr', sprintf('"%s" is not valid directory and failed to create it' . PHP_EOL, $path));
305
            return;
306
        }
307
308 View Code Duplication
        if (file_exists($path) && !is_dir($path)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
309
            file_put_contents('php://stderr', sprintf('"%s" is not valid directory' . PHP_EOL, $path));
310
            return;
311
        }
312 View Code Duplication
        if (file_exists($path) && !is_writable($path)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
313
            file_put_contents('php://stderr', sprintf('"%s" directory is not writable' . PHP_EOL, $path));
314
            return;
315
        }
316
317
        $path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine());
318
        $screenshot = $driver->getWebDriverSession()->screenshot();
319
        file_put_contents($path, base64_decode($screenshot));
320
        file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path));
321
    }
322
323
    /**
324
     * @Then /^I should be redirected to "([^"]+)"/
325
     */
326
    public function stepIShouldBeRedirectedTo($url)
327
    {
328
        if ($this->getMainContext()->canIntercept()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method canIntercept() does only exist in the following implementations of said interface: SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
329
            $client = $this->getSession()->getDriver()->getClient();
0 ignored issues
show
Bug introduced by
The method getClient() does not seem to exist on object<Behat\Mink\Driver\DriverInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
330
            $client->followRedirects(true);
331
            $client->followRedirect();
332
333
            $url = $this->getMainContext()->joinUrlParts($this->context['base_url'], $url);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method joinUrlParts() does only exist in the following implementations of said interface: SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
334
335
            assertTrue($this->getMainContext()->isCurrentUrlSimilarTo($url), sprintf('Current URL is not %s', $url));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method isCurrentUrlSimilarTo() does only exist in the following implementations of said interface: SilverStripe\BehatExtens...ext\SilverStripeContext.

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...
336
        }
337
    }
338
339
    /**
340
     * @Given /^the page can't be found/
341
     */
342
    public function stepPageCantBeFound()
343
    {
344
        $page = $this->getSession()->getPage();
345
        assertTrue(
346
            // Content from ErrorPage default record
347
            $page->hasContent('Page not found')
348
            // Generic ModelAsController message
349
            || $page->hasContent('The requested page could not be found')
350
        );
351
    }
352
353
    /**
354
     * @Given /^I wait (?:for )?([\d\.]+) second(?:s?)$/
355
     */
356
    public function stepIWaitFor($secs)
357
    {
358
        $this->getSession()->wait((float)$secs*1000);
359
    }
360
361
    /**
362
     * @Given /^I press the "([^"]*)" button$/
363
     */
364
    public function stepIPressTheButton($button)
365
    {
366
        $page = $this->getSession()->getPage();
367
        $els = $page->findAll('named', array('link_or_button', "'$button'"));
368
        $matchedEl = null;
369
        foreach ($els as $el) {
370
            if ($el->isVisible()) {
371
                $matchedEl = $el;
372
            }
373
        }
374
        assertNotNull($matchedEl, sprintf('%s button not found', $button));
375
        $matchedEl->click();
376
    }
377
378
    /**
379
     * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
380
     * Example1: I press the "Remove current combo" button, confirming the dialog
381
     * Example2: I follow the "Remove current combo" link, confirming the dialog
382
     *
383
     * @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), confirming the dialog$/
384
     */
385
    public function stepIPressTheButtonConfirmingTheDialog($button)
386
    {
387
        $this->stepIPressTheButton($button);
388
        $this->iConfirmTheDialog();
389
    }
390
391
    /**
392
     * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
393
     * Example: I follow the "Remove current combo" link, dismissing the dialog
394
     *
395
     * @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), dismissing the dialog$/
396
     */
397
    public function stepIPressTheButtonDismissingTheDialog($button)
398
    {
399
        $this->stepIPressTheButton($button);
400
        $this->iDismissTheDialog();
401
    }
402
403
    /**
404
     * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element$/
405
     */
406
    public function iClickInTheElement($clickType, $text, $selector)
407
    {
408
        $clickTypeMap = array(
409
            "double click" => "doubleclick",
410
            "click" => "click"
411
        );
412
        $page = $this->getSession()->getPage();
413
        $parentElement = $page->find('css', $selector);
414
        assertNotNull($parentElement, sprintf('"%s" element not found', $selector));
415
        $element = $parentElement->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
416
        assertNotNull($element, sprintf('"%s" not found', $text));
417
        $clickTypeFn = $clickTypeMap[$clickType];
418
        $element->$clickTypeFn();
419
    }
420
421
    /**
422
    * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
423
    * Example: I click "Delete" in the ".actions" element, confirming the dialog
424
    *
425
    * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, confirming the dialog$/
426
    */
427
   public function iClickInTheElementConfirmingTheDialog($clickType, $text, $selector)
428
   {
429
       $this->iClickInTheElement($clickType, $text, $selector);
430
       $this->iConfirmTheDialog();
431
   }
432
   /**
433
    * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
434
    * Example: I click "Delete" in the ".actions" element, dismissing the dialog
435
    *
436
    * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, dismissing the dialog$/
437
    */
438
   public function iClickInTheElementDismissingTheDialog($clickType, $text, $selector)
439
   {
440
       $this->iClickInTheElement($clickType, $text, $selector);
441
       $this->iDismissTheDialog();
442
   }
443
444
    /**
445
     * @Given /^I type "([^"]*)" into the dialog$/
446
     */
447
    public function iTypeIntoTheDialog($data)
448
    {
449
        $data = array(
450
            'text' => $data,
451
        );
452
        $this->getSession()->getDriver()->getWebDriverSession()->postAlert_text($data);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Mink\Driver\DriverInterface as the method getWebDriverSession() does only exist in the following implementations of said interface: Behat\Mink\Driver\Selenium2Driver.

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...
453
    }
454
455
    /**
456
     * @Given /^I confirm the dialog$/
457
     */
458
    public function iConfirmTheDialog()
459
    {
460
        $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Mink\Driver\DriverInterface as the method getWebDriverSession() does only exist in the following implementations of said interface: Behat\Mink\Driver\Selenium2Driver.

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...
461
        $this->handleAjaxTimeout();
462
    }
463
464
    /**
465
     * @Given /^I dismiss the dialog$/
466
     */
467
    public function iDismissTheDialog()
468
    {
469
        $this->getSession()->getDriver()->getWebDriverSession()->dismiss_alert();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Mink\Driver\DriverInterface as the method getWebDriverSession() does only exist in the following implementations of said interface: Behat\Mink\Driver\Selenium2Driver.

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...
470
        $this->handleAjaxTimeout();
471
    }
472
473
    /**
474
     * @Given /^(?:|I )attach the file "(?P<path>[^"]*)" to "(?P<field>(?:[^"]|\\")*)" with HTML5$/
475
     */
476
    public function iAttachTheFileTo($field, $path)
477
    {
478
        // Remove wrapped button styling to make input field accessible to Selenium
479
        $js = <<<JS
480
var input = jQuery('[name="$field"]');
481
if(input.closest('.ss-uploadfield-item-info').length) {
482
    while(!input.parent().is('.ss-uploadfield-item-info')) input = input.unwrap();
483
}
484
JS;
485
486
        $this->getSession()->executeScript($js);
487
        $this->getSession()->wait(1000);
488
489
        return new Step\Given(sprintf('I attach the file "%s" to "%s"', $path, $field));
490
    }
491
492
    /**
493
     * Select an individual input from within a group, matched by the top-most label.
494
     *
495
     * @Given /^I select "([^"]*)" from "([^"]*)" input group$/
496
     */
497
    public function iSelectFromInputGroup($value, $labelText)
498
    {
499
        $page = $this->getSession()->getPage();
500
        $parent = null;
501
502
        foreach ($page->findAll('css', 'label') as $label) {
503
            if ($label->getText() == $labelText) {
504
                $parent = $label->getParent();
505
            }
506
        }
507
508
        if (!$parent) {
509
            throw new \InvalidArgumentException(sprintf('Input group with label "%s" cannot be found', $labelText));
510
        }
511
512
        foreach ($parent->findAll('css', 'label') as $option) {
513
            if ($option->getText() == $value) {
514
                $input = null;
515
516
                // First, look for inputs referenced by the "for" element on this label
517
                $for = $option->getAttribute('for');
518
                if ($for) {
519
                    $input = $parent->findById($for);
520
                }
521
522
                // Otherwise look for inputs _inside_ the label
523
                if (!$input) {
524
                    $input = $option->find('css', 'input');
525
                }
526
527
                if (!$input) {
528
                    throw new \InvalidArgumentException(sprintf('Input "%s" cannot be found', $value));
529
                }
530
531
                $this->getSession()->getDriver()->click($input->getXPath());
532
            }
533
        }
534
    }
535
536
    /**
537
     * Pauses the scenario until the user presses a key. Useful when debugging a scenario.
538
     *
539
     * @Then /^(?:|I )put a breakpoint$/
540
     */
541
    public function iPutABreakpoint()
542
    {
543
        fwrite(STDOUT, "\033[s    \033[93m[Breakpoint] Press \033[1;93m[RETURN]\033[0;93m to continue...\033[0m");
544
        while (fgets(STDIN, 1024) == '') {
545
        }
546
        fwrite(STDOUT, "\033[u");
547
548
        return;
549
    }
550
551
    /**
552
     * Transforms relative time statements compatible with strtotime().
553
     * Example: "time of 1 hour ago" might return "22:00:00" if its currently "23:00:00".
554
     * Customize through {@link setTimeFormat()}.
555
     *
556
     * @Transform /^(?:(the|a)) time of (?<val>.*)$/
557
     */
558 View Code Duplication
    public function castRelativeToAbsoluteTime($prefix, $val)
0 ignored issues
show
Unused Code introduced by
The parameter $prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
559
    {
560
        $timestamp = strtotime($val);
561
        if (!$timestamp) {
562
            throw new \InvalidArgumentException(sprintf(
563
                "Can't resolve '%s' into a valid datetime value",
564
                $val
565
            ));
566
        }
567
        return date($this->timeFormat, $timestamp);
568
    }
569
570
    /**
571
     * Transforms relative date and time statements compatible with strtotime().
572
     * Example: "datetime of 2 days ago" might return "2013-10-10 22:00:00" if its currently
573
     * the 12th of October 2013. Customize through {@link setDatetimeFormat()}.
574
     *
575
     * @Transform /^(?:(the|a)) datetime of (?<val>.*)$/
576
     */
577 View Code Duplication
    public function castRelativeToAbsoluteDatetime($prefix, $val)
0 ignored issues
show
Unused Code introduced by
The parameter $prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
578
    {
579
        $timestamp = strtotime($val);
580
        if (!$timestamp) {
581
            throw new \InvalidArgumentException(sprintf(
582
                "Can't resolve '%s' into a valid datetime value",
583
                $val
584
            ));
585
        }
586
        return date($this->datetimeFormat, $timestamp);
587
    }
588
589
    /**
590
     * Transforms relative date statements compatible with strtotime().
591
     * Example: "date 2 days ago" might return "2013-10-10" if its currently
592
     * the 12th of October 2013. Customize through {@link setDateFormat()}.
593
     *
594
     * @Transform /^(?:(the|a)) date of (?<val>.*)$/
595
     */
596 View Code Duplication
    public function castRelativeToAbsoluteDate($prefix, $val)
0 ignored issues
show
Unused Code introduced by
The parameter $prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
597
    {
598
        $timestamp = strtotime($val);
599
        if (!$timestamp) {
600
            throw new \InvalidArgumentException(sprintf(
601
                "Can't resolve '%s' into a valid datetime value",
602
                $val
603
            ));
604
        }
605
        return date($this->dateFormat, $timestamp);
606
    }
607
608
    public function getDateFormat()
609
    {
610
        return $this->dateFormat;
611
    }
612
613
    public function setDateFormat($format)
614
    {
615
        $this->dateFormat = $format;
616
    }
617
618
    public function getTimeFormat()
619
    {
620
        return $this->timeFormat;
621
    }
622
623
    public function setTimeFormat($format)
624
    {
625
        $this->timeFormat = $format;
626
    }
627
628
    public function getDatetimeFormat()
629
    {
630
        return $this->datetimeFormat;
631
    }
632
633
    public function setDatetimeFormat($format)
634
    {
635
        $this->datetimeFormat = $format;
636
    }
637
638
    /**
639
     * Checks that field with specified in|name|label|value is disabled.
640
     * Example: Then the field "Email" should be disabled
641
     * Example: Then the "Email" field should be disabled
642
     *
643
     * @Then /^the "(?P<name>(?:[^"]|\\")*)" (?P<type>(?:(field|button))) should (?P<negate>(?:(not |)))be disabled/
644
     * @Then /^the (?P<type>(?:(field|button))) "(?P<name>(?:[^"]|\\")*)" should (?P<negate>(?:(not |)))be disabled/
645
     */
646
    public function stepFieldShouldBeDisabled($name, $type, $negate)
647
    {
648
        $page = $this->getSession()->getPage();
649
        if ($type == 'field') {
650
            $element = $page->findField($name);
651
        } else {
652
            $element = $page->find('named', array(
653
                'button', $this->getSession()->getSelectorsHandler()->xpathLiteral($name)
654
            ));
655
        }
656
657
        assertNotNull($element, sprintf("Element '%s' not found", $name));
658
659
        $disabledAttribute = $element->getAttribute('disabled');
660
        if (trim($negate)) {
661
            assertNull($disabledAttribute, sprintf("Failed asserting element '%s' is not disabled", $name));
662
        } else {
663
            assertNotNull($disabledAttribute, sprintf("Failed asserting element '%s' is disabled", $name));
664
        }
665
    }
666
667
    /**
668
     * Checks that checkbox with specified in|name|label|value is enabled.
669
     * Example: Then the field "Email" should be enabled
670
     * Example: Then the "Email" field should be enabled
671
     *
672
     * @Then /^the "(?P<field>(?:[^"]|\\")*)" field should be enabled/
673
     * @Then /^the field "(?P<field>(?:[^"]|\\")*)" should be enabled/
674
     */
675
    public function stepFieldShouldBeEnabled($field)
676
    {
677
        $page = $this->getSession()->getPage();
678
        $fieldElement = $page->findField($field);
679
        assertNotNull($fieldElement, sprintf("Field '%s' not found", $field));
680
681
        $disabledAttribute = $fieldElement->getAttribute('disabled');
682
683
        assertNull($disabledAttribute, sprintf("Failed asserting field '%s' is enabled", $field));
684
    }
685
686
    /**
687
     * Clicks a link in a specific region (an element identified by a CSS selector, a "data-title" attribute,
688
     * or a named region mapped to a CSS selector via Behat configuration).
689
     *
690
     * Example: Given I follow "Select" in the "header .login-form" region
691
     * Example: Given I follow "Select" in the "My Login Form" region
692
     *
693
     * @Given /^I (?:follow|click) "(?P<link>[^"]*)" in the "(?P<region>[^"]*)" region$/
694
     */
695 View Code Duplication
    public function iFollowInTheRegion($link, $region)
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...
696
    {
697
        $context = $this->getMainContext();
698
        $regionObj = $context->getRegionObj($region);
699
        assertNotNull($regionObj);
700
701
        $linkObj = $regionObj->findLink($link);
702
        if (empty($linkObj)) {
703
            throw new \Exception(sprintf('The link "%s" was not found in the region "%s" 
704
				on the page %s', $link, $region, $this->getSession()->getCurrentUrl()));
705
        }
706
707
        $linkObj->click();
708
    }
709
710
    /**
711
     * Fills in a field in a specfic region similar to (@see iFollowInTheRegion or @see iSeeTextInRegion)
712
     *
713
     * Example: Given I fill in "Hello" with "World"
714
     *
715
     * @Given /^I fill in "(?P<field>[^"]*)" with "(?P<value>[^"]*)" in the "(?P<region>[^"]*)" region$/
716
     */
717 View Code Duplication
    public function iFillinTheRegion($field, $value, $region)
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...
718
    {
719
        $context = $this->getMainContext();
720
        $regionObj = $context->getRegionObj($region);
721
        assertNotNull($regionObj, "Region Object is null");
722
723
        $fieldObj = $regionObj->findField($field);
724
        if (empty($fieldObj)) {
725
            throw new \Exception(sprintf('The field "%s" was not found in the region "%s" 
726
				on the page %s', $field, $region, $this->getSession()->getCurrentUrl()));
727
        }
728
729
        $regionObj->fillField($field, $value);
730
    }
731
732
733
    /**
734
     * Asserts text in a specific region (an element identified by a CSS selector, a "data-title" attribute,
735
     * or a named region mapped to a CSS selector via Behat configuration).
736
     * Supports regular expressions in text value.
737
     *
738
     * Example: Given I should see "My Text" in the "header .login-form" region
739
     * Example: Given I should not see "My Text" in the "My Login Form" region
740
     *
741
     * @Given /^I should (?P<negate>(?:(not |)))see "(?P<text>[^"]*)" in the "(?P<region>[^"]*)" region$/
742
     */
743
    public function iSeeTextInRegion($negate, $text, $region)
744
    {
745
        $context = $this->getMainContext();
746
        $regionObj = $context->getRegionObj($region);
747
        assertNotNull($regionObj);
748
749
        $actual = $regionObj->getText();
750
        $actual = preg_replace('/\s+/u', ' ', $actual);
751
        $regex  = '/'.preg_quote($text, '/').'/ui';
752
753
        if (trim($negate)) {
754 View Code Duplication
            if (preg_match($regex, $actual)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
755
                $message = sprintf(
756
                    'The text "%s" was found in the text of the "%s" region on the page %s.',
757
                    $text,
758
                    $region,
759
                    $this->getSession()->getCurrentUrl()
760
                );
761
762
                throw new \Exception($message);
763
            }
764 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
765
            if (!preg_match($regex, $actual)) {
766
                $message = sprintf(
767
                    'The text "%s" was not found anywhere in the text of the "%s" region on the page %s.',
768
                    $text,
769
                    $region,
770
                    $this->getSession()->getCurrentUrl()
771
                );
772
773
                throw new \Exception($message);
774
            }
775
        }
776
    }
777
778
    /**
779
     * Selects the specified radio button
780
     *
781
     * @Given /^I select the "([^"]*)" radio button$/
782
     */
783
    public function iSelectTheRadioButton($radioLabel)
784
    {
785
        $session = $this->getSession();
786
        $radioButton = $session->getPage()->find('named', array(
787
                      'radio', $this->getSession()->getSelectorsHandler()->xpathLiteral($radioLabel)
788
                  ));
789
        assertNotNull($radioButton);
790
        $session->getDriver()->click($radioButton->getXPath());
791
    }
792
793
    /**
794
     * @Then /^the "([^"]*)" table should contain "([^"]*)"$/
795
     */
796 View Code Duplication
    public function theTableShouldContain($selector, $text)
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...
797
    {
798
        $table = $this->getTable($selector);
799
800
        $element = $table->find('named', array('content', "'$text'"));
801
        assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
802
    }
803
804
    /**
805
     * @Then /^the "([^"]*)" table should not contain "([^"]*)"$/
806
     */
807 View Code Duplication
    public function theTableShouldNotContain($selector, $text)
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...
808
    {
809
        $table = $this->getTable($selector);
810
811
        $element = $table->find('named', array('content', "'$text'"));
812
        assertNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
813
    }
814
815
    /**
816
     * @Given /^I click on "([^"]*)" in the "([^"]*)" table$/
817
     */
818 View Code Duplication
    public function iClickOnInTheTable($text, $selector)
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...
819
    {
820
        $table = $this->getTable($selector);
821
822
        $element = $table->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
823
        assertNotNull($element, sprintf('Element containing `%s` not found', $text));
824
        $element->click();
825
    }
826
827
    /**
828
     * Finds the first visible table by various factors:
829
     * - table[id]
830
     * - table[title]
831
     * - table *[class=title]
832
     * - fieldset[data-name] table
833
     * - table caption
834
     *
835
     * @return Behat\Mink\Element\NodeElement
836
     */
837
    protected function getTable($selector)
838
    {
839
        $selector = $this->getSession()->getSelectorsHandler()->xpathLiteral($selector);
840
        $page = $this->getSession()->getPage();
841
        $candidates = $page->findAll(
842
            'xpath',
843
            $this->getSession()->getSelectorsHandler()->selectorToXpath(
844
                "xpath", ".//table[(./@id = $selector or  contains(./@title, $selector))]"
845
            )
846
        );
847
848
        // Find tables by a <caption> field
849
        $candidates += $page->findAll('xpath', "//table//caption[contains(normalize-space(string(.)), 
850
			$selector)]/ancestor-or-self::table[1]");
851
852
        // Find tables by a .title node
853
        $candidates += $page->findAll('xpath', "//table//*[contains(concat(' ',normalize-space(@class),' '), ' title ') and contains(normalize-space(string(.)), 
854
			$selector)]/ancestor-or-self::table[1]");
855
856
        // Some tables don't have a visible title, so look for a fieldset with data-name instead
857
        $candidates += $page->findAll('xpath', "//fieldset[@data-name=$selector]//table");
858
859
        assertTrue((bool)$candidates, 'Could not find any table elements');
860
861
        $table = null;
862
        foreach ($candidates as $candidate) {
863
            if (!$table && $candidate->isVisible()) {
864
                $table = $candidate;
865
            }
866
        }
867
868
        assertTrue((bool)$table, 'Found table elements, but none are visible');
869
870
        return $table;
871
    }
872
873
    /**
874
     * Checks the order of two texts.
875
     * Assumptions: the two texts appear in their conjunct parent element once
876
     * @Then /^I should see the text "(?P<textBefore>(?:[^"]|\\")*)" (before|after) the text "(?P<textAfter>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
877
     */
878
    public function theTextBeforeAfter($textBefore, $order, $textAfter, $element)
879
    {
880
        $ele = $this->getSession()->getPage()->find('css', $element);
881
        assertNotNull($ele, sprintf('%s not found', $element));
882
883
        // Check both of the texts exist in the element
884
        $text = $ele->getText();
885
        assertTrue(strpos($text, $textBefore) !== 'FALSE', sprintf('%s not found in the element %s', $textBefore, $element));
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of strpos($text, $textBefore) (integer) and 'FALSE' (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
886
        assertTrue(strpos($text, $textAfter) !== 'FALSE', sprintf('%s not found in the element %s', $textAfter, $element));
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of strpos($text, $textAfter) (integer) and 'FALSE' (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
887
888
        /// Use strpos to get the position of the first occurrence of the two texts (case-sensitive)
889
        // and compare them with the given order (before or after)
890
        if ($order === 'before') {
891
            assertTrue(strpos($text, $textBefore) < strpos($text, $textAfter));
892
        } else {
893
            assertTrue(strpos($text, $textBefore) > strpos($text, $textAfter));
894
        }
895
    }
896
897
    /**
898
    * Wait until a certain amount of seconds till I see an element  identified by a CSS selector.
899
    *
900
    * Example: Given I wait for 10 seconds until I see the ".css_element" element
901
    *
902
    * @Given /^I wait for (\d+) seconds until I see the "([^"]*)" element$/
903
    **/
904 View Code Duplication
    public function iWaitXUntilISee($wait, $selector)
0 ignored issues
show
Unused Code introduced by
The parameter $wait is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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...
905
    {
906
        $page = $this->getSession()->getPage();
907
908
        $this->spin(function ($page) use ($page, $selector) {
0 ignored issues
show
Bug Best Practice introduced by
The parameter name $page conflicts with one of the imported variables.
Loading history...
909
            $element = $page->find('css', $selector);
910
911
            if (empty($element)) {
912
                return false;
913
            } else {
914
                return $element->isVisible();
915
            }
916
        });
917
    }
918
919
    /**
920
     * Wait until a particular element is visible, using a CSS selector. Useful for content loaded via AJAX, or only
921
     * populated after JS execution.
922
     *
923
     * Example: Given I wait until I see the "header .login-form" element
924
     *
925
     * @Given /^I wait until I see the "([^"]*)" element$/
926
     */
927 View Code Duplication
    public function iWaitUntilISee($selector)
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...
928
    {
929
        $page = $this->getSession()->getPage();
930
        $this->spin(function ($page) use ($page, $selector) {
0 ignored issues
show
Bug Best Practice introduced by
The parameter name $page conflicts with one of the imported variables.
Loading history...
931
            $element = $page->find('css', $selector);
932
            if (empty($element)) {
933
                return false;
934
            } else {
935
                return ($element->isVisible());
936
            }
937
        });
938
    }
939
940
    /**
941
     * Wait until a particular string is found on the page. Useful for content loaded via AJAX, or only populated after
942
     * JS execution.
943
     *
944
     * Example: Given I wait until I see the text "Welcome back, John!"
945
     *
946
     * @Given /^I wait until I see the text "([^"]*)"$/
947
     */
948
    public function iWaitUntilISeeText($text)
949
    {
950
        $page = $this->getSession()->getPage();
951
        $session = $this->getSession();
952
        $this->spin(function ($page) use ($page, $session, $text) {
0 ignored issues
show
Bug Best Practice introduced by
The parameter name $page conflicts with one of the imported variables.
Loading history...
953
            $element = $page->find(
954
                'xpath',
955
                $session->getSelectorsHandler()->selectorToXpath("xpath", ".//*[contains(text(), '$text')]")
956
            );
957
958
            if (empty($element)) {
959
                return false;
960
            } else {
961
                return ($element->isVisible());
962
            }
963
        });
964
    }
965
966
    /**
967
     * @Given /^I scroll to the bottom$/
968
     */
969
    public function iScrollToBottom()
970
    {
971
        $javascript = 'window.scrollTo(0, Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, document.documentElement.clientHeight));';
972
        $this->getSession()->executeScript($javascript);
973
    }
974
975
    /**
976
     * @Given /^I scroll to the top$/
977
     */
978
    public function iScrollToTop()
979
    {
980
        $this->getSession()->executeScript('window.scrollTo(0,0);');
981
    }
982
983
    /**
984
     * Scroll to a certain element by label.
985
     * Requires an "id" attribute to uniquely identify the element in the document.
986
     *
987
     * Example: Given I scroll to the "Submit" button
988
     * Example: Given I scroll to the "My Date" field
989
     *
990
     * @Given /^I scroll to the "([^"]*)" (field|link|button)$/
991
     */
992
    public function iScrollToField($locator, $type)
993
    {
994
        $page = $this->getSession()->getPage();
995
        $el = $page->find('named', array($type, "'$locator'"));
996
        assertNotNull($el, sprintf('%s element not found', $locator));
997
998
        $id = $el->getAttribute('id');
999
        if (empty($id)) {
1000
            throw new \InvalidArgumentException('Element requires an "id" attribute');
1001
        }
1002
1003
        $js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
1004
        $this->getSession()->executeScript($js);
1005
    }
1006
1007
    /**
1008
     * Scroll to a certain element by CSS selector.
1009
     * Requires an "id" attribute to uniquely identify the element in the document.
1010
     *
1011
     * Example: Given I scroll to the ".css_element" element
1012
     *
1013
     * @Given /^I scroll to the "(?P<locator>(?:[^"]|\\")*)" element$/
1014
     */
1015
    public function iScrollToElement($locator)
1016
    {
1017
        $el = $this->getSession()->getPage()->find('css', $locator);
1018
        assertNotNull($el, sprintf('The element "%s" is not found', $locator));
1019
1020
        $id = $el->getAttribute('id');
1021
        if (empty($id)) {
1022
            throw new \InvalidArgumentException('Element requires an "id" attribute');
1023
        }
1024
1025
        $js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
1026
        $this->getSession()->executeScript($js);
1027
    }
1028
1029
    /**
1030
     * Continuously poll the dom until callback returns true, code copied from
1031
     * (@link http://docs.behat.org/cookbook/using_spin_functions.html)
1032
     * If not found within a given wait period, timeout and throw error
1033
     *
1034
     * @param callback $lambda The function to run continuously
1035
     * @param integer $wait Timeout in seconds
1036
     * @return bool Returns true if the lambda returns successfully
1037
     * @throws \Exception Thrown if the wait threshold is exceeded without the lambda successfully returning
1038
     */
1039
    public function spin($lambda, $wait = 60)
1040
    {
1041
        for ($i = 0; $i < $wait; $i++) {
1042
            try {
1043
                if ($lambda($this)) {
1044
                    return true;
1045
                }
1046
            } catch (\Exception $e) {
1047
                // do nothing
1048
            }
1049
1050
            sleep(1);
1051
        }
1052
1053
        $backtrace = debug_backtrace();
1054
1055
        throw new \Exception(sprintf(
1056
            "Timeout thrown by %s::%s()\n.",
1057
            $backtrace[1]['class'],
1058
            $backtrace[1]['function']
1059
        ));
1060
    }
1061
1062
1063
1064
    /**
1065
     * We have to catch exceptions and log somehow else otherwise behat falls over
1066
     */
1067
    protected function logException($e)
1068
    {
1069
        file_put_contents('php://stderr', 'Exception caught: '.$e);
1070
    }
1071
}
1072