Completed
Push — master ( 987505...ce3c0b )
by Hamish
8s
created

BasicContext::readErrorHandlerAfterStep()   B

Complexity

Conditions 3
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
c 8
b 0
f 0
nc 8
nop 1
dl 0
loc 24
rs 8.9713
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(
219
            $timeoutMs,
220
            "(typeof window.__ajaxStatus !== 'undefined' ? window.__ajaxStatus() : 'no ajax') !== 'waiting'"
221
        );
222
223
        // wait additional 100ms to allow DOM to update
224
        $this->getSession()->wait(100);
225
    }
226
227
    /**
228
     * Take screenshot when step fails.
229
     * Works only with Selenium2Driver.
230
     *
231
     * @AfterStep
232
     */
233
    public function takeScreenshotAfterFailedStep(StepEvent $event)
234
    {
235
        if (4 === $event->getResult()) {
236
            try {
237
                $this->takeScreenshot($event);
238
            } catch (\WebDriver\Exception $e) {
239
                $this->logException($e);
240
            }
241
        }
242
    }
243
244
    /**
245
     * Close modal dialog if test scenario fails on CMS page
246
     *
247
     * @AfterScenario
248
     */
249
    public function closeModalDialog(ScenarioEvent $event)
250
    {
251
        try {
252
            // Only for failed tests on CMS page
253
            if (4 === $event->getResult()) {
254
                $cmsElement = $this->getSession()->getPage()->find('css', '.cms');
255
                if ($cmsElement) {
256
                    try {
257
                        // Navigate away triggered by reloading the page
258
                        $this->getSession()->reload();
259
                        $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...
260
                    } catch (\WebDriver\Exception $e) {
261
                        // no-op, alert might not be present
262
                    }
263
                }
264
            }
265
        } catch (\WebDriver\Exception $e) {
266
            $this->logException($e);
267
        }
268
    }
269
270
    /**
271
     * Delete any created files and folders from assets directory
272
     *
273
     * @AfterScenario @assets
274
     */
275
    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...
276
    {
277
        foreach (\File::get() as $file) {
278
            $file->delete();
279
        }
280
        \Filesystem::removeFolder(ASSETS_PATH, true);
281
    }
282
283
    public function takeScreenshot(StepEvent $event)
284
    {
285
        $driver = $this->getSession()->getDriver();
286
        // quit silently when unsupported
287
        if (!($driver instanceof Selenium2Driver)) {
288
            return;
289
        }
290
291
        $parent = $event->getLogicalParent();
292
        $feature = $parent->getFeature();
293
        $step = $event->getStep();
294
        $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...
295
296
        $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...
297
        if (!$path) {
298
            return;
299
        } // quit silently when path is not set
300
301
        \Filesystem::makeFolder($path);
302
        $path = realpath($path);
303
304 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...
305
            file_put_contents('php://stderr', sprintf('"%s" is not valid directory and failed to create it' . PHP_EOL, $path));
306
            return;
307
        }
308
309 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...
310
            file_put_contents('php://stderr', sprintf('"%s" is not valid directory' . PHP_EOL, $path));
311
            return;
312
        }
313 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...
314
            file_put_contents('php://stderr', sprintf('"%s" directory is not writable' . PHP_EOL, $path));
315
            return;
316
        }
317
318
        $path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine());
319
        $screenshot = $driver->getWebDriverSession()->screenshot();
320
        file_put_contents($path, base64_decode($screenshot));
321
        file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path));
322
    }
323
324
    /**
325
     * @Then /^I should be redirected to "([^"]+)"/
326
     */
327
    public function stepIShouldBeRedirectedTo($url)
328
    {
329
        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...
330
            $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...
331
            $client->followRedirects(true);
332
            $client->followRedirect();
333
334
            $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...
335
336
            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...
337
        }
338
    }
339
340
    /**
341
     * @Given /^the page can't be found/
342
     */
343
    public function stepPageCantBeFound()
344
    {
345
        $page = $this->getSession()->getPage();
346
        assertTrue(
347
            // Content from ErrorPage default record
348
            $page->hasContent('Page not found')
349
            // Generic ModelAsController message
350
            || $page->hasContent('The requested page could not be found')
351
        );
352
    }
353
354
    /**
355
     * @Given /^I wait (?:for )?([\d\.]+) second(?:s?)$/
356
     */
357
    public function stepIWaitFor($secs)
358
    {
359
        $this->getSession()->wait((float)$secs*1000);
360
    }
361
362
    /**
363
     * @Given /^I press the "([^"]*)" button$/
364
     */
365
    public function stepIPressTheButton($button)
366
    {
367
        $page = $this->getSession()->getPage();
368
        $els = $page->findAll('named', array('link_or_button', "'$button'"));
369
        $matchedEl = null;
370
        foreach ($els as $el) {
371
            if ($el->isVisible()) {
372
                $matchedEl = $el;
373
            }
374
        }
375
        assertNotNull($matchedEl, sprintf('%s button not found', $button));
376
        $matchedEl->click();
377
    }
378
379
    /**
380
     * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
381
     * Example1: I press the "Remove current combo" button, confirming the dialog
382
     * Example2: I follow the "Remove current combo" link, confirming the dialog
383
     *
384
     * @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), confirming the dialog$/
385
     */
386
    public function stepIPressTheButtonConfirmingTheDialog($button)
387
    {
388
        $this->stepIPressTheButton($button);
389
        $this->iConfirmTheDialog();
390
    }
391
392
    /**
393
     * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
394
     * Example: I follow the "Remove current combo" link, dismissing the dialog
395
     *
396
     * @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), dismissing the dialog$/
397
     */
398
    public function stepIPressTheButtonDismissingTheDialog($button)
399
    {
400
        $this->stepIPressTheButton($button);
401
        $this->iDismissTheDialog();
402
    }
403
404
    /**
405
     * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element$/
406
     */
407
    public function iClickInTheElement($clickType, $text, $selector)
408
    {
409
        $clickTypeMap = array(
410
            "double click" => "doubleclick",
411
            "click" => "click"
412
        );
413
        $page = $this->getSession()->getPage();
414
        $parentElement = $page->find('css', $selector);
415
        assertNotNull($parentElement, sprintf('"%s" element not found', $selector));
416
        $element = $parentElement->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
417
        assertNotNull($element, sprintf('"%s" not found', $text));
418
        $clickTypeFn = $clickTypeMap[$clickType];
419
        $element->$clickTypeFn();
420
    }
421
422
    /**
423
    * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
424
    * Example: I click "Delete" in the ".actions" element, confirming the dialog
425
    *
426
    * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, confirming the dialog$/
427
    */
428
    public function iClickInTheElementConfirmingTheDialog($clickType, $text, $selector)
429
    {
430
        $this->iClickInTheElement($clickType, $text, $selector);
431
        $this->iConfirmTheDialog();
432
    }
433
   /**
434
    * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
435
    * Example: I click "Delete" in the ".actions" element, dismissing the dialog
436
    *
437
    * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, dismissing the dialog$/
438
    */
439
    public function iClickInTheElementDismissingTheDialog($clickType, $text, $selector)
440
    {
441
        $this->iClickInTheElement($clickType, $text, $selector);
442
        $this->iDismissTheDialog();
443
    }
444
445
    /**
446
     * @Given /^I type "([^"]*)" into the dialog$/
447
     */
448
    public function iTypeIntoTheDialog($data)
449
    {
450
        $data = array(
451
            'text' => $data,
452
        );
453
        $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...
454
    }
455
456
    /**
457
     * @Given /^I confirm the dialog$/
458
     */
459
    public function iConfirmTheDialog()
460
    {
461
        $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...
462
        $this->handleAjaxTimeout();
463
    }
464
465
    /**
466
     * @Given /^I dismiss the dialog$/
467
     */
468
    public function iDismissTheDialog()
469
    {
470
        $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...
471
        $this->handleAjaxTimeout();
472
    }
473
474
    /**
475
     * @Given /^(?:|I )attach the file "(?P<path>[^"]*)" to "(?P<field>(?:[^"]|\\")*)" with HTML5$/
476
     */
477
    public function iAttachTheFileTo($field, $path)
478
    {
479
        // Remove wrapped button styling to make input field accessible to Selenium
480
        $js = <<<JS
481
var input = jQuery('[name="$field"]');
482
if(input.closest('.ss-uploadfield-item-info').length) {
483
    while(!input.parent().is('.ss-uploadfield-item-info')) input = input.unwrap();
484
}
485
JS;
486
487
        $this->getSession()->executeScript($js);
488
        $this->getSession()->wait(1000);
489
490
        return new Step\Given(sprintf('I attach the file "%s" to "%s"', $path, $field));
491
    }
492
493
    /**
494
     * Select an individual input from within a group, matched by the top-most label.
495
     *
496
     * @Given /^I select "([^"]*)" from "([^"]*)" input group$/
497
     */
498
    public function iSelectFromInputGroup($value, $labelText)
499
    {
500
        $page = $this->getSession()->getPage();
501
        $parent = null;
502
503
        foreach ($page->findAll('css', 'label') as $label) {
504
            if ($label->getText() == $labelText) {
505
                $parent = $label->getParent();
506
            }
507
        }
508
509
        if (!$parent) {
510
            throw new \InvalidArgumentException(sprintf('Input group with label "%s" cannot be found', $labelText));
511
        }
512
513
        foreach ($parent->findAll('css', 'label') as $option) {
514
            if ($option->getText() == $value) {
515
                $input = null;
516
517
                // First, look for inputs referenced by the "for" element on this label
518
                $for = $option->getAttribute('for');
519
                if ($for) {
520
                    $input = $parent->findById($for);
521
                }
522
523
                // Otherwise look for inputs _inside_ the label
524
                if (!$input) {
525
                    $input = $option->find('css', 'input');
526
                }
527
528
                if (!$input) {
529
                    throw new \InvalidArgumentException(sprintf('Input "%s" cannot be found', $value));
530
                }
531
532
                $this->getSession()->getDriver()->click($input->getXPath());
533
            }
534
        }
535
    }
536
537
    /**
538
     * Pauses the scenario until the user presses a key. Useful when debugging a scenario.
539
     *
540
     * @Then /^(?:|I )put a breakpoint$/
541
     */
542
    public function iPutABreakpoint()
543
    {
544
        fwrite(STDOUT, "\033[s    \033[93m[Breakpoint] Press \033[1;93m[RETURN]\033[0;93m to continue...\033[0m");
545
        while (fgets(STDIN, 1024) == '') {
546
        }
547
        fwrite(STDOUT, "\033[u");
548
549
        return;
550
    }
551
552
    /**
553
     * Transforms relative time statements compatible with strtotime().
554
     * Example: "time of 1 hour ago" might return "22:00:00" if its currently "23:00:00".
555
     * Customize through {@link setTimeFormat()}.
556
     *
557
     * @Transform /^(?:(the|a)) time of (?<val>.*)$/
558
     */
559 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...
560
    {
561
        $timestamp = strtotime($val);
562
        if (!$timestamp) {
563
            throw new \InvalidArgumentException(sprintf(
564
                "Can't resolve '%s' into a valid datetime value",
565
                $val
566
            ));
567
        }
568
        return date($this->timeFormat, $timestamp);
569
    }
570
571
    /**
572
     * Transforms relative date and time statements compatible with strtotime().
573
     * Example: "datetime of 2 days ago" might return "2013-10-10 22:00:00" if its currently
574
     * the 12th of October 2013. Customize through {@link setDatetimeFormat()}.
575
     *
576
     * @Transform /^(?:(the|a)) datetime of (?<val>.*)$/
577
     */
578 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...
579
    {
580
        $timestamp = strtotime($val);
581
        if (!$timestamp) {
582
            throw new \InvalidArgumentException(sprintf(
583
                "Can't resolve '%s' into a valid datetime value",
584
                $val
585
            ));
586
        }
587
        return date($this->datetimeFormat, $timestamp);
588
    }
589
590
    /**
591
     * Transforms relative date statements compatible with strtotime().
592
     * Example: "date 2 days ago" might return "2013-10-10" if its currently
593
     * the 12th of October 2013. Customize through {@link setDateFormat()}.
594
     *
595
     * @Transform /^(?:(the|a)) date of (?<val>.*)$/
596
     */
597 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...
598
    {
599
        $timestamp = strtotime($val);
600
        if (!$timestamp) {
601
            throw new \InvalidArgumentException(sprintf(
602
                "Can't resolve '%s' into a valid datetime value",
603
                $val
604
            ));
605
        }
606
        return date($this->dateFormat, $timestamp);
607
    }
608
609
    public function getDateFormat()
610
    {
611
        return $this->dateFormat;
612
    }
613
614
    public function setDateFormat($format)
615
    {
616
        $this->dateFormat = $format;
617
    }
618
619
    public function getTimeFormat()
620
    {
621
        return $this->timeFormat;
622
    }
623
624
    public function setTimeFormat($format)
625
    {
626
        $this->timeFormat = $format;
627
    }
628
629
    public function getDatetimeFormat()
630
    {
631
        return $this->datetimeFormat;
632
    }
633
634
    public function setDatetimeFormat($format)
635
    {
636
        $this->datetimeFormat = $format;
637
    }
638
639
    /**
640
     * Checks that field with specified in|name|label|value is disabled.
641
     * Example: Then the field "Email" should be disabled
642
     * Example: Then the "Email" field should be disabled
643
     *
644
     * @Then /^the "(?P<name>(?:[^"]|\\")*)" (?P<type>(?:(field|button))) should (?P<negate>(?:(not |)))be disabled/
645
     * @Then /^the (?P<type>(?:(field|button))) "(?P<name>(?:[^"]|\\")*)" should (?P<negate>(?:(not |)))be disabled/
646
     */
647
    public function stepFieldShouldBeDisabled($name, $type, $negate)
648
    {
649
        $page = $this->getSession()->getPage();
650
        if ($type == 'field') {
651
            $element = $page->findField($name);
652
        } else {
653
            $element = $page->find('named', array(
654
                'button', $this->getSession()->getSelectorsHandler()->xpathLiteral($name)
655
            ));
656
        }
657
658
        assertNotNull($element, sprintf("Element '%s' not found", $name));
659
660
        $disabledAttribute = $element->getAttribute('disabled');
661
        if (trim($negate)) {
662
            assertNull($disabledAttribute, sprintf("Failed asserting element '%s' is not disabled", $name));
663
        } else {
664
            assertNotNull($disabledAttribute, sprintf("Failed asserting element '%s' is disabled", $name));
665
        }
666
    }
667
668
    /**
669
     * Checks that checkbox with specified in|name|label|value is enabled.
670
     * Example: Then the field "Email" should be enabled
671
     * Example: Then the "Email" field should be enabled
672
     *
673
     * @Then /^the "(?P<field>(?:[^"]|\\")*)" field should be enabled/
674
     * @Then /^the field "(?P<field>(?:[^"]|\\")*)" should be enabled/
675
     */
676
    public function stepFieldShouldBeEnabled($field)
677
    {
678
        $page = $this->getSession()->getPage();
679
        $fieldElement = $page->findField($field);
680
        assertNotNull($fieldElement, sprintf("Field '%s' not found", $field));
681
682
        $disabledAttribute = $fieldElement->getAttribute('disabled');
683
684
        assertNull($disabledAttribute, sprintf("Failed asserting field '%s' is enabled", $field));
685
    }
686
687
    /**
688
     * Clicks a link in a specific region (an element identified by a CSS selector, a "data-title" attribute,
689
     * or a named region mapped to a CSS selector via Behat configuration).
690
     *
691
     * Example: Given I follow "Select" in the "header .login-form" region
692
     * Example: Given I follow "Select" in the "My Login Form" region
693
     *
694
     * @Given /^I (?:follow|click) "(?P<link>[^"]*)" in the "(?P<region>[^"]*)" region$/
695
     */
696 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...
697
    {
698
        $context = $this->getMainContext();
699
        $regionObj = $context->getRegionObj($region);
700
        assertNotNull($regionObj);
701
702
        $linkObj = $regionObj->findLink($link);
703
        if (empty($linkObj)) {
704
            throw new \Exception(sprintf('The link "%s" was not found in the region "%s" 
705
				on the page %s', $link, $region, $this->getSession()->getCurrentUrl()));
706
        }
707
708
        $linkObj->click();
709
    }
710
711
    /**
712
     * Fills in a field in a specfic region similar to (@see iFollowInTheRegion or @see iSeeTextInRegion)
713
     *
714
     * Example: Given I fill in "Hello" with "World"
715
     *
716
     * @Given /^I fill in "(?P<field>[^"]*)" with "(?P<value>[^"]*)" in the "(?P<region>[^"]*)" region$/
717
     */
718 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...
719
    {
720
        $context = $this->getMainContext();
721
        $regionObj = $context->getRegionObj($region);
722
        assertNotNull($regionObj, "Region Object is null");
723
724
        $fieldObj = $regionObj->findField($field);
725
        if (empty($fieldObj)) {
726
            throw new \Exception(sprintf('The field "%s" was not found in the region "%s" 
727
				on the page %s', $field, $region, $this->getSession()->getCurrentUrl()));
728
        }
729
730
        $regionObj->fillField($field, $value);
731
    }
732
733
734
    /**
735
     * Asserts text in a specific region (an element identified by a CSS selector, a "data-title" attribute,
736
     * or a named region mapped to a CSS selector via Behat configuration).
737
     * Supports regular expressions in text value.
738
     *
739
     * Example: Given I should see "My Text" in the "header .login-form" region
740
     * Example: Given I should not see "My Text" in the "My Login Form" region
741
     *
742
     * @Given /^I should (?P<negate>(?:(not |)))see "(?P<text>[^"]*)" in the "(?P<region>[^"]*)" region$/
743
     */
744
    public function iSeeTextInRegion($negate, $text, $region)
745
    {
746
        $context = $this->getMainContext();
747
        $regionObj = $context->getRegionObj($region);
748
        assertNotNull($regionObj);
749
750
        $actual = $regionObj->getText();
751
        $actual = preg_replace('/\s+/u', ' ', $actual);
752
        $regex  = '/'.preg_quote($text, '/').'/ui';
753
754
        if (trim($negate)) {
755 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...
756
                $message = sprintf(
757
                    'The text "%s" was found in the text of the "%s" region on the page %s.',
758
                    $text,
759
                    $region,
760
                    $this->getSession()->getCurrentUrl()
761
                );
762
763
                throw new \Exception($message);
764
            }
765 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...
766
            if (!preg_match($regex, $actual)) {
767
                $message = sprintf(
768
                    'The text "%s" was not found anywhere in the text of the "%s" region on the page %s.',
769
                    $text,
770
                    $region,
771
                    $this->getSession()->getCurrentUrl()
772
                );
773
774
                throw new \Exception($message);
775
            }
776
        }
777
    }
778
779
    /**
780
     * Selects the specified radio button
781
     *
782
     * @Given /^I select the "([^"]*)" radio button$/
783
     */
784
    public function iSelectTheRadioButton($radioLabel)
785
    {
786
        $session = $this->getSession();
787
        $radioButton = $session->getPage()->find('named', array(
788
                      'radio', $this->getSession()->getSelectorsHandler()->xpathLiteral($radioLabel)
789
                  ));
790
        assertNotNull($radioButton);
791
        $session->getDriver()->click($radioButton->getXPath());
792
    }
793
794
    /**
795
     * @Then /^the "([^"]*)" table should contain "([^"]*)"$/
796
     */
797 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...
798
    {
799
        $table = $this->getTable($selector);
800
801
        $element = $table->find('named', array('content', "'$text'"));
802
        assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
803
    }
804
805
    /**
806
     * @Then /^the "([^"]*)" table should not contain "([^"]*)"$/
807
     */
808 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...
809
    {
810
        $table = $this->getTable($selector);
811
812
        $element = $table->find('named', array('content', "'$text'"));
813
        assertNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
814
    }
815
816
    /**
817
     * @Given /^I click on "([^"]*)" in the "([^"]*)" table$/
818
     */
819 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...
820
    {
821
        $table = $this->getTable($selector);
822
823
        $element = $table->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
824
        assertNotNull($element, sprintf('Element containing `%s` not found', $text));
825
        $element->click();
826
    }
827
828
    /**
829
     * Finds the first visible table by various factors:
830
     * - table[id]
831
     * - table[title]
832
     * - table *[class=title]
833
     * - fieldset[data-name] table
834
     * - table caption
835
     *
836
     * @return Behat\Mink\Element\NodeElement
837
     */
838
    protected function getTable($selector)
839
    {
840
        $selector = $this->getSession()->getSelectorsHandler()->xpathLiteral($selector);
841
        $page = $this->getSession()->getPage();
842
        $candidates = $page->findAll(
843
            'xpath',
844
            $this->getSession()->getSelectorsHandler()->selectorToXpath(
845
                "xpath",
846
                ".//table[(./@id = $selector or  contains(./@title, $selector))]"
847
            )
848
        );
849
850
        // Find tables by a <caption> field
851
        $candidates += $page->findAll('xpath', "//table//caption[contains(normalize-space(string(.)), 
852
			$selector)]/ancestor-or-self::table[1]");
853
854
        // Find tables by a .title node
855
        $candidates += $page->findAll('xpath', "//table//*[contains(concat(' ',normalize-space(@class),' '), ' title ') and contains(normalize-space(string(.)), 
856
			$selector)]/ancestor-or-self::table[1]");
857
858
        // Some tables don't have a visible title, so look for a fieldset with data-name instead
859
        $candidates += $page->findAll('xpath', "//fieldset[@data-name=$selector]//table");
860
861
        assertTrue((bool)$candidates, 'Could not find any table elements');
862
863
        $table = null;
864
        foreach ($candidates as $candidate) {
865
            if (!$table && $candidate->isVisible()) {
866
                $table = $candidate;
867
            }
868
        }
869
870
        assertTrue((bool)$table, 'Found table elements, but none are visible');
871
872
        return $table;
873
    }
874
875
    /**
876
     * Checks the order of two texts.
877
     * Assumptions: the two texts appear in their conjunct parent element once
878
     * @Then /^I should see the text "(?P<textBefore>(?:[^"]|\\")*)" (before|after) the text "(?P<textAfter>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
879
     */
880
    public function theTextBeforeAfter($textBefore, $order, $textAfter, $element)
881
    {
882
        $ele = $this->getSession()->getPage()->find('css', $element);
883
        assertNotNull($ele, sprintf('%s not found', $element));
884
885
        // Check both of the texts exist in the element
886
        $text = $ele->getText();
887
        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...
888
        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...
889
890
        /// Use strpos to get the position of the first occurrence of the two texts (case-sensitive)
891
        // and compare them with the given order (before or after)
892
        if ($order === 'before') {
893
            assertTrue(strpos($text, $textBefore) < strpos($text, $textAfter));
894
        } else {
895
            assertTrue(strpos($text, $textBefore) > strpos($text, $textAfter));
896
        }
897
    }
898
899
    /**
900
    * Wait until a certain amount of seconds till I see an element  identified by a CSS selector.
901
    *
902
    * Example: Given I wait for 10 seconds until I see the ".css_element" element
903
    *
904
    * @Given /^I wait for (\d+) seconds until I see the "([^"]*)" element$/
905
    **/
906 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...
907
    {
908
        $page = $this->getSession()->getPage();
909
910
        $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...
911
            $element = $page->find('css', $selector);
912
913
            if (empty($element)) {
914
                return false;
915
            } else {
916
                return $element->isVisible();
917
            }
918
        });
919
    }
920
921
    /**
922
     * Wait until a particular element is visible, using a CSS selector. Useful for content loaded via AJAX, or only
923
     * populated after JS execution.
924
     *
925
     * Example: Given I wait until I see the "header .login-form" element
926
     *
927
     * @Given /^I wait until I see the "([^"]*)" element$/
928
     */
929 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...
930
    {
931
        $page = $this->getSession()->getPage();
932
        $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...
933
            $element = $page->find('css', $selector);
934
            if (empty($element)) {
935
                return false;
936
            } else {
937
                return ($element->isVisible());
938
            }
939
        });
940
    }
941
942
    /**
943
     * Wait until a particular string is found on the page. Useful for content loaded via AJAX, or only populated after
944
     * JS execution.
945
     *
946
     * Example: Given I wait until I see the text "Welcome back, John!"
947
     *
948
     * @Given /^I wait until I see the text "([^"]*)"$/
949
     */
950
    public function iWaitUntilISeeText($text)
951
    {
952
        $page = $this->getSession()->getPage();
953
        $session = $this->getSession();
954
        $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...
955
            $element = $page->find(
956
                'xpath',
957
                $session->getSelectorsHandler()->selectorToXpath("xpath", ".//*[contains(text(), '$text')]")
958
            );
959
960
            if (empty($element)) {
961
                return false;
962
            } else {
963
                return ($element->isVisible());
964
            }
965
        });
966
    }
967
968
    /**
969
     * @Given /^I scroll to the bottom$/
970
     */
971
    public function iScrollToBottom()
972
    {
973
        $javascript = 'window.scrollTo(0, Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, document.documentElement.clientHeight));';
974
        $this->getSession()->executeScript($javascript);
975
    }
976
977
    /**
978
     * @Given /^I scroll to the top$/
979
     */
980
    public function iScrollToTop()
981
    {
982
        $this->getSession()->executeScript('window.scrollTo(0,0);');
983
    }
984
985
    /**
986
     * Scroll to a certain element by label.
987
     * Requires an "id" attribute to uniquely identify the element in the document.
988
     *
989
     * Example: Given I scroll to the "Submit" button
990
     * Example: Given I scroll to the "My Date" field
991
     *
992
     * @Given /^I scroll to the "([^"]*)" (field|link|button)$/
993
     */
994
    public function iScrollToField($locator, $type)
995
    {
996
        $page = $this->getSession()->getPage();
997
        $el = $page->find('named', array($type, "'$locator'"));
998
        assertNotNull($el, sprintf('%s element not found', $locator));
999
1000
        $id = $el->getAttribute('id');
1001
        if (empty($id)) {
1002
            throw new \InvalidArgumentException('Element requires an "id" attribute');
1003
        }
1004
1005
        $js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
1006
        $this->getSession()->executeScript($js);
1007
    }
1008
1009
    /**
1010
     * Scroll to a certain element by CSS selector.
1011
     * Requires an "id" attribute to uniquely identify the element in the document.
1012
     *
1013
     * Example: Given I scroll to the ".css_element" element
1014
     *
1015
     * @Given /^I scroll to the "(?P<locator>(?:[^"]|\\")*)" element$/
1016
     */
1017
    public function iScrollToElement($locator)
1018
    {
1019
        $el = $this->getSession()->getPage()->find('css', $locator);
1020
        assertNotNull($el, sprintf('The element "%s" is not found', $locator));
1021
1022
        $id = $el->getAttribute('id');
1023
        if (empty($id)) {
1024
            throw new \InvalidArgumentException('Element requires an "id" attribute');
1025
        }
1026
1027
        $js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
1028
        $this->getSession()->executeScript($js);
1029
    }
1030
1031
    /**
1032
     * Continuously poll the dom until callback returns true, code copied from
1033
     * (@link http://docs.behat.org/cookbook/using_spin_functions.html)
1034
     * If not found within a given wait period, timeout and throw error
1035
     *
1036
     * @param callback $lambda The function to run continuously
1037
     * @param integer $wait Timeout in seconds
1038
     * @return bool Returns true if the lambda returns successfully
1039
     * @throws \Exception Thrown if the wait threshold is exceeded without the lambda successfully returning
1040
     */
1041
    public function spin($lambda, $wait = 60)
1042
    {
1043
        for ($i = 0; $i < $wait; $i++) {
1044
            try {
1045
                if ($lambda($this)) {
1046
                    return true;
1047
                }
1048
            } catch (\Exception $e) {
1049
                // do nothing
1050
            }
1051
1052
            sleep(1);
1053
        }
1054
1055
        $backtrace = debug_backtrace();
1056
1057
        throw new \Exception(sprintf(
1058
            "Timeout thrown by %s::%s()\n.",
1059
            $backtrace[1]['class'],
1060
            $backtrace[1]['function']
1061
        ));
1062
    }
1063
1064
1065
1066
    /**
1067
     * We have to catch exceptions and log somehow else otherwise behat falls over
1068
     */
1069
    protected function logException($e)
1070
    {
1071
        file_put_contents('php://stderr', 'Exception caught: '.$e);
1072
    }
1073
}
1074