Completed
Pull Request — 1.0 (#107)
by
unknown
04:47
created

iClickInTheElementConfirmingTheDialog()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 3
1
<?php
2
3
namespace SilverStripe\BehatExtension\Context;
4
5
use Behat\Behat\Context\ClosuredContextInterface,
6
    Behat\Behat\Context\TranslatedContextInterface,
7
    Behat\Behat\Context\BehatContext,
8
    Behat\Behat\Context\Step,
9
    Behat\Behat\Event\StepEvent,
10
    Behat\Behat\Event\ScenarioEvent,
11
    Behat\Behat\Exception\PendingException;
12
13
use Behat\Mink\Driver\Selenium2Driver;
14
15
use Behat\Gherkin\Node\PyStringNode,
16
    Behat\Gherkin\Node\TableNode;
17
18
// PHPUnit
19
require_once 'PHPUnit/Autoload.php';
20
require_once 'PHPUnit/Framework/Assert/Functions.php';
21
22
/**
23
 * BasicContext
24
 *
25
 * Context used to define generic steps like following anchors or pressing buttons.
26
 * Handles timeouts.
27
 * Handles redirections.
28
 * Handles AJAX enabled links, buttons and forms - jQuery is assumed.
29
 */
30
class BasicContext extends BehatContext
31
{
32
    protected $context;
33
34
    /**
35
	 * Date format in date() syntax
36
	 * @var String
37
	 */
38
	protected $dateFormat = 'Y-m-d';
39
40
	/**
41
	 * Time format in date() syntax
42
	 * @var String
43
	 */
44
	protected $timeFormat = 'H:i:s';
45
46
	/**
47
	 * Date/time format in date() syntax
48
	 * @var String
49
	 */
50
	protected $datetimeFormat = 'Y-m-d H:i:s';
51
52
    /**
53
     * Initializes context.
54
     * Every scenario gets it's own context object.
55
     *
56
     * @param   array   $parameters     context parameters (set them up through behat.yml)
57
     */
58
	public function __construct(array $parameters) {
59
        // Initialize your context here
60
        $this->context = $parameters;
61
    }
62
63
	/**
64
	 * Get Mink session from MinkContext
65
	 *
66
	 * @return \Behat\Mink\Session
67
	 */
68
	public function getSession($name = null) {
69
		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...
70
	}
71
72
    /**
73
     * @AfterStep ~@modal
74
     *
75
     * Excluding scenarios with @modal tag is required,
76
     * because modal dialogs stop any JS interaction
77
     */
78
	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...
79
		try{
80
			$javascript = <<<JS
81
window.onerror = function(message, file, line, column, error) {
82
    var body = document.getElementsByTagName('body')[0];
83
	var msg = message + " in " + file + ":" + line + ":" + column;
84
	if(error !== undefined && error.stack !== undefined) {
85
		msg += "\\nSTACKTRACE:\\n" + error.stack;
86
	}
87
	body.setAttribute('data-jserrors', '[captured JavaScript error] ' + msg);
88
}
89
if ('undefined' !== typeof window.jQuery) {
90
    window.jQuery('body').ajaxError(function(event, jqxhr, settings, exception) {
91
        if ('abort' === exception) return;
92
        window.onerror(event.type + ': ' + settings.type + ' ' + settings.url + ' ' + exception + ' ' + jqxhr.responseText);
93
    });
94
}
95
JS;
96
97
			$this->getSession()->executeScript($javascript);
98
        }catch(\WebDriver\Exception $e){
99
        	$this->logException($e);
100
        }
101
    }
102
103
    /**
104
     * @AfterStep ~@modal
105
     *
106
     * Excluding scenarios with @modal tag is required,
107
     * because modal dialogs stop any JS interaction
108
     */
109
	public function readErrorHandlerAfterStep(StepEvent $event) {
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
		try{
142
			$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...
143
			$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
144
145
			if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText())) {
146
				return;
147
			}
148
149
			$javascript = <<<JS
150
if ('undefined' !== typeof window.jQuery && 'undefined' !== typeof window.jQuery.fn.on) {
151
    window.jQuery(document).on('ajaxStart.ss.test.behaviour', function(){
152
        window.__ajaxStatus = function() {
153
            return 'waiting';
154
        };
155
    });
156
    window.jQuery(document).on('ajaxComplete.ss.test.behaviour', function(e, jqXHR){
157
        if (null === jqXHR.getResponseHeader('X-ControllerURL')) {
158
            window.__ajaxStatus = function() {
159
                return 'no ajax';
160
            };
161
        }
162
    });
163
    window.jQuery(document).on('ajaxSuccess.ss.test.behaviour', function(e, jqXHR){
164
        if (null === jqXHR.getResponseHeader('X-ControllerURL')) {
165
            window.__ajaxStatus = function() {
166
                return 'success';
167
            };
168
        }
169
    });
170
}
171
JS;
172
			$this->getSession()->wait(500); // give browser a chance to process and render response
173
			$this->getSession()->executeScript($javascript);
174
		}catch(\WebDriver\Exception $e){
175
			$this->logException($e);
176
        }
177
    }
178
179
    /**
180
     * Wait for the __ajaxStatus()to return anything but 'waiting'.
181
     * Don't wait longer than 5 seconds.
182
     *
183
     * Don't unregister handler if we're dealing with modal windows
184
     *
185
     * @AfterStep ~@modal
186
     */
187 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...
188
		try{
189
			$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...
190
			$ajaxEnabledSteps = implode('|', array_filter($ajaxEnabledSteps));
191
192
			if (empty($ajaxEnabledSteps) || !preg_match('/(' . $ajaxEnabledSteps . ')/i', $event->getStep()->getText())) {
193
				return;
194
			}
195
196
			$this->handleAjaxTimeout();
197
198
			$javascript = <<<JS
199
if ('undefined' !== typeof window.jQuery && 'undefined' !== typeof window.jQuery.fn.off) {
200
window.jQuery(document).off('ajaxStart.ss.test.behaviour');
201
window.jQuery(document).off('ajaxComplete.ss.test.behaviour');
202
window.jQuery(document).off('ajaxSuccess.ss.test.behaviour');
203
}
204
JS;
205
			$this->getSession()->executeScript($javascript);
206
		}catch(\WebDriver\Exception $e){
207
			$this->logException($e);
208
        }
209
    }
210
211
	public function handleAjaxTimeout() {
212
        $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...
213
214
        // Wait for an ajax request to complete, but only for a maximum of 5 seconds to avoid deadlocks
215
        $this->getSession()->wait($timeoutMs,
216
            "(typeof window.__ajaxStatus !== 'undefined' ? window.__ajaxStatus() : 'no ajax') !== 'waiting'"
217
        );
218
219
        // wait additional 100ms to allow DOM to update
220
        $this->getSession()->wait(100);
221
    }
222
223
    /**
224
     * Take screenshot when step fails.
225
     * Works only with Selenium2Driver.
226
     *
227
     * @AfterStep
228
     */
229
	public function takeScreenshotAfterFailedStep(StepEvent $event) {
230
		if (4 === $event->getResult()) {
231
			try{
232
				$this->takeScreenshot($event);
233
			}catch(\WebDriver\Exception $e){
234
				$this->logException($e);
235
			}
236
		}
237
	}
238
239
	/**
240
	 * Close modal dialog if test scenario fails on CMS page
241
	 *
242
	 * @AfterScenario
243
	 */
244
	public function closeModalDialog(ScenarioEvent $event) {
245
		try{
246
			// Only for failed tests on CMS page
247
			if (4 === $event->getResult()) {
248
				$cmsElement = $this->getSession()->getPage()->find('css', '.cms');
249
				if($cmsElement) {
250
					try {
251
						// Navigate away triggered by reloading the page
252
						$this->getSession()->reload();
253
						$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...
254
					} catch(\WebDriver\Exception $e) {
255
						// no-op, alert might not be present
256
					}
257
				}
258
			}
259
		}catch(\WebDriver\Exception $e){
260
			$this->logException($e);
261
		}
262
	}
263
	
264
    /**
265
     * Delete any created files and folders from assets directory
266
     *
267
     * @AfterScenario @assets
268
     */
269
	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...
270
        foreach(\File::get() as $file) {
271
            if(file_exists($file->getFullPath())) $file->delete();
272
        }
273
    }
274
275
    public function takeScreenshot(StepEvent $event) {
276
        $driver = $this->getSession()->getDriver();
277
        // quit silently when unsupported
278
        if (!($driver instanceof Selenium2Driver)) {
279
            return;
280
        }
281
282
        $parent = $event->getLogicalParent();
283
        $feature = $parent->getFeature();
284
        $step = $event->getStep();
285
        $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...
286
287
        $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...
288
        if(!$path) return; // quit silently when path is not set
289
290
        \Filesystem::makeFolder($path);
291
        $path = realpath($path);
292
293 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...
294
            file_put_contents('php://stderr', sprintf('"%s" is not valid directory and failed to create it' . PHP_EOL, $path));
295
            return;
296
        }
297
298 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...
299
            file_put_contents('php://stderr', sprintf('"%s" is not valid directory' . PHP_EOL, $path));
300
            return;
301
        }
302 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...
303
            file_put_contents('php://stderr', sprintf('"%s" directory is not writable' . PHP_EOL, $path));
304
            return;
305
        }
306
307
        $path = sprintf('%s/%s_%d.png', $path, basename($feature->getFile()), $step->getLine());
308
        $screenshot = $driver->getWebDriverSession()->screenshot();
309
        file_put_contents($path, base64_decode($screenshot));
310
        file_put_contents('php://stderr', sprintf('Saving screenshot into %s' . PHP_EOL, $path));
311
    }
312
313
    /**
314
     * @Then /^I should be redirected to "([^"]+)"/
315
     */
316
	public function stepIShouldBeRedirectedTo($url) {
317
        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...
318
            $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...
319
            $client->followRedirects(true);
320
            $client->followRedirect();
321
322
            $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...
323
324
            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...
325
        }
326
    }
327
328
    /**
329
     * @Given /^the page can't be found/
330
     */
331
	public function stepPageCantBeFound() {
332
        $page = $this->getSession()->getPage();
333
        assertTrue(
334
            // Content from ErrorPage default record
335
            $page->hasContent('Page not found')
336
            // Generic ModelAsController message
337
            || $page->hasContent('The requested page could not be found')
338
        );
339
    }
340
341
    /**
342
     * @Given /^I wait (?:for )?([\d\.]+) second(?:s?)$/
343
     */
344
	public function stepIWaitFor($secs) {
345
        $this->getSession()->wait((float)$secs*1000);
346
    }
347
348
    /**
349
     * @Given /^I press the "([^"]*)" button$/
350
     */
351
	public function stepIPressTheButton($button) {
352
        $page = $this->getSession()->getPage();
353
        $els = $page->findAll('named', array('link_or_button', "'$button'"));
354
        $matchedEl = null;
355
        foreach($els as $el) {
356
            if($el->isVisible()) $matchedEl = $el;
357
        }
358
        assertNotNull($matchedEl, sprintf('%s button not found', $button));
359
        $matchedEl->click();
360
    }
361
362
    /**
363
     * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
364
     * Example1: I press the "Remove current combo" button, confirming the dialog
365
     * Example2: I follow the "Remove current combo" link, confirming the dialog
366
     *
367
     * @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), confirming the dialog$/
368
     */
369
	public function stepIPressTheButtonConfirmingTheDialog($button) {
370
        $this->stepIPressTheButton($button);
371
        $this->iConfirmTheDialog();
372
    }
373
374
    /**
375
     * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
376
     * Example: I follow the "Remove current combo" link, dismissing the dialog
377
     *
378
     * @Given /^I (?:press|follow) the "([^"]*)" (?:button|link), dismissing the dialog$/
379
     */
380
	public function stepIPressTheButtonDismissingTheDialog($button) {
381
		$this->stepIPressTheButton($button);
382
		$this->iDismissTheDialog();
383
    }
384
385
    /**
386
     * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element$/
387
     */
388
    public function iClickInTheElement($clickType, $text, $selector) {
389
        $clickTypeMap = array(
390
            "double click" => "doubleclick",
391
            "click" => "click"
392
        );
393
        $page = $this->getSession()->getPage();
394
        $parentElement = $page->find('css', $selector);
395
        assertNotNull($parentElement, sprintf('"%s" element not found', $selector));
396
        $element = $parentElement->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
397
        assertNotNull($element, sprintf('"%s" not found', $text));
398
        $clickTypeFn = $clickTypeMap[$clickType];
399
        $element->$clickTypeFn();
400
    }
401
    
402
    /**
403
    * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
404
    * Example: I click "Delete" in the ".actions" element, confirming the dialog
405
    *
406
    * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, confirming the dialog$/
407
    */
408
   public function iClickInTheElementConfirmingTheDialog($clickType, $text, $selector) {
409
       $this->iClickInTheElement($clickType, $text, $selector);
410
       $this->iConfirmTheDialog();
411
   }
412
   /**
413
    * Needs to be in single command to avoid "unexpected alert open" errors in Selenium.
414
    * Example: I click "Delete" in the ".actions" element, dismissing the dialog
415
    *
416
    * @Given /^I (click|double click) "([^"]*)" in the "([^"]*)" element, dismissing the dialog$/
417
    */
418
   public function iClickInTheElementDismissingTheDialog($clickType, $text, $selector) {
419
       $this->iClickInTheElement($clickType, $text, $selector);
420
       $this->iDismissTheDialog();
421
   }
422
423
    /**
424
     * @Given /^I type "([^"]*)" into the dialog$/
425
     */
426
	public function iTypeIntoTheDialog($data) {
427
        $data = array(
428
            'text' => $data,
429
        );
430
        $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...
431
    }
432
433
    /**
434
     * @Given /^I confirm the dialog$/
435
     */
436
	public function iConfirmTheDialog() {
437
        $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...
438
        $this->handleAjaxTimeout();
439
    }
440
441
    /**
442
     * @Given /^I dismiss the dialog$/
443
     */
444
	public function iDismissTheDialog() {
445
        $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...
446
        $this->handleAjaxTimeout();
447
    }
448
449
    /**
450
     * @Given /^(?:|I )attach the file "(?P<path>[^"]*)" to "(?P<field>(?:[^"]|\\")*)" with HTML5$/
451
     */
452
	public function iAttachTheFileTo($field, $path) {
453
        // Remove wrapped button styling to make input field accessible to Selenium
454
        $js = <<<JS
455
var input = jQuery('[name="$field"]');
456
if(input.closest('.ss-uploadfield-item-info').length) {
457
    while(!input.parent().is('.ss-uploadfield-item-info')) input = input.unwrap();
458
}
459
JS;
460
461
        $this->getSession()->executeScript($js);
462
        $this->getSession()->wait(1000);
463
464
        return new Step\Given(sprintf('I attach the file "%s" to "%s"', $path, $field));
465
    }
466
467
	/**
468
	 * Select an individual input from within a group, matched by the top-most label.
469
	 *
470
	 * @Given /^I select "([^"]*)" from "([^"]*)" input group$/
471
	 */
472
	public function iSelectFromInputGroup($value, $labelText) {
473
		$page = $this->getSession()->getPage();
474
		$parent = null;
475
476
		foreach($page->findAll('css', 'label') as $label) {
477
			if($label->getText() == $labelText) {
478
				$parent = $label->getParent();
479
			}
480
		}
481
482
		if(!$parent) throw new \InvalidArgumentException(sprintf('Input group with label "%s" cannot be found', $labelText));
483
484
		foreach($parent->findAll('css', 'label') as $option) {
485
			if($option->getText() == $value) {
486
				$for = $option->getAttribute('for');
487
				$input = $parent->findById($for);
488
489
				if(!$input) throw new \InvalidArgumentException(sprintf('Input "%s" cannot be found', $value));
490
491
				$this->getSession()->getDriver()->click($input->getXPath());
492
			}
493
		}
494
	}
495
496
    /**
497
     * Pauses the scenario until the user presses a key. Useful when debugging a scenario.
498
     *
499
     * @Then /^(?:|I )put a breakpoint$/
500
     */
501
	public function iPutABreakpoint() {
502
        fwrite(STDOUT, "\033[s    \033[93m[Breakpoint] Press \033[1;93m[RETURN]\033[0;93m to continue...\033[0m");
503
        while (fgets(STDIN, 1024) == '') {}
504
        fwrite(STDOUT, "\033[u");
505
506
        return;
507
    }
508
509
	/**
510
	 * Transforms relative time statements compatible with strtotime().
511
	 * Example: "time of 1 hour ago" might return "22:00:00" if its currently "23:00:00".
512
	 * Customize through {@link setTimeFormat()}.
513
	 *
514
	 * @Transform /^(?:(the|a)) time of (?<val>.*)$/
515
	 */
516 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...
517
		$timestamp = strtotime($val);
518
		if(!$timestamp) {
519
			throw new \InvalidArgumentException(sprintf(
520
				"Can't resolve '%s' into a valid datetime value",
521
				$val
522
			));
523
		}
524
		return date($this->timeFormat, $timestamp);
525
	}
526
527
	/**
528
	 * Transforms relative date and time statements compatible with strtotime().
529
	 * Example: "datetime of 2 days ago" might return "2013-10-10 22:00:00" if its currently
530
	 * the 12th of October 2013. Customize through {@link setDatetimeFormat()}.
531
	 *
532
	 * @Transform /^(?:(the|a)) datetime of (?<val>.*)$/
533
	 */
534 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...
535
		$timestamp = strtotime($val);
536
		if(!$timestamp) {
537
			throw new \InvalidArgumentException(sprintf(
538
				"Can't resolve '%s' into a valid datetime value",
539
				$val
540
			));
541
		}
542
		return date($this->datetimeFormat, $timestamp);
543
	}
544
545
	/**
546
	 * Transforms relative date statements compatible with strtotime().
547
	 * Example: "date 2 days ago" might return "2013-10-10" if its currently
548
	 * the 12th of October 2013. Customize through {@link setDateFormat()}.
549
	 *
550
	 * @Transform /^(?:(the|a)) date of (?<val>.*)$/
551
	 */
552 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...
553
		$timestamp = strtotime($val);
554
		if(!$timestamp) {
555
			throw new \InvalidArgumentException(sprintf(
556
				"Can't resolve '%s' into a valid datetime value",
557
				$val
558
			));
559
		}
560
		return date($this->dateFormat, $timestamp);
561
	}
562
563
	public function getDateFormat() {
564
		return $this->dateFormat;
565
	}
566
567
	public function setDateFormat($format) {
568
		$this->dateFormat = $format;
569
	}
570
571
	public function getTimeFormat() {
572
		return $this->timeFormat;
573
	}
574
575
	public function setTimeFormat($format) {
576
		$this->timeFormat = $format;
577
	}
578
579
	public function getDatetimeFormat() {
580
		return $this->datetimeFormat;
581
	}
582
583
	public function setDatetimeFormat($format) {
584
		$this->datetimeFormat = $format;
585
	}
586
587
    /**
588
     * Checks that field with specified in|name|label|value is disabled.
589
     * Example: Then the field "Email" should be disabled
590
     * Example: Then the "Email" field should be disabled
591
     *
592
     * @Then /^the "(?P<name>(?:[^"]|\\")*)" (?P<type>(?:(field|button))) should (?P<negate>(?:(not |)))be disabled/
593
     * @Then /^the (?P<type>(?:(field|button))) "(?P<name>(?:[^"]|\\")*)" should (?P<negate>(?:(not |)))be disabled/
594
     */
595
    public function stepFieldShouldBeDisabled($name, $type, $negate) {
596
        $page = $this->getSession()->getPage();
597
        if($type == 'field') {
598
            $element = $page->findField($name);
599
        } else {
600
            $element = $page->find('named', array(
601
                'button', $this->getSession()->getSelectorsHandler()->xpathLiteral($name)
602
            ));
603
        }
604
605
        assertNotNull($element, sprintf("Element '%s' not found", $name));
606
607
        $disabledAttribute = $element->getAttribute('disabled');
608
        if(trim($negate)) {
609
            assertNull($disabledAttribute, sprintf("Failed asserting element '%s' is not disabled", $name));
610
        } else {
611
            assertNotNull($disabledAttribute, sprintf("Failed asserting element '%s' is disabled", $name));
612
        }
613
    }
614
615
	/**
616
	 * Checks that checkbox with specified in|name|label|value is enabled.
617
	 * Example: Then the field "Email" should be enabled
618
	 * Example: Then the "Email" field should be enabled
619
	 *
620
	 * @Then /^the "(?P<field>(?:[^"]|\\")*)" field should be enabled/
621
	 * @Then /^the field "(?P<field>(?:[^"]|\\")*)" should be enabled/
622
	 */
623
	public function stepFieldShouldBeEnabled($field) {
624
		$page = $this->getSession()->getPage();
625
		$fieldElement = $page->findField($field);
626
		assertNotNull($fieldElement, sprintf("Field '%s' not found", $field));
627
628
		$disabledAttribute = $fieldElement->getAttribute('disabled');
629
630
		assertNull($disabledAttribute, sprintf("Failed asserting field '%s' is enabled", $field));
631
	}
632
633
    /**
634
     * Clicks a link in a specific region (an element identified by a CSS selector, a "data-title" attribute,
635
     * or a named region mapped to a CSS selector via Behat configuration).
636
     *
637
     * Example: Given I follow "Select" in the "header .login-form" region
638
     * Example: Given I follow "Select" in the "My Login Form" region
639
     *
640
     * @Given /^I (?:follow|click) "(?P<link>[^"]*)" in the "(?P<region>[^"]*)" region$/
641
     */
642 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...
643
        $context = $this->getMainContext();
644
        $regionObj = $context->getRegionObj($region);
645
        assertNotNull($regionObj);
646
647
        $linkObj = $regionObj->findLink($link);
648
        if (empty($linkObj)) {
649
			throw new \Exception(sprintf('The link "%s" was not found in the region "%s" 
650
				on the page %s', $link, $region, $this->getSession()->getCurrentUrl()));
651
        }
652
653
        $linkObj->click();
654
    }
655
656
    /**
657
     * Fills in a field in a specfic region similar to (@see iFollowInTheRegion or @see iSeeTextInRegion)
658
     *
659
     * Example: Given I fill in "Hello" with "World"
660
     *
661
     * @Given /^I fill in "(?P<field>[^"]*)" with "(?P<value>[^"]*)" in the "(?P<region>[^"]*)" region$/
662
     */
663 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...
664
        $context = $this->getMainContext();
665
        $regionObj = $context->getRegionObj($region);
666
        assertNotNull($regionObj, "Region Object is null");
667
668
        $fieldObj = $regionObj->findField($field);
669
        if (empty($fieldObj)) {
670
			throw new \Exception(sprintf('The field "%s" was not found in the region "%s" 
671
				on the page %s', $field, $region, $this->getSession()->getCurrentUrl()));
672
        }
673
674
        $regionObj->fillField($field, $value);
675
    }
676
677
678
    /**
679
     * Asserts text in a specific region (an element identified by a CSS selector, a "data-title" attribute,
680
     * or a named region mapped to a CSS selector via Behat configuration).
681
     * Supports regular expressions in text value.
682
     *
683
     * Example: Given I should see "My Text" in the "header .login-form" region
684
     * Example: Given I should not see "My Text" in the "My Login Form" region
685
     *
686
     * @Given /^I should (?P<negate>(?:(not |)))see "(?P<text>[^"]*)" in the "(?P<region>[^"]*)" region$/
687
     */
688
    public function iSeeTextInRegion($negate, $text, $region) {
689
        $context = $this->getMainContext();
690
        $regionObj = $context->getRegionObj($region);
691
        assertNotNull($regionObj);
692
693
        $actual = $regionObj->getText();
694
        $actual = preg_replace('/\s+/u', ' ', $actual);
695
        $regex  = '/'.preg_quote($text, '/').'/ui';
696
697
        if(trim($negate)) {
698 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...
699
                $message = sprintf(
700
                    'The text "%s" was found in the text of the "%s" region on the page %s.',
701
                    $text,
702
                    $region,
703
                    $this->getSession()->getCurrentUrl()
704
                );
705
706
                throw new \Exception($message);
707
            }
708 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...
709
            if (!preg_match($regex, $actual)) {
710
                $message = sprintf(
711
                    'The text "%s" was not found anywhere in the text of the "%s" region on the page %s.',
712
                    $text,
713
                    $region,
714
                    $this->getSession()->getCurrentUrl()
715
                );
716
717
                throw new \Exception($message);
718
            }
719
        }
720
721
    }
722
723
	/**
724
	 * Selects the specified radio button
725
	 *
726
	 * @Given /^I select the "([^"]*)" radio button$/
727
	 */
728
	public function iSelectTheRadioButton($radioLabel) {
729
		$session = $this->getSession();
730
		$radioButton = $session->getPage()->find('named', array(
731
                      'radio', $this->getSession()->getSelectorsHandler()->xpathLiteral($radioLabel)
732
                  ));
733
		assertNotNull($radioButton);
734
		$session->getDriver()->click($radioButton->getXPath());
735
	}
736
737
    /**
738
     * @Then /^the "([^"]*)" table should contain "([^"]*)"$/
739
     */
740
    public function theTableShouldContain($selector, $text) {
741
        $table = $this->getTable($selector);
742
743
        $element = $table->find('named', array('content', "'$text'"));
744
        assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
745
    }
746
747
    /**
748
     * @Then /^the "([^"]*)" table should not contain "([^"]*)"$/
749
     */
750
    public function theTableShouldNotContain($selector, $text) {
751
        $table = $this->getTable($selector);
752
753
        $element = $table->find('named', array('content', "'$text'"));
754
        assertNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $selector));
755
    }
756
757
    /**
758
     * @Given /^I click on "([^"]*)" in the "([^"]*)" table$/
759
     */
760
    public function iClickOnInTheTable($text, $selector) {
761
        $table = $this->getTable($selector);
762
763
        $element = $table->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text));
764
        assertNotNull($element, sprintf('Element containing `%s` not found', $text));
765
        $element->click();
766
    }
767
768
    /**
769
     * Finds the first visible table by various factors:
770
     * - table[id]
771
     * - table[title]
772
     * - table *[class=title]
773
     * - fieldset[data-name] table
774
     * - table caption
775
     *
776
     * @return Behat\Mink\Element\NodeElement
777
     */
778
    protected function getTable($selector) {
779
        $selector = $this->getSession()->getSelectorsHandler()->xpathLiteral($selector);
780
        $page = $this->getSession()->getPage();
781
        $candidates = $page->findAll(
782
            'xpath',
783
            $this->getSession()->getSelectorsHandler()->selectorToXpath(
784
                "xpath", ".//table[(./@id = $selector or  contains(./@title, $selector))]"
785
            )
786
        );
787
788
        // Find tables by a <caption> field
789
		$candidates += $page->findAll('xpath', "//table//caption[contains(normalize-space(string(.)), 
790
			$selector)]/ancestor-or-self::table[1]");
791
792
        // Find tables by a .title node
793
		$candidates += $page->findAll('xpath', "//table//*[@class='title' and contains(normalize-space(string(.)), 
794
			$selector)]/ancestor-or-self::table[1]");
795
796
        // Some tables don't have a visible title, so look for a fieldset with data-name instead
797
        $candidates += $page->findAll('xpath', "//fieldset[@data-name=$selector]//table");
798
799
        assertTrue((bool)$candidates, 'Could not find any table elements');
800
801
        $table = null;
802
        foreach($candidates as $candidate) {
803
            if(!$table && $candidate->isVisible()) {
804
                $table = $candidate;
805
            }
806
        }
807
808
        assertTrue((bool)$table, 'Found table elements, but none are visible');
809
810
        return $table;
811
    }
812
813
	/**
814
	 * Checks the order of two texts.
815
	 * Assumptions: the two texts appear in their conjunct parent element once
816
	 * @Then /^I should see the text "(?P<textBefore>(?:[^"]|\\")*)" (before|after) the text "(?P<textAfter>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
817
	 */
818
	public function theTextBeforeAfter($textBefore, $order, $textAfter, $element) {
819
		$ele = $this->getSession()->getPage()->find('css', $element);
820
		assertNotNull($ele, sprintf('%s not found', $element));
821
822
		// Check both of the texts exist in the element
823
		$text = $ele->getText();
824
		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...
825
		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...
826
827
		/// Use strpos to get the position of the first occurrence of the two texts (case-sensitive)
828
		// and compare them with the given order (before or after)
829
		if($order === 'before') {
830
			assertTrue(strpos($text, $textBefore) < strpos($text, $textAfter));
831
		} else {
832
			assertTrue(strpos($text, $textBefore) > strpos($text, $textAfter));
833
		}
834
	}
835
836
	/**
837
	* Wait until a certain amount of seconds till I see an element  identified by a CSS selector.
838
	*
839
	* Example: Given I wait for 10 seconds until I see the ".css_element" element
840
	*
841
	* @Given /^I wait for (\d+) seconds until I see the "([^"]*)" element$/
842
	**/
843 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...
844
		$page = $this->getSession()->getPage();
845
846
		$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...
847
			$element = $page->find('css', $selector);
848
849
			if(empty($element)) {
850
				return false;
851
			} else {
852
				return $element->isVisible();
853
			}
854
		});
855
	}
856
857
    /**
858
     * Wait until a particular element is visible, using a CSS selector. Useful for content loaded via AJAX, or only
859
     * populated after JS execution.
860
     *
861
     * Example: Given I wait until I see the "header .login-form" element
862
     *
863
     * @Given /^I wait until I see the "([^"]*)" element$/
864
     */
865 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...
866
        $page = $this->getSession()->getPage();
867
        $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...
868
            $element = $page->find('css', $selector);
869
            if(empty($element)){
870
                return false;
871
            } else{
872
                return ($element->isVisible());
873
            }
874
        });
875
    }
876
877
    /**
878
     * Wait until a particular string is found on the page. Useful for content loaded via AJAX, or only populated after
879
     * JS execution.
880
     *
881
     * Example: Given I wait until I see the text "Welcome back, John!"
882
     *
883
     * @Given /^I wait until I see the text "([^"]*)"$/
884
     */
885
    public function iWaitUntilISeeText($text){
886
        $page = $this->getSession()->getPage();
887
        $session = $this->getSession();
888
        $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...
889
            $element = $page->find(
890
                'xpath',
891
                $session->getSelectorsHandler()->selectorToXpath("xpath", ".//*[contains(text(), '$text')]")
892
            );
893
894
            if(empty($element)) {
895
                return false;
896
            } else {
897
                return ($element->isVisible());
898
            }
899
        });
900
    }
901
902
	/**
903
	 * @Given /^I scroll to the bottom$/
904
	 */
905
	public function iScrollToBottom() {
906
		$javascript = 'window.scrollTo(0, Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, document.documentElement.clientHeight));';
907
		$this->getSession()->executeScript($javascript);
908
	}
909
910
	/**
911
	 * @Given /^I scroll to the top$/
912
	 */
913
	public function iScrollToTop() {
914
		$this->getSession()->executeScript('window.scrollTo(0,0);');
915
	}
916
917
	/**
918
	 * Scroll to a certain element by label.
919
	 * Requires an "id" attribute to uniquely identify the element in the document.
920
	 *
921
	 * Example: Given I scroll to the "Submit" button
922
	 * Example: Given I scroll to the "My Date" field
923
	 *
924
	 * @Given /^I scroll to the "([^"]*)" (field|link|button)$/
925
	 */
926
	public function iScrollToField($locator, $type) {
927
		$page = $this->getSession()->getPage();
928
        $el = $page->find('named', array($type, "'$locator'"));
929
        assertNotNull($el, sprintf('%s element not found', $locator));
930
931
        $id = $el->getAttribute('id');
932
		if(empty($id)) {
933
			throw new \InvalidArgumentException('Element requires an "id" attribute');
934
		}
935
936
		$js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
937
		$this->getSession()->executeScript($js);
938
	}
939
940
	/**
941
	 * Scroll to a certain element by CSS selector.
942
	 * Requires an "id" attribute to uniquely identify the element in the document.
943
	 *
944
	 * Example: Given I scroll to the ".css_element" element
945
	 *
946
	 * @Given /^I scroll to the "(?P<locator>(?:[^"]|\\")*)" element$/
947
	 */
948
	public function iScrollToElement($locator) {
949
		$el = $this->getSession()->getPage()->find('css', $locator);
950
		assertNotNull($el, sprintf('The element "%s" is not found', $locator));
951
952
		$id = $el->getAttribute('id');
953
		if(empty($id)) {
954
			throw new \InvalidArgumentException('Element requires an "id" attribute');
955
		}
956
957
		$js = sprintf("document.getElementById('%s').scrollIntoView(true);", $id);
958
		$this->getSession()->executeScript($js);
959
	}
960
961
    /**
962
     * Continuously poll the dom until callback returns true, code copied from
963
     * (@link http://docs.behat.org/cookbook/using_spin_functions.html)
964
     * If not found within a given wait period, timeout and throw error
965
     *
966
     * @param callback $lambda The function to run continuously
967
     * @param integer $wait Timeout in seconds
968
     * @return bool Returns true if the lambda returns successfully
969
     * @throws \Exception Thrown if the wait threshold is exceeded without the lambda successfully returning
970
     */
971
    public function spin($lambda, $wait = 60) {
972
        for ($i = 0; $i < $wait; $i++) {
973
            try {
974
                if($lambda($this)) {
975
                    return true;
976
                }
977
            } catch (\Exception $e) {
978
                // do nothing
979
            }
980
981
            sleep(1);
982
        }
983
984
        $backtrace = debug_backtrace();
985
986
        throw new \Exception(sprintf(
987
            "Timeout thrown by %s::%s()\n.",
988
            $backtrace[1]['class'],
989
            $backtrace[1]['function']
990
        ));
991
    }
992
	
993
	
994
	
995
	/**
996
	 * We have to catch exceptions and log somehow else otherwise behat falls over
997
	 */
998
	protected function logException($e){
999
		file_put_contents('php://stderr', 'Exception caught: '.$e);
1000
	}
1001
	
1002
}
1003